Merge branch 'main' into persistence-2-electric-boogaloo

# Conflicts:
#	Cargo.lock
#	minisql/Cargo.toml
#	minisql/src/internals/row.rs
#	minisql/src/interpreter.rs
#	minisql/src/schema.rs
#	minisql/src/type_system.rs
This commit is contained in:
Jindřich Moravec 2024-01-28 15:34:14 +01:00
commit 6bf4e34006
28 changed files with 1146 additions and 654 deletions

253
Cargo.lock generated
View file

@ -17,6 +17,54 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "anstream"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
]
[[package]]
name = "anyhow"
version = "1.0.76"
@ -110,6 +158,46 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
dependencies = [
"heck",
"proc-macro2 1.0.78",
"quote 1.0.35",
"syn 2.0.48",
]
[[package]]
name = "clap_lex"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
[[package]]
name = "client"
version = "0.1.0"
@ -121,12 +209,35 @@ dependencies = [
"tokio",
]
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "getrandom"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "gimli"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
version = "0.3.3"
@ -167,6 +278,7 @@ version = "0.1.0"
dependencies = [
"bimap",
"serde",
"thiserror",
]
[[package]]
@ -186,7 +298,7 @@ checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
dependencies = [
"libc",
"wasi",
"windows-sys",
"windows-sys 0.48.0",
]
[[package]]
@ -260,7 +372,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
"windows-targets 0.48.5",
]
[[package]]
@ -280,6 +392,12 @@ version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "0.4.30"
@ -326,6 +444,36 @@ dependencies = [
"proc-macro2 1.0.78",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
@ -372,9 +520,12 @@ name = "server"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"clap",
"minisql",
"parser",
"proto",
"rand",
"tokio",
]
@ -400,9 +551,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
dependencies = [
"libc",
"windows-sys",
"windows-sys 0.48.0",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "0.15.44"
@ -461,7 +618,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys",
"windows-sys 0.48.0",
]
[[package]]
@ -487,6 +644,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "version_check"
version = "0.1.5"
@ -511,7 +674,16 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.0",
]
[[package]]
@ -520,13 +692,28 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
dependencies = [
"windows_aarch64_gnullvm 0.52.0",
"windows_aarch64_msvc 0.52.0",
"windows_i686_gnu 0.52.0",
"windows_i686_msvc 0.52.0",
"windows_x86_64_gnu 0.52.0",
"windows_x86_64_gnullvm 0.52.0",
"windows_x86_64_msvc 0.52.0",
]
[[package]]
@ -535,38 +722,80 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"

View file

@ -8,3 +8,4 @@ edition = "2021"
[dependencies]
bimap = { version = "0.6.3", features = ["serde"] }
serde = { version = "1.0.196", features = ["derive"] }
thiserror = "1.0.50"

View file

@ -1,17 +1,33 @@
use std::num::{ParseFloatError, ParseIntError};
use std::str::Utf8Error;
use thiserror::Error;
use crate::internals::row::ColumnPosition;
use crate::schema::{ColumnName, TableName};
use crate::operation::InsertionValues;
use crate::type_system::{DbType, Uuid, Value};
#[derive(Debug)]
#[derive(Debug, Error)]
pub enum Error {
TableDoesNotExist(TableName),
ColumnDoesNotExist(TableName, ColumnName),
#[error("column position {1} of table {0} does not exist")]
ColumnPositionDoesNotExist(TableName, ColumnPosition),
#[error("column {1} of table {0} has unexpected type {2:?} and value {3:?}")]
ValueDoesNotMatchExpectedType(TableName, ColumnName, DbType, Value),
#[error("table {0} already contains row with id {1}")]
AttemptingToInsertAlreadyPresentId(TableName, Uuid),
MissingTypeAnnotationOfColumn(TableName, ColumnPosition),
MissingColumnInInsertValues(TableName, ColumnName, InsertionValues),
MismatchBetweenInsertValuesAndColumns(TableName, InsertionValues),
#[error("table {0} cannot be indexed on column {1}")]
AttemptToIndexNonIndexableColumn(TableName, ColumnName),
}
#[derive(Debug, Error)]
pub enum TypeConversionError {
#[error("failed to decode bytes to string")]
TextDecodeFailed(#[from] Utf8Error),
#[error("failed to parse float from text")]
NumberDecodeFailed(#[from] ParseFloatError),
#[error("failed to parse int from text")]
IntDecodeFailed(#[from] ParseIntError),
#[error("unknown type with oid {oid} and size {size}")]
UnknownType {
oid: i32,
size: i16
}
}

View file

@ -1,7 +1,9 @@
use crate::type_system::Value;
use crate::operation::InsertionValues;
use std::ops::{Index, IndexMut};
use std::slice::SliceIndex;
use serde::{Deserialize, Serialize};
use crate::restricted_row::RestrictedRow;
pub type ColumnPosition = usize;
@ -43,6 +45,10 @@ impl Row {
Row(vec![])
}
pub fn new_from_insertion_values(insertion_values: InsertionValues) -> Self {
Row(insertion_values)
}
pub fn with_number_of_columns(number_of_columns: usize) -> Self {
Row(Vec::with_capacity(number_of_columns))
}
@ -59,14 +65,15 @@ impl Row {
self.0.get(column_position)
}
pub fn restrict_columns(&self, columns: &Vec<ColumnPosition>) -> Row {
pub fn restrict_columns(&self, columns: &Vec<ColumnPosition>) -> RestrictedRow {
// If the index from `columns` is non-existant in `row`, it will just ignore it.
let mut subrow: Row = Row::new();
let mut subrow: Vec<(ColumnPosition, Value)> = vec![];
for column_position in columns {
if let Some(value) = self.get(*column_position) {
subrow.0.push(value.clone())
subrow.push((*column_position, value.clone()));
}
}
subrow
subrow.into()
}
}

View file

@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
use crate::error::Error;
use crate::internals::column_index::ColumnIndex;
use crate::internals::row::{ColumnPosition, Row};
use crate::restricted_row::RestrictedRow;
use crate::schema::{ColumnName, TableSchema, TableName};
use crate::result::DbResult;
use crate::type_system::{IndexableValue, Uuid, Value};
@ -68,7 +69,7 @@ impl Table {
.collect()
}
pub fn select_all_rows<'a>(&'a self, selected_column_positions: Vec<ColumnPosition>) -> impl Iterator<Item=Row> + 'a {
pub fn select_all_rows<'a>(&'a self, selected_column_positions: Vec<ColumnPosition>) -> impl Iterator<Item=RestrictedRow> + 'a {
self.rows
.values()
.map(move |row| row.restrict_columns(&selected_column_positions))
@ -79,7 +80,7 @@ impl Table {
selected_column_positions: Vec<ColumnPosition>,
column_position: ColumnPosition,
value: Value,
) -> DbResult<impl Iterator<Item=Row> + 'a> {
) -> DbResult<impl Iterator<Item=RestrictedRow> + 'a> {
let restrict_columns_of_row = move |row: Row| row.restrict_columns(&selected_column_positions);
match value {
Value::Indexable(value) => match self.fetch_ids_from_index(column_position, &value)? {
@ -207,6 +208,7 @@ impl Table {
match value {
IndexableValue::Uuid(id) => Ok(Some(HashSet::from([*id]))),
_ => {
// TODO: This validation step is not really necessary.
let column_name: ColumnName = self
.schema
.column_name_from_column_position(column_position)?;
@ -223,8 +225,8 @@ impl Table {
match self.indexes.get(&column_position) {
Some(index) => {
// Note that we are cloning the ids here! This can be very wasteful in some cases.
// It would be possible to just return a reference,
// but this seems fairly non-trivial.
// Theoretically it would be possible to return a reference,
// but after attempting to do this it seems very non-trivial.
let ids = index.get(value).cloned();
Ok(ids)
}

View file

@ -1,12 +1,11 @@
use crate::error::Error;
use crate::internals::row::{ColumnPosition, Row};
use crate::internals::row::ColumnPosition;
use crate::schema::{TableName, TableSchema};
use crate::internals::table::Table;
use crate::operation::{ColumnSelection, Condition, Operation};
use crate::operation::{Operation, Condition};
use crate::result::DbResult;
use crate::type_system::{DbType, IndexableValue, Value};
use bimap::BiMap;
use serde::{Deserialize, Serialize};
use crate::restricted_row::RestrictedRow;
// Use `TablePosition` as index
pub type Tables = Vec<Table>;
@ -21,18 +20,20 @@ pub struct State {
// #[derive(Debug)]
pub enum Response<'a> {
Selected(Box<dyn Iterator<Item=Row> + 'a + Send>),
Selected(&'a TableSchema, Box<dyn Iterator<Item=RestrictedRow> + 'a + Send>),
Inserted,
Deleted(usize), // how many were deleted
TableCreated,
IndexCreated,
}
pub type DbSchema<'a> = Vec<(TableName, TablePosition, &'a TableSchema)>;
impl std::fmt::Debug for Response<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
use Response::*;
match self {
Selected(_rows) =>
Selected(_schema, _rows) =>
// TODO: How can we iterate through the rows without having to take ownership of
// them?
f.write_str("Some rows... trust me"),
@ -56,37 +57,21 @@ impl State {
}
}
/// TODO: return a reference to avoid allocations
pub fn metadata<'a>(&'a self) -> Vec<(String, &'a TableSchema)> {
let mut m = Vec::new();
for (name, pos) in &self.table_name_position_mapping {
let table_schema = self.tables.get(*pos).unwrap().schema();
m.push((name.clone(), table_schema));
pub fn db_schema<'a>(&'a self) -> DbSchema {
let mut schema: DbSchema = Vec::new();
for (table_name, &table_position) in &self.table_name_position_mapping {
let table_schema = self.tables[table_position].schema();
schema.push((table_name.clone(), table_position, table_schema));
}
m
schema
}
fn table_from_name<'a>(&'a self, table_name: &TableName) -> DbResult<&'a Table> {
match self.table_name_position_mapping.get_by_left(table_name) {
Some(table_position) => {
let table = &self.tables[*table_position];
Ok(table)
}
None => Err(Error::TableDoesNotExist(table_name.clone())),
}
fn table_at<'a>(&'a self, table_position: TablePosition) -> &'a Table {
&self.tables[table_position]
}
fn table_from_name_mut<'b: 'a, 'a>(
&'b mut self,
table_name: &TableName,
) -> DbResult<&'a mut Table> {
match self.table_name_position_mapping.get_by_left(table_name) {
Some(table_position) => {
let table = &mut self.tables[*table_position];
Ok(table)
}
None => Err(Error::TableDoesNotExist(table_name.clone())),
}
fn table_at_mut<'a>(&'a mut self, table_position: TablePosition) -> &'a mut Table {
&mut self.tables[table_position]
}
fn attach_table(&mut self, table_name: TableName, table: Table) {
@ -101,51 +86,42 @@ impl State {
use Operation::*;
match operation {
Select(table_name, column_selection, maybe_condition) => {
let table: &Table = self.table_from_name(&table_name)?;
Select(table_position, column_selection, maybe_condition) => {
let table: &Table = self.table_at(table_position);
let selected_column_positions: Vec<ColumnPosition> = table
.schema()
.column_positions_from_column_selection(&column_selection)?;
let selected_rows = match maybe_condition {
None => {
let x = table.select_all_rows(selected_column_positions);
Box::new(x) as Box<dyn Iterator<Item=Row> + 'a + Send>
let rows = table.select_all_rows(column_selection);
Box::new(rows) as Box<dyn Iterator<Item=RestrictedRow> + 'a + Send>
},
Some(Condition::Eq(eq_column_name, value)) => {
let eq_column_position = table
.schema()
.column_position_from_column_name(&eq_column_name)?;
Some(Condition::Eq(eq_column, value)) => {
let x =
table.select_rows_where_eq(
selected_column_positions,
eq_column_position,
column_selection,
eq_column,
value,
)?;
Box::new(x) as Box<dyn Iterator<Item=Row> + 'a + Send>
Box::new(x) as Box<dyn Iterator<Item=RestrictedRow> + 'a + Send>
}
};
Ok(Response::Selected(selected_rows))
}
Insert(table_name, values) => {
let table: &mut Table = self.table_from_name_mut(&table_name)?;
Ok(Response::Selected(table.schema(), selected_rows))
},
Insert(table_position, values) => {
let table: &mut Table = self.table_at_mut(table_position);
let (id, row) = table.schema().row_from_insertion_values(values)?;
table.insert_row_at(id, row)?;
Ok(Response::Inserted)
}
Delete(table_name, maybe_condition) => {
let table: &mut Table = self.table_from_name_mut(&table_name)?;
Delete(table_position, maybe_condition) => {
let table: &mut Table = self.table_at_mut(table_position);
let rows_affected = match maybe_condition {
None => table.delete_all_rows(),
Some(Condition::Eq(eq_column_name, value)) => {
let eq_column_position = table
.schema()
.column_position_from_column_name(&eq_column_name)?;
table.delete_rows_where_eq(eq_column_position, value)?
Some(Condition::Eq(eq_column, value)) => {
table.delete_rows_where_eq(eq_column, value)?
}
};
@ -157,13 +133,9 @@ impl State {
Ok(Response::TableCreated)
}
CreateIndex(table_name, column_name) => {
let table: &mut Table = self.table_from_name_mut(&table_name)?;
let column_position: ColumnPosition = table
.schema()
.column_position_from_column_name(&column_name)?;
table.attach_index(column_position)?;
CreateIndex(table_position, column) => {
let table: &mut Table = self.table_at_mut(table_position);
table.attach_index(column)?;
Ok(Response::IndexCreated)
}
}
@ -173,7 +145,10 @@ impl State {
#[cfg(test)]
mod tests {
use super::*;
use crate::internals::row::ColumnPosition;
use std::collections::HashSet;
use crate::type_system::{DbType, IndexableValue, Value};
use crate::operation::Operation;
fn users_schema() -> TableSchema {
let id: ColumnPosition = 0;
@ -214,33 +189,22 @@ mod tests {
let mut state = State::new();
let users_schema = users_schema();
let users = users_schema.table_name().clone();
let users_position = 0;
state
.interpret(Operation::CreateTable(users.clone(), users_schema))
.interpret(Operation::CreateTable(users, users_schema.clone()))
.unwrap();
let response: Response = state
.interpret(Operation::Select(users.clone(), ColumnSelection::All, None))
.interpret(Operation::Select(users_position, users_schema.all_selection(), None))
.unwrap();
assert!(matches!(response, Response::Selected(_)));
let Response::Selected(rows) = response else {
assert!(matches!(response, Response::Selected(_, _)));
let Response::Selected(_schema, rows) = response else {
panic!()
};
let rows: Vec<_> = rows.collect();
assert!(rows.len() == 0);
}
#[test]
fn test_select_nonexistant_table() {
let mut state = State::new();
let response: DbResult<Response> = state.interpret(Operation::Select(
"table_that_doesnt_exist".to_string(),
ColumnSelection::All,
None,
));
assert!(matches!(response, Err(Error::TableDoesNotExist(_))));
}
#[test]
fn test_insert_select_basic1() {
use IndexableValue::*;
@ -248,10 +212,11 @@ mod tests {
let mut state = State::new();
let users_schema = users_schema();
let users = users_schema.table_name().clone();
let users = 0;
state
.interpret(Operation::CreateTable(users.clone(), users_schema))
.interpret(Operation::CreateTable("users".to_string(), users_schema.clone()))
.unwrap();
let (id, name, age) = (
@ -261,21 +226,21 @@ mod tests {
);
state
.interpret(Operation::Insert(
users.clone(),
users,
vec![
("id".to_string(), id.clone()),
("name".to_string(), name.clone()),
("age".to_string(), age.clone()),
id.clone(),
name.clone(),
age.clone(),
],
))
.unwrap();
let response: Response = state
.interpret(Operation::Select(users.clone(), ColumnSelection::All, None))
.interpret(Operation::Select(users, users_schema.all_selection(), None))
.unwrap();
assert!(matches!(response, Response::Selected(_)));
let Response::Selected(rows) = response else {
assert!(matches!(response, Response::Selected(_, _)));
let Response::Selected(_schema, rows) = response else {
panic!()
};
let rows: Vec<_> = rows.collect();
@ -283,14 +248,13 @@ mod tests {
let row = &rows[0];
assert!(row.len() == 3);
assert!(row[0] == id);
assert!(row[1] == name);
assert!(row[2] == age);
assert!(row[0].1 == id);
assert!(row[1].1 == name);
assert!(row[2].1 == age);
}
#[test]
fn test_insert_select_basic2() {
use ColumnSelection::*;
use Condition::*;
use IndexableValue::*;
use Operation::*;
@ -298,10 +262,13 @@ mod tests {
let mut state = State::new();
let users_schema = users_schema();
let users = users_schema.table_name().clone();
let users_position: TablePosition = 0;
let id_column: ColumnPosition = 0;
let name_column: ColumnPosition = 1;
state
.interpret(CreateTable(users.clone(), users_schema))
.interpret(CreateTable(users_schema.table_name().clone(), users_schema.clone()))
.unwrap();
let (id0, name0, age0) = (
@ -311,11 +278,11 @@ mod tests {
);
state
.interpret(Insert(
users.clone(),
users_position,
vec![
("id".to_string(), id0.clone()),
("name".to_string(), name0.clone()),
("age".to_string(), age0.clone()),
id0.clone(),
name0.clone(),
age0.clone(),
],
))
.unwrap();
@ -327,48 +294,49 @@ mod tests {
);
state
.interpret(Insert(
users.clone(),
users_position,
vec![
("id".to_string(), id1.clone()),
("name".to_string(), name1.clone()),
("age".to_string(), age1.clone()),
id1.clone(),
name1.clone(),
age1.clone(),
],
))
.unwrap();
{
let response: Response = state.interpret(Select(users.clone(), All, None)).unwrap();
let response: Response = state.interpret(Select(users_position, users_schema.all_selection(), None)).unwrap();
assert!(matches!(response, Response::Selected(_)));
let Response::Selected(rows) = response else {
assert!(matches!(response, Response::Selected(_, _)));
let Response::Selected(_, rows) = response else {
panic!()
};
let rows: Vec<_> = rows.collect();
assert!(rows.len() == 2);
let row0 = &rows[0];
let row1 = &rows[1];
assert!(row0.len() == 3);
assert!(row0[0] == id0);
assert!(row0[1] == name0);
assert!(row0[2] == age0);
assert!(row0[0].1 == id0);
assert!(row0[1].1 == name0);
assert!(row0[2].1 == age0);
assert!(row1.len() == 3);
assert!(row1[0] == id1);
assert!(row1[1] == name1);
assert!(row1[2] == age1);
assert!(row1[0].1 == id1);
assert!(row1[1].1 == name1);
assert!(row1[2].1 == age1);
}
{
let response: Response = state
.interpret(Select(
users.clone(),
All,
Some(Eq("id".to_string(), id0.clone())),
users_position,
users_schema.all_selection(),
Some(Eq(id_column, id0.clone())),
))
.unwrap();
assert!(matches!(response, Response::Selected(_)));
let Response::Selected(rows) = response else {
assert!(matches!(response, Response::Selected(_, _)));
let Response::Selected(_, rows) = response else {
panic!()
};
let rows: Vec<_> = rows.collect();
@ -376,21 +344,21 @@ mod tests {
let row0 = &rows[0];
assert!(row0.len() == 3);
assert!(row0[0] == id0);
assert!(row0[1] == name0);
assert!(row0[2] == age0);
assert!(row0[0].1 == id0);
assert!(row0[1].1 == name0);
assert!(row0[2].1 == age0);
}
{
let response: Response = state
.interpret(Select(
users.clone(),
Columns(vec!["name".to_string(), "id".to_string()]),
Some(Eq("id".to_string(), id0.clone())),
users_position,
vec![name_column, id_column],
Some(Eq(id_column, id0.clone())),
))
.unwrap();
assert!(matches!(response, Response::Selected(_)));
let Response::Selected(rows) = response else {
assert!(matches!(response, Response::Selected(_, _)));
let Response::Selected(_, rows) = response else {
panic!()
};
let rows: Vec<_> = rows.collect();
@ -398,14 +366,13 @@ mod tests {
let row0 = &rows[0];
assert!(row0.len() == 2);
assert!(row0[0] == name0);
assert!(row0[1] == id0);
assert!(row0[0].1 == name0);
assert!(row0[1].1 == id0);
}
}
#[test]
fn test_delete() {
use ColumnSelection::*;
use Condition::*;
use IndexableValue::*;
use Operation::*;
@ -413,10 +380,12 @@ mod tests {
let mut state = State::new();
let users_schema = users_schema();
let users = users_schema.table_name().clone();
let users_position: TablePosition = 0;
let id_column: ColumnPosition = 0;
state
.interpret(CreateTable(users.clone(), users_schema))
.interpret(CreateTable(users_schema.table_name().clone(), users_schema.clone()))
.unwrap();
let (id0, name0, age0) = (
@ -426,11 +395,11 @@ mod tests {
);
state
.interpret(Insert(
users.clone(),
users_position,
vec![
("id".to_string(), id0.clone()),
("name".to_string(), name0.clone()),
("age".to_string(), age0.clone()),
id0.clone(),
name0.clone(),
age0.clone(),
],
))
.unwrap();
@ -442,11 +411,11 @@ mod tests {
);
state
.interpret(Insert(
users.clone(),
users_position,
vec![
("id".to_string(), id1.clone()),
("name".to_string(), name1.clone()),
("age".to_string(), age1.clone()),
id1.clone(),
name1.clone(),
age1.clone(),
],
))
.unwrap();
@ -454,17 +423,17 @@ mod tests {
{
let delete_response: Response = state
.interpret(Delete(
users.clone(),
Some(Eq("id".to_string(), id0.clone())),
users_position,
Some(Eq(id_column, id0.clone())),
))
.unwrap();
assert!(matches!(delete_response, Response::Deleted(1)));
}
let response: Response = state.interpret(Select(users.clone(), All, None)).unwrap();
let response: Response = state.interpret(Select(users_position, users_schema.all_selection(), None)).unwrap();
assert!(matches!(response, Response::Selected(_)));
let Response::Selected(rows) = response else {
assert!(matches!(response, Response::Selected(_, _)));
let Response::Selected(_, rows) = response else {
panic!()
};
let rows: Vec<_> = rows.collect();
@ -472,9 +441,9 @@ mod tests {
let row = &rows[0];
assert!(row.len() == 3);
assert!(row[0] == id1);
assert!(row[1] == name1);
assert!(row[2] == age1);
assert!(row[0].1 == id1);
assert!(row[1].1 == name1);
assert!(row[2].1 == age1);
}
#[test]
@ -485,14 +454,16 @@ mod tests {
let mut state = State::new();
let users_schema = users_schema();
let users = users_schema.table_name().clone();
let users_position: TablePosition = 0;
let name_column: ColumnPosition = 1;
state
.interpret(CreateTable(users.clone(), users_schema))
.interpret(CreateTable(users_schema.table_name().clone(), users_schema.clone()))
.unwrap();
state
.interpret(CreateIndex(users.clone(), "name".to_string()))
.interpret(CreateIndex(users_position, name_column))
.unwrap();
let (id0, name0, age0) = (
@ -502,11 +473,11 @@ mod tests {
);
state
.interpret(Insert(
users.clone(),
users_position,
vec![
("id".to_string(), id0.clone()),
("name".to_string(), name0.clone()),
("age".to_string(), age0.clone()),
id0.clone(),
name0.clone(),
age0.clone(),
],
))
.unwrap();
@ -518,11 +489,11 @@ mod tests {
);
state
.interpret(Insert(
users.clone(),
users_position,
vec![
("id".to_string(), id1.clone()),
("name".to_string(), name1.clone()),
("age".to_string(), age1.clone()),
id1.clone(),
name1.clone(),
age1.clone(),
],
))
.unwrap();
@ -547,33 +518,34 @@ mod tests {
}
pub fn example() {
use ColumnSelection::*;
use crate::type_system::{IndexableValue, Value, DbType};
use Condition::*;
use IndexableValue::*;
use Operation::*;
use Value::*;
let users_schema: TableSchema = {
let id: ColumnPosition = 0;
let name: ColumnPosition = 1;
let age: ColumnPosition = 2;
let id_column: ColumnPosition = 0;
let name_column: ColumnPosition = 1;
let age_column: ColumnPosition = 2;
let users_schema: TableSchema = {
TableSchema::new(
"users".to_string(),
id,
id_column,
vec!(
("id".to_string(), id),
("name".to_string(), name),
("age".to_string(), age),
("id".to_string(), id_column),
("name".to_string(), name_column),
("age".to_string(), age_column),
),
vec![DbType::Uuid, DbType::String, DbType::Int],
)
};
let users_position: TablePosition = 0;
let users = users_schema.table_name().clone();
let mut state = State::new();
state
.interpret(Operation::CreateTable(users.clone(), users_schema))
.interpret(Operation::CreateTable(users, users_schema.clone()))
.unwrap();
let (id0, name0, age0) = (
@ -584,11 +556,11 @@ pub fn example() {
println!("==INSERT Plato==");
state
.interpret(Insert(
users.clone(),
users_position,
vec![
("id".to_string(), id0.clone()),
("name".to_string(), name0.clone()),
("age".to_string(), age0.clone()),
id0.clone(),
name0.clone(),
age0.clone(),
],
))
.unwrap();
@ -601,11 +573,11 @@ pub fn example() {
println!("==INSERT Aristotle==");
state
.interpret(Insert(
users.clone(),
users_position,
vec![
("id".to_string(), id1.clone()),
("name".to_string(), name1.clone()),
("age".to_string(), age1.clone()),
id1.clone(),
name1.clone(),
age1.clone(),
],
))
.unwrap();
@ -613,7 +585,7 @@ pub fn example() {
{
let response: Response = state
.interpret(Operation::Select(users.clone(), ColumnSelection::All, None))
.interpret(Operation::Select(users_position, users_schema.all_selection(), None))
.unwrap();
println!("==SELECT ALL==");
println!("{:?}", response);
@ -622,9 +594,9 @@ pub fn example() {
{
let response: Response = state
.interpret(Select(
users.clone(),
All,
Some(Eq("id".to_string(), id0.clone())),
users_position,
users_schema.all_selection(),
Some(Eq(id_column, id0.clone())),
))
.unwrap();
println!("==SELECT Plato==");
@ -638,16 +610,16 @@ pub fn example() {
// "infer" them?
let _delete_response: Response = state
.interpret(Delete(
users.clone(),
Some(Eq("id".to_string(), id0.clone())),
users_position,
Some(Eq(id_column, id0.clone())),
))
.unwrap();
println!("==DELETE Plato==");
}
let response: Response = state
.interpret(Select(
users.clone(),
Columns(vec!["name".to_string(), "id".to_string()]),
users_position,
vec![name_column, id_column],
None,
))
.unwrap();

View file

@ -5,3 +5,4 @@ pub mod type_system;
mod error;
mod internals;
mod result;
pub mod restricted_row;

View file

@ -1,40 +1,23 @@
use crate::schema::{ColumnName, TableName, TableSchema};
use crate::schema::{TableName, TableSchema};
use crate::type_system::Value;
use crate::internals::row::ColumnPosition;
use crate::interpreter::TablePosition;
// ==============SQL operations================
// TODO: Note that every operation has a table name.
// Perhaps consider factoring the table name out
// and think of the operations as operating on a unique table.
// TODO: `TableName` should be replaced by `TablePosition`
// Validated operation. Constructed by validation crate.
#[derive(Debug)]
pub enum Operation {
Select(TableName, ColumnSelection, Option<Condition>),
Insert(TableName, InsertionValues),
Delete(TableName, Option<Condition>),
// Update(...),
Select(TablePosition, ColumnSelection, Option<Condition>),
Insert(TablePosition, InsertionValues),
Delete(TablePosition, Option<Condition>),
CreateTable(TableName, TableSchema),
CreateIndex(TableName, ColumnName),
// DropTable(TableName),
CreateIndex(TablePosition, ColumnPosition),
}
pub type InsertionValues = Vec<(ColumnName, Value)>;
pub type InsertionValues = Vec<Value>;
pub enum ColumnSelection {
All,
Columns(Vec<ColumnName>),
}
pub type ColumnSelection = Vec<ColumnPosition>;
#[derive(Debug)]
pub enum Condition {
// And(Box<Condition>, Box<Condition>),
// Or(Box<Condition>, Box<Condition>),
// Not(Box<Condition>),
Eq(ColumnName, Value),
// LessOrEqual(ColumnName, DbValue),
// Less(ColumnName, DbValue),
// StringCondition(StringCondition),
Eq(ColumnPosition, Value),
}
// enum StringCondition {
// Prefix(ColumnName, String),
// Substring(ColumnName, String),
// }

View file

@ -0,0 +1,35 @@
use std::ops::Index;
use std::slice::SliceIndex;
use crate::internals::row::ColumnPosition;
use crate::type_system::Value;
#[derive(Debug, Clone)]
pub struct RestrictedRow(Vec<(ColumnPosition, Value)>);
impl<Idx> Index<Idx> for RestrictedRow
where
Idx: SliceIndex<[(ColumnPosition, Value)]>,
{
type Output = Idx::Output;
fn index(&self, index: Idx) -> &Self::Output {
&self.0[index]
}
}
impl From<Vec<(ColumnPosition, Value)>> for RestrictedRow {
fn from(v: Vec<(ColumnPosition, Value)>) -> Self {
RestrictedRow(v)
}
}
impl RestrictedRow {
pub fn len(&self) -> usize {
self.0.len()
}
pub fn iter(&self) -> impl Iterator<Item=&(ColumnPosition, Value)> {
self.0.iter()
}
}

View file

@ -1,15 +1,14 @@
use crate::error::Error;
use crate::internals::row::{ColumnPosition, Row};
use crate::operation::{ColumnSelection, InsertionValues};
use crate::operation::{InsertionValues, ColumnSelection};
use crate::result::DbResult;
use crate::type_system::{DbType, IndexableValue, Uuid, Value};
use bimap::BiMap;
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
// Note that it is nice to split metadata from the data because
// then you can give the metadata to the parser without giving it the data.
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableSchema {
table_name: TableName, // used for descriptive errors
primary_key: ColumnPosition,
@ -49,53 +48,26 @@ impl TableSchema {
self.column_name_position_mapping.get_by_left(column_name).copied()
}
pub fn all_selection(&self) -> ColumnSelection {
let mut selection: ColumnSelection = self.column_name_position_mapping.iter().map(|(_, column)| *column).collect();
selection.sort();
selection
}
pub fn get_column(&self, column_name: &ColumnName) -> Option<(ColumnPosition, DbType)> {
let column = self.get_column_position(column_name)?;
Some((column, self.column_type(column)))
}
pub fn get_type_at(&self, column_name: &ColumnName) -> Option<DbType> {
let position = self.get_column_position(column_name)?;
self.types.get(position).copied()
}
// TODO: Get rid of this after validation is merged
fn get_column(&self, column_name: &ColumnName) -> DbResult<(DbType, ColumnPosition)> {
match self.column_name_position_mapping.get_by_left(column_name) {
Some(column_position) => match self.types.get(*column_position) {
Some(type_) => Ok((*type_, *column_position)),
None => Err(Error::MissingTypeAnnotationOfColumn(
self.table_name.clone(),
*column_position,
)),
},
None => Err(Error::ColumnDoesNotExist(
self.table_name.clone(),
column_name.clone(),
)),
}
}
// TODO: Get rid of this after validation is merged
pub fn column_position_from_column_name(
&self,
column_name: &ColumnName,
) -> DbResult<ColumnPosition> {
self.get_column(column_name)
.map(|(_, column_position)| column_position)
}
pub fn is_primary(&self, column_position: ColumnPosition) -> bool {
self.primary_key == column_position
}
fn column_positions_from_column_names(
&self,
column_names: &[ColumnName],
) -> DbResult<Vec<ColumnPosition>> {
let mut positions: Vec<ColumnPosition> = Vec::with_capacity(column_names.len());
for column_name in column_names {
let column_position = self.column_position_from_column_name(column_name)?;
positions.push(column_position)
}
Ok(positions)
}
pub fn column_name_from_column_position(
&self,
column_position: ColumnPosition,
@ -112,27 +84,6 @@ impl TableSchema {
}
}
pub fn column_positions_from_column_selection(
&self,
column_selection: &ColumnSelection,
) -> DbResult<Vec<ColumnPosition>> {
match column_selection {
ColumnSelection::All => {
let mut column_positions: Vec<ColumnPosition> = self
.column_name_position_mapping
.iter()
.map(|(_, column_position)| *column_position)
.collect();
column_positions.sort();
Ok(column_positions)
}
ColumnSelection::Columns(column_names) => {
self.column_positions_from_column_names(column_names)
}
}
}
pub fn number_of_columns(&self) -> usize {
self.column_name_position_mapping.len()
}
@ -141,44 +92,12 @@ impl TableSchema {
&self,
insertion_values: InsertionValues,
) -> DbResult<(Uuid, Row)> {
// TODO: There should be proper validation of the insertion_values.
// And it shouldn't really be done here.
//
// In the below we don't check for duplicate column names
//
let number_of_columns = self.number_of_columns();
if number_of_columns != insertion_values.len() {
return Err(Error::MismatchBetweenInsertValuesAndColumns(
self.table_name.clone(),
insertion_values,
));
}
let mut row: Row = Row::with_number_of_columns(number_of_columns);
let mut values: HashMap<ColumnName, Value> = HashMap::new();
for (column_name, db_value) in &insertion_values {
values.insert(column_name.clone(), db_value.clone());
}
for column_position in 0..number_of_columns {
let column_name: ColumnName = self.column_name_from_column_position(column_position)?;
match values.get(&column_name) {
Some(db_value) => row.push(db_value.clone()),
None => {
return Err(Error::MissingColumnInInsertValues(
self.table_name.clone(),
column_name,
insertion_values,
))
}
}
}
let row: Row = Row::new_from_insertion_values(insertion_values);
let id: Uuid = match row.get(self.primary_key) {
Some(Value::Indexable(IndexableValue::Uuid(id))) => *id,
Some(_) => unreachable!(),
None => unreachable!(),
Some(_) => unreachable!(), // SAFETY: Should be guaranteed by validation
None => unreachable!(), // SAFETY: Should be guaranteed by validation
};
Ok((id, row))

View file

@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize};
use crate::error::TypeConversionError;
// ==============Types================
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@ -41,4 +42,156 @@ impl Value {
},
}
}
pub fn type_oid(&self) -> i32 {
match self {
Self::Number(_) => 701,
Self::Indexable(val) => match val {
IndexableValue::String(_) => 25,
IndexableValue::Int(_) => 23,
IndexableValue::Uuid(_) => 2950,
},
}
}
pub fn type_size(&self) -> i16 {
match self {
Self::Number(_) => 8,
Self::Indexable(val) => match val {
IndexableValue::String(_) => -2, // null terminated string
IndexableValue::Int(_) => 8,
IndexableValue::Uuid(_) => 16,
},
}
}
pub fn as_text_bytes(&self) -> Vec<u8> {
match self {
Self::Number(n) => format!("{n}").into_bytes(),
Self::Indexable(i) => match i {
IndexableValue::String(s) => format!("{s}\0").into_bytes(),
IndexableValue::Int(i) => format!("{i}").into_bytes(),
IndexableValue::Uuid(u) => format!("{u}").into_bytes(),
},
}
}
pub fn from_text_bytes(bytes: &[u8], type_oid: i32, type_size: i16) -> Result<Value, TypeConversionError> {
match (type_oid, type_size) {
(701, 8) => {
let s = std::str::from_utf8(bytes)?;
let n = s.parse::<f64>()?;
Ok(Value::Number(n))
}
(25, -2) => {
let s = std::str::from_utf8(bytes)?;
let s = &s[..s.len() - 1]; // remove null terminator
Ok(Value::Indexable(IndexableValue::String(s.to_string())))
}
(23, 8) => {
let s = std::str::from_utf8(bytes)?;
let n = s.parse::<u64>()?;
Ok(Value::Indexable(IndexableValue::Int(n)))
}
(2950, 16) => {
let s = std::str::from_utf8(bytes)?;
let n = s.parse::<Uuid>()?;
Ok(Value::Indexable(IndexableValue::Uuid(n)))
}
(oid, size) => Err(TypeConversionError::UnknownType { oid, size }),
}
}
}
#[cfg(test)]
mod tests {
use crate::error::TypeConversionError::UnknownType;
use super::{Value, IndexableValue};
#[test]
fn test_encode_number() {
let value = Value::Number(123.456);
let oid = value.type_oid();
let size = value.type_size();
let bytes = value.as_text_bytes();
let from_bytes = Value::from_text_bytes(&bytes, oid, size).unwrap();
assert_eq!(value, from_bytes);
assert_eq!(oid, 701);
assert_eq!(size, 8);
}
#[test]
fn test_encode_string() {
let value = Value::Indexable(IndexableValue::String("hello".to_string()));
let oid = value.type_oid();
let size = value.type_size();
let bytes = value.as_text_bytes();
let from_bytes = Value::from_text_bytes(&bytes, oid, size).unwrap();
assert_eq!(value, from_bytes);
assert_eq!(oid, 25);
assert_eq!(size, -2);
}
#[test]
fn test_encode_string_utf8() {
let value = Value::Indexable(IndexableValue::String("#速度与激情9 早上好中国 现在我有冰激淋 我很喜欢冰激淋 但是《速度与激情9》比冰激淋 🍧🍦🍨".to_string()));
let oid = value.type_oid();
let size = value.type_size();
let bytes = value.as_text_bytes();
let from_bytes = Value::from_text_bytes(&bytes, oid, size).unwrap();
assert_eq!(value, from_bytes);
assert_eq!(oid, 25);
assert_eq!(size, -2);
}
#[test]
fn test_encode_int() {
let value = Value::Indexable(IndexableValue::Int(123));
let oid = value.type_oid();
let size = value.type_size();
let bytes = value.as_text_bytes();
let from_bytes = Value::from_text_bytes(&bytes, oid, size).unwrap();
assert_eq!(value, from_bytes);
assert_eq!(oid, 23);
assert_eq!(size, 8);
}
#[test]
fn test_encode_uuid() {
let value = Value::Indexable(IndexableValue::Uuid(123));
let oid = value.type_oid();
let size = value.type_size();
let bytes = value.as_text_bytes();
let from_bytes = Value::from_text_bytes(&bytes, oid, size).unwrap();
assert_eq!(value, from_bytes);
assert_eq!(oid, 2950);
assert_eq!(size, 16);
}
#[test]
fn test_mismatched_size() {
let value = Value::Indexable(IndexableValue::Uuid(123));
let oid = value.type_oid();
let size = 8;
let bytes = value.as_text_bytes();
let from_bytes = Value::from_text_bytes(&bytes, oid, size);
assert!(matches!(from_bytes, Err(UnknownType { oid: 2950, size: 8 })))
}
}

View file

@ -1,4 +1,5 @@
use minisql::{operation::Operation, schema::TableSchema};
use minisql::{operation::Operation, interpreter::DbSchema};
use crate::syntax::RawQuerySyntax;
use nom::{branch::alt, multi::many0, IResult};
use thiserror::Error;
@ -12,7 +13,7 @@ pub enum Error {
ValidationError(#[from] ValidationError)
}
pub fn parse_statement<'a>(input: &'a str) -> IResult<&str, Operation> {
pub fn parse_statement<'a>(input: &'a str) -> IResult<&str, RawQuerySyntax> {
alt((
parse_insert,
parse_create,
@ -24,18 +25,17 @@ pub fn parse_statement<'a>(input: &'a str) -> IResult<&str, Operation> {
))(input)
}
pub fn parse_statements<'a>(input: &'a str) -> IResult<&str, Vec<Operation>> {
pub fn parse_statements<'a>(input: &'a str) -> IResult<&str, Vec<RawQuerySyntax>> {
many0(parse_statement)(input)
}
pub fn parse_and_validate(query: String, db_metadata: &Vec<(String, &TableSchema)>) -> Result<Operation, Error> {
pub fn parse_and_validate(query: String, db_schema: &DbSchema) -> Result<Operation, Error> {
let (_, op) = parse_statement(query.as_str())
.map_err(|err| {
Error::ParsingError(err.to_string())
})?;
validate_operation(&op, db_metadata)?;
Ok(op)
Ok(validate_operation(op, db_schema)?)
}
// #[test]

View file

@ -2,6 +2,7 @@
mod parsing;
mod validation;
mod core;
mod syntax;
pub use core::parse_and_validate;
pub use core::Error;

View file

@ -6,8 +6,9 @@ use nom::{
bytes::complete::tag,
IResult, branch::alt,
};
use minisql::{operation::Condition, type_system::DbType};
use minisql::type_system::DbType;
use crate::syntax::Condition;
use super::literal::parse_db_value;
pub fn parse_table_name(input: &str) -> IResult<&str, &str> {
@ -67,7 +68,9 @@ fn parse_equality(input: &str) -> IResult<&str, Condition> {
#[cfg(test)]
mod tests {
use minisql::{operation::Condition, type_system::DbType};
use minisql::type_system::DbType;
use crate::syntax::Condition;
use crate::parsing::common::{parse_db_type, parse_equality};
#[test]

View file

@ -1,4 +1,4 @@
use minisql::{operation::Operation, schema::{ColumnName, TableSchema}, type_system::DbType};
use minisql::{schema::{ColumnName, TableSchema}, type_system::DbType};
use nom::{
bytes::complete::tag,
character::complete::{char, multispace0, multispace1},
@ -8,8 +8,9 @@ use nom::{
};
use super::common::{parse_table_name, parse_identifier, parse_db_type};
use crate::syntax::RawQuerySyntax;
pub fn parse_create(input: &str) -> IResult<&str, Operation> {
pub fn parse_create(input: &str) -> IResult<&str, RawQuerySyntax> {
let (input, _) = tag("CREATE")(input)?;
let (input, _) = multispace1(input)?;
let (input, _) = tag("TABLE")(input)?;
@ -41,7 +42,7 @@ pub fn parse_create(input: &str) -> IResult<&str, Operation> {
);
Ok((
input,
Operation::CreateTable(table_name.to_string(), schema),
RawQuerySyntax::CreateTable(table_name.to_string(), schema),
))
}
@ -68,8 +69,8 @@ pub fn parse_column_definition(input: &str) -> IResult<&str, (ColumnName, DbType
#[cfg(test)]
mod tests {
use minisql::operation::Operation;
use crate::parsing::create::parse_create;
use crate::syntax::RawQuerySyntax;
#[test]
fn test_parse_create_no_spaces() {
@ -94,13 +95,13 @@ mod tests {
#[test]
fn test_parse_create() {
let (_, create) = parse_create("CREATE TABLE \"Table1\"( id UUID , column1 INT );").expect("should parse");
assert!(matches!(create, Operation::CreateTable(_ ,_)));
assert!(matches!(create, RawQuerySyntax::CreateTable(_ ,_)));
match create {
Operation::CreateTable(name, schema) => {
RawQuerySyntax::CreateTable(name, schema) => {
assert_eq!(name, "Table1");
assert_eq!(schema.number_of_columns(), 2);
assert_eq!(schema.column_position_from_column_name(&"id".to_string()).unwrap(), 0);
assert_eq!(schema.column_position_from_column_name(&"column1".to_string()).unwrap(), 1);
assert_eq!(schema.get_column_position(&"id".to_string()).unwrap(), 0);
assert_eq!(schema.get_column_position(&"column1".to_string()).unwrap(), 1);
}
_ => {}
}

View file

@ -1,13 +1,13 @@
use minisql::operation::Operation;
use nom::{
bytes::complete::tag,
character::complete::{char, multispace0, multispace1},
IResult,
};
use crate::syntax::RawQuerySyntax;
use super::common::{parse_table_name, parse_condition};
pub fn parse_delete(input: &str) -> IResult<&str, Operation> {
pub fn parse_delete(input: &str) -> IResult<&str, RawQuerySyntax> {
let (input, _) = tag("DELETE")(input)?;
let (input, _) = multispace1(input)?;
let (input, _) = tag("FROM")(input)?;
@ -19,19 +19,19 @@ pub fn parse_delete(input: &str) -> IResult<&str, Operation> {
let (input, _) = char(';')(input)?;
Ok((
input,
Operation::Delete(table_name.to_string(), condition),
RawQuerySyntax::Delete(table_name.to_string(), condition),
))
}
#[cfg(test)]
mod tests {
use minisql::operation::Operation;
use crate::syntax::RawQuerySyntax;
use crate::parsing::delete::parse_delete;
#[test]
fn test_parse_delete() {
let (_, operation) = parse_delete("DELETE FROM \"T1\" WHERE id = 1 ;").expect("should parse");
assert!(matches!(operation, Operation::Delete(_, _)))
assert!(matches!(operation, RawQuerySyntax::Delete(_, _)))
}
// TODO: add test with condition

View file

@ -1,4 +1,4 @@
use minisql::operation::Operation;
use crate::syntax::RawQuerySyntax;
use nom::{
bytes::complete::tag,
character::complete::{char, multispace0, multispace1},
@ -7,7 +7,7 @@ use nom::{
use super::common::{parse_identifier, parse_table_name};
pub fn parse_create_index(input: &str) -> IResult<&str, Operation> {
pub fn parse_create_index(input: &str) -> IResult<&str, RawQuerySyntax> {
let (input, _) = tag("CREATE")(input)?;
let unique = |input| -> IResult<&str, bool> {
let (input, _) = multispace1(input)?;
@ -31,23 +31,23 @@ pub fn parse_create_index(input: &str) -> IResult<&str, Operation> {
let (input, _) = char(')')(input)?;
let (input, _) = multispace0(input)?;
let (input, _) = char(';')(input)?;
let operation = Operation::CreateIndex(table_name.to_string(), column_name.to_string());
let operation = RawQuerySyntax::CreateIndex(table_name.to_string(), column_name.to_string());
Ok((input, operation))
}
#[cfg(test)]
mod tests {
use minisql::operation::Operation;
use crate::syntax::RawQuerySyntax;
use crate::parsing::index::parse_create_index;
#[test]
fn test_create_index() {
let (_, operation) = parse_create_index("CREATE UNIQUE INDEX idxcontactsemail ON \"contacts\" (email);").expect("should parse");
assert!(matches!(operation, Operation::CreateIndex(_, _)));
match operation {
Operation::CreateIndex(table_name, column_name) => {
let (_, syntax) = parse_create_index("CREATE UNIQUE INDEX idxcontactsemail ON \"contacts\" (email);").expect("should parse");
assert!(matches!(syntax, RawQuerySyntax::CreateIndex(_, _)));
match syntax {
RawQuerySyntax::CreateIndex(table_name, column_name) => {
assert_eq!(table_name, "contacts");
assert_eq!(column_name, "email");
}
@ -57,10 +57,10 @@ mod tests {
#[test]
fn test_create_index_with_spaces() {
let (_, operation) = parse_create_index("CREATE UNIQUE INDEX idxcontactsemail ON \"contacts\" ( email ) ;").expect("should parse");
assert!(matches!(operation, Operation::CreateIndex(_, _)));
match operation {
Operation::CreateIndex(table_name, column_name) => {
let (_, syntax) = parse_create_index("CREATE UNIQUE INDEX idxcontactsemail ON \"contacts\" ( email ) ;").expect("should parse");
assert!(matches!(syntax, RawQuerySyntax::CreateIndex(_, _)));
match syntax {
RawQuerySyntax::CreateIndex(table_name, column_name) => {
assert_eq!(table_name, "contacts");
assert_eq!(column_name, "email");
}

View file

@ -1,5 +1,6 @@
use super::{literal::parse_db_value, common::{parse_table_name, parse_identifier}};
use minisql::{operation::Operation, type_system::Value};
use crate::syntax::RawQuerySyntax;
use minisql::type_system::Value;
use nom::{
bytes::complete::tag,
character::complete::{multispace0, multispace1, char},
@ -9,7 +10,7 @@ use nom::{
IResult,
};
pub fn parse_insert(input: &str) -> IResult<&str, Operation> {
pub fn parse_insert(input: &str) -> IResult<&str, RawQuerySyntax> {
let (input, _) = tag("INSERT")(input)?;
let (input, _) = multispace1(input)?;
let (input, _) = tag("INTO")(input)?;
@ -33,7 +34,7 @@ pub fn parse_insert(input: &str) -> IResult<&str, Operation> {
let (input, _) = char(';')(input)?;
Ok((
input,
Operation::Insert(table_name.to_string(), column_names.into_iter().zip(values).collect()),
RawQuerySyntax::Insert(table_name.to_string(), column_names.into_iter().zip(values).collect()),
))
}
@ -48,17 +49,18 @@ pub fn parse_values(input: &str) -> IResult<&str, Vec<Value>> {
#[cfg(test)]
mod tests {
use minisql::{operation::Operation, type_system::{IndexableValue, Value}};
use minisql::type_system::{IndexableValue, Value};
use crate::syntax::RawQuerySyntax;
use super::parse_insert;
#[test]
fn test_parse_insert() {
let sql = "INSERT INTO \"MyTable\" (id, data) VALUES(1, \"Text\");";
let operation = parse_insert(sql).expect("should parse");
match operation {
("", Operation::Insert(table_name, insertion_values)) => {
let syntax = parse_insert(sql).expect("should parse");
match syntax {
("", RawQuerySyntax::Insert(table_name, insertion_values)) => {
assert_eq!(table_name, "MyTable");
assert_eq!(
insertion_values,
@ -78,7 +80,7 @@ mod tests {
let sql = "INSERT INTO \"MyTable\" ( id, data ) VALUES ( 1, \"Text\" ) ;";
let operation = parse_insert(sql).expect("should parse");
match operation {
("", Operation::Insert(table_name, insertion_values)) => {
("", RawQuerySyntax::Insert(table_name, insertion_values)) => {
assert_eq!(table_name, "MyTable");
assert_eq!(insertion_values,
vec![

View file

@ -1,5 +1,5 @@
use super::common::{parse_table_name, parse_column_name, parse_condition};
use minisql::operation::{ColumnSelection, Operation};
use crate::syntax::{ColumnSelection, RawQuerySyntax};
use nom::{
branch::alt,
bytes::complete::tag,
@ -11,7 +11,7 @@ use nom::{
IResult,
};
pub fn parse_select(input: &str) -> IResult<&str, Operation> {
pub fn parse_select(input: &str) -> IResult<&str, RawQuerySyntax> {
let (input, _) = tag("SELECT")(input)?;
let (input, _) = multispace1(input)?;
@ -27,7 +27,7 @@ pub fn parse_select(input: &str) -> IResult<&str, Operation> {
let (input, _) = tag(";")(input)?;
Ok((
input,
Operation::Select(table_name.to_string(), column_selection, condition),
RawQuerySyntax::Select(table_name.to_string(), column_selection, condition),
))
}
@ -44,7 +44,7 @@ pub fn try_parse_column_selection(input: &str) -> IResult<&str, ColumnSelection>
#[cfg(test)]
mod tests {
use minisql::operation::{ColumnSelection, Operation};
use crate::syntax::{ColumnSelection, RawQuerySyntax};
use crate::parsing::{common::{parse_column_name, parse_table_name}, select::parse_select};
@ -53,7 +53,7 @@ mod tests {
let sql = "SELECT * FROM \"MyTable\";";
let operation = parse_select(sql).expect("should parse");
match operation {
("", Operation::Select(table_name, column_selection, maybe_condition)) => {
("", RawQuerySyntax::Select(table_name, column_selection, maybe_condition)) => {
assert_eq!(table_name, "MyTable");
assert!(matches!(column_selection, ColumnSelection::All));
assert!(matches!(maybe_condition, None));
@ -80,7 +80,7 @@ mod tests {
let sql = "SELECT name , email FROM \"AddressBook\" ;";
let operation = parse_select(sql).expect("should parse");
match operation {
("", Operation::Select(table_name, column_selection, maybe_condition)) => {
("", RawQuerySyntax::Select(table_name, column_selection, maybe_condition)) => {
assert_eq!(table_name, "AddressBook");
assert!(matches!(column_selection, ColumnSelection::Columns(_)));
match column_selection {
@ -102,11 +102,11 @@ mod tests {
#[test]
fn test_parse_select_where() {
use minisql::operation::Condition;
use crate::syntax::Condition;
let sql = "SELECT * FROM \"AddressBook\" WHERE id = 5 ;";
let operation = parse_select(sql).expect("should parse");
match operation {
("", Operation::Select(table_name, column_selection, maybe_condition)) => {
("", RawQuerySyntax::Select(table_name, column_selection, maybe_condition)) => {
assert_eq!(table_name, "AddressBook");
assert!(matches!(column_selection, ColumnSelection::All));
assert!(matches!(maybe_condition, Some(Condition::Eq(_, _))));

36
parser/src/syntax.rs Normal file
View file

@ -0,0 +1,36 @@
use minisql::{type_system::Value, schema::{TableSchema, ColumnName, TableName}};
// TODO: Move this out into separate file and rename to something like Syntax, SyntaxTree,
// OperationSyntax, RawOperationSyntax
pub enum RawQuerySyntax {
Select(TableName, ColumnSelection, Option<Condition>),
Insert(TableName, InsertionValues),
Delete(TableName, Option<Condition>),
// Update(...),
CreateTable(TableName, TableSchema),
CreateIndex(TableName, ColumnName),
// DropTable(TableName),
}
pub type InsertionValues = Vec<(ColumnName, Value)>;
pub enum ColumnSelection {
All,
Columns(Vec<ColumnName>),
}
pub enum Condition {
// And(Box<Condition>, Box<Condition>),
// Or(Box<Condition>, Box<Condition>),
// Not(Box<Condition>),
Eq(ColumnName, Value),
// LessOrEqual(ColumnName, DbValue),
// Less(ColumnName, DbValue),
// StringCondition(StringCondition),
}
// enum StringCondition {
// Prefix(ColumnName, String),
// Substring(ColumnName, String),
// }

View file

@ -1,9 +1,11 @@
use std::collections::HashSet;
use std::collections::HashMap;
use thiserror::Error;
use minisql::{operation::{ColumnSelection, Condition, InsertionValues, Operation}, schema::{TableSchema, ColumnName, TableName}, type_system::DbType};
use crate::syntax;
use crate::syntax::RawQuerySyntax;
use minisql::operation;
use minisql::{operation::Operation, type_system::Value, schema::{TableSchema, ColumnName, TableName}, type_system::DbType, interpreter::{TablePosition, DbSchema}};
#[derive(Debug, Error)]
pub enum ValidationError {
@ -25,69 +27,53 @@ pub enum ValidationError {
RequiredColumnsAreMissing(Vec<ColumnName>)
}
pub type DbSchema<'a> = Vec<(TableName, &'a TableSchema)>;
/// Validates the operation based on db_metadata
pub fn validate_operation(operation: &Operation, db_schema: &DbSchema) -> Result<(), ValidationError> {
match operation {
Operation::Select(table_name, column_selection, condition) => {
validate_select(table_name, column_selection, condition, db_schema)?;
/// Validates and converts the raw syntax into a proper interpreter operation based on db schema.
pub fn validate_operation(query: RawQuerySyntax, db_schema: &DbSchema) -> Result<Operation, ValidationError> {
match query {
RawQuerySyntax::Select(table_name, column_selection, condition) => {
validate_select(table_name, column_selection, condition, db_schema)
},
Operation::Insert(table_name, insertion_values) => {
validate_insert(&table_name, insertion_values, db_schema)?;
RawQuerySyntax::Insert(table_name, insertion_values) => {
validate_insert(table_name, insertion_values, db_schema)
},
Operation::Delete(table_name, condition) => {
validate_delete(table_name, condition, db_schema)?;
RawQuerySyntax::Delete(table_name, condition) => {
validate_delete(table_name, condition, db_schema)
},
// Operation::Update(table_name, insertion_values, condition) => {
// validate_update(table_name, insertion_values, db_metadata)?;
// },
Operation::CreateTable(table_name, schema) => {
validate_create(table_name, schema, db_schema)?;
RawQuerySyntax::CreateTable(table_name, schema) => {
validate_create(table_name, schema, db_schema)
},
Operation::CreateIndex(table_name, column_name) => {
validate_create_index(table_name, column_name, db_schema)?;
RawQuerySyntax::CreateIndex(table_name, column_name) => {
validate_create_index(table_name, column_name, db_schema)
},
// Operation::DropTable(table_name) => {
// validate_drop(table_name, db_schema)?;
// }
}
Ok(())
}
fn validate_table_exists<'a>(db_schema: &DbSchema<'a>, table_name: &'a TableName) -> Result<&'a TableSchema, ValidationError> {
db_schema.iter().find(|(tname, _)| table_name.eq(tname))
fn validate_table_exists<'a>(db_schema: &DbSchema<'a>, table_name: &'a TableName) -> Result<(TablePosition, &'a TableSchema), ValidationError> {
db_schema.iter().find(|(tname, _, _)| table_name.eq(tname))
.ok_or(ValidationError::TableDoesNotExist(table_name.to_string()))
.map(|(_, table_schema)| table_schema).copied()
.map(|(_, table_position, table_schema)| (*table_position, *table_schema))
}
// pub fn validate_drop(table_name: &str, db_metadata: &Vec<(String, TableSchema)>) -> Result<(), ValidationError> {
// db_metadata.iter().find(|(tname, _)| table_name.eq(tname))
// .ok_or(ValidationError::TableDoesNotExist(table_name.to_string()))?;
// Ok(())
// }
pub fn validate_create(table_name: &TableName, schema: &TableSchema, db_schema: &DbSchema) -> Result<(), ValidationError> {
if let Some(_) = get_table_schema(db_schema, table_name) {
pub fn validate_create(table_name: TableName, table_schema: TableSchema, db_schema: &DbSchema) -> Result<Operation, ValidationError> {
if let Some(_) = get_table_schema(db_schema, &table_name) {
return Err(ValidationError::TableAlreadyExists(table_name.to_string()));
}
find_first_duplicate(&schema.get_columns())
find_first_duplicate(&table_schema.get_columns())
.map_or_else(
|| Ok(()),
|duplicate_column| Err(ValidationError::DuplicateColumn(duplicate_column.to_string()))
)?;
// TODO: Ensure it has a primary key??
Ok(())
Ok(Operation::CreateTable(table_name, table_schema))
}
pub fn validate_select(table_name: &TableName, column_selection: &ColumnSelection, condition: &Option<Condition>, db_schema: &Vec<(TableName, &TableSchema)>) -> Result<(), ValidationError> {
let schema = validate_table_exists(db_schema, table_name)?;
pub fn validate_select(table_name: TableName, column_selection: syntax::ColumnSelection, condition: Option<syntax::Condition>, db_schema: &DbSchema) -> Result<Operation, ValidationError> {
let (table_position, schema) = validate_table_exists(db_schema, &table_name)?;
match column_selection {
ColumnSelection::Columns(columns) => {
let non_existant_columns: Vec<String> =
syntax::ColumnSelection::Columns(columns) => {
let non_existant_columns: Vec<ColumnName> =
columns.iter().filter_map(|column|
if schema.does_column_exist(&column) {
Some(column.clone())
@ -97,52 +83,21 @@ pub fn validate_select(table_name: &TableName, column_selection: &ColumnSelectio
if non_existant_columns.len() > 0 {
Err(ValidationError::ColumnsDoNotExist(non_existant_columns))
} else {
validate_condition(condition, schema)
let selection: operation::ColumnSelection =
columns.iter().filter_map(|column_name| schema.get_column_position(column_name)).collect();
let validated_condition = validate_condition(condition, schema)?;
Ok(Operation::Select(table_position, selection, validated_condition))
}
}
ColumnSelection::All => Ok(())
syntax::ColumnSelection::All => {
let validated_condition = validate_condition(condition, schema)?;
Ok(Operation::Select(table_position, schema.all_selection(), validated_condition))
}
}
}
// pub fn validate_update(table_name: &str, insertion_values: &InsertionValues, db_metadata: &Vec<(String, TableSchema)>) -> Result<(), ValidationError> {
// let schema = validate_table_exists(db_schema, table_name)?;
// let mut column_names = HashSet::new();
// // Find duplicate columns
// for (name, _) in insertion_values {
// if column_names.contains(name) {
// return Err(ValidationError::DuplicateColumn(name.clone()));
// } else {
// column_names.insert(name.clone());
// }
// }
// // Ensure columns exist in schema
// let column_value_type: Vec<_> = insertion_values.iter().map(|(column, value)| {
// (column, value, schema.column_name_position_mapping.iter().find(|(name, _) | {
// (*name).eq(column)
// }).map(|(_, t)| schema.types.get(*t as usize)))
// }).collect();
// if let Some((name, _, _)) = column_value_type.iter().find(|(_, _, t)| {
// t.is_none()
// }) {
// return Err(ValidationError::ColumnsDoNotExist(vec![(*name).clone())]);
// }
// // Check types
// if let Some((_, _, _)) = column_value_type.iter().find(|(_, value, t)| {
// if let Some(Some(column_type)) = t {
// !type_of(value).eq(column_type)
// } else {
// false
// }
// }) {
// // TODO: Add column name information
// return Err(ValidationError::TypeMismatch);
// }
// Ok(())
// }
pub fn validate_insert(table_name: &TableName, insertion_values: &InsertionValues, db_schema: &DbSchema) -> Result<(), ValidationError> {
let schema = validate_table_exists(db_schema, table_name)?;
pub fn validate_insert(table_name: TableName, insertion_values: syntax::InsertionValues, db_schema: &DbSchema) -> Result<Operation, ValidationError> {
let (table_position, schema) = validate_table_exists(db_schema, &table_name)?;
// Check for duplicate columns in insertion_values.
let columns_in_query_vec: Vec<&ColumnName> = insertion_values.iter().map(|(column_name, _)| column_name).collect();
@ -157,74 +112,82 @@ pub fn validate_insert(table_name: &TableName, insertion_values: &InsertionValue
let columns_in_schema: HashSet<&ColumnName> = HashSet::from_iter(schema.get_columns());
let non_existant_columns = Vec::from_iter(columns_in_query.difference(&columns_in_schema));
if non_existant_columns.len() > 0 {
return Err(ValidationError::ColumnsDoNotExist(non_existant_columns.iter().map(|str| str.to_string()).collect()));
return Err(ValidationError::ColumnsDoNotExist(non_existant_columns.iter().map(|column_name| column_name.to_string()).collect()));
}
let missing_required_columns = Vec::from_iter(columns_in_schema.difference(&columns_in_query));
if missing_required_columns.len() > 0 {
return Err(ValidationError::RequiredColumnsAreMissing(missing_required_columns.iter().map(|str| str.to_string()).collect()));
}
// Check types
// Check types and prepare for creation of InsertionValues for the interpreter
let mut values_map: HashMap<_, Value> = HashMap::new();
for (column_name, value) in insertion_values {
let expected_type = schema.get_type_at(column_name).ok_or(ValidationError::ColumnsDoNotExist(vec![column_name.to_string()]))?; // By the previous validation steps this is never gonna trigger an error.
let (column, expected_type) = schema.get_column(&column_name).ok_or(ValidationError::ColumnsDoNotExist(vec![column_name.to_string()]))?; // By the previous validation steps this is never gonna trigger an error.
let value_type = value.to_type();
if value_type != expected_type {
return Err(ValidationError::TypeMismatch { column_name: column_name.to_string(), received_type: value_type, expected_type });
}
values_map.insert(column, value);
}
Ok(())
// These are values ordered by the column position
let values: operation::InsertionValues = values_map.into_values().collect();
Ok(Operation::Insert(table_position, values))
}
pub fn validate_delete(table_name: &TableName, condition: &Option<Condition>, db_schema: &DbSchema) -> Result<(), ValidationError> {
let schema = validate_table_exists(db_schema, table_name)?;
validate_condition(condition, schema)?;
Ok(())
pub fn validate_delete(table_name: TableName, condition: Option<syntax::Condition>, db_schema: &DbSchema) -> Result<Operation, ValidationError> {
let (table_position, schema) = validate_table_exists(db_schema, &table_name)?;
let validated_condition = validate_condition(condition, schema)?;
Ok(Operation::Delete(table_position, validated_condition))
}
fn validate_condition(condition: &Option<Condition>, schema: &TableSchema) -> Result<(), ValidationError> {
fn validate_condition(condition: Option<syntax::Condition>, schema: &TableSchema) -> Result<Option<operation::Condition>, ValidationError> {
match condition {
Some(condition) => {
match condition {
Condition::Eq(column_name, value) => {
let expected_type: DbType = schema.get_type_at(column_name).ok_or(ValidationError::ColumnsDoNotExist(vec![column_name.to_string()]))?;
syntax::Condition::Eq(column_name, value) => {
let (column, expected_type) = schema.get_column(&column_name).ok_or(ValidationError::ColumnsDoNotExist(vec![column_name.to_string()]))?;
let value_type: DbType = value.to_type();
if !expected_type.eq(&value_type) {
if expected_type.eq(&value_type) {
Ok(Some(operation::Condition::Eq(column, value)))
} else {
return Err(ValidationError::TypeMismatch { column_name: column_name.to_string(), received_type: value_type, expected_type });
}
}
}
}
None => {}
None => Ok(None)
}
Ok(())
}
fn validate_create_index(table_name: &TableName, column_name: &ColumnName, db_schema: &DbSchema) -> Result<(), ValidationError> {
let schema = validate_table_exists(db_schema, table_name)?;
if schema.does_column_exist(column_name) {
Ok(())
} else {
Err(ValidationError::ColumnsDoNotExist(vec![column_name.to_string()]))
}
fn validate_create_index(table_name: TableName, column_name: ColumnName, db_schema: &DbSchema) -> Result<Operation, ValidationError> {
// TODO: You should disallow indexing of Number columns.
let (table_position, schema) = validate_table_exists(db_schema, &table_name)?;
schema
.get_column_position(&column_name)
.map_or_else(
|| Err(ValidationError::ColumnsDoNotExist(vec![column_name.to_string()])),
|column| Ok(Operation::CreateIndex(table_position, column))
)
}
// ===Helpers===
fn find_first_duplicate<A>(xs: &[A]) -> Option<&A>
where A: Eq + std::hash::Hash
fn find_first_duplicate<T>(ts: &[T]) -> Option<&T>
where T: Eq + std::hash::Hash
{
let mut already_seen_elements: HashSet<&A> = HashSet::new();
for x in xs {
if already_seen_elements.contains(x) {
return Some(x);
let mut already_seen_elements: HashSet<&T> = HashSet::new();
for t in ts {
if already_seen_elements.contains(t) {
return Some(t);
} else {
already_seen_elements.insert(&x);
already_seen_elements.insert(&t);
}
}
None
}
fn get_table_schema<'a>(db_schema: &DbSchema<'a>, table_name: &'a TableName) -> Option<&'a TableSchema> {
let (_, table_schema) = db_schema.iter().find(|(tname, _)| table_name.eq(tname))?;
let (_, _, table_schema) = db_schema.iter().find(|(tname, _, _)| table_name.eq(tname))?;
Some(table_schema)
}

View file

@ -4,6 +4,7 @@ use crate::reader::errors::{ProtoConsumeError, ProtoPeekError, ProtoReadError};
use crate::writer::errors::ProtoWriteError;
use thiserror::Error;
use tokio::io;
use crate::message::special::CancelRequestData;
#[derive(Debug, Error)]
pub enum ClientHandshakeError {
@ -23,6 +24,8 @@ pub enum ClientHandshakeError {
pub enum ServerHandshakeError {
#[error("startup message not found")]
MissingStartupMessage,
#[error("cancel request found instead of startup message")]
IsCancelRequest(CancelRequestData),
#[error("socket communication failed")]
Io(#[from] io::Error),
#[error("deserialization of inner data failed")]

View file

@ -8,6 +8,7 @@ use crate::writer::backend::BackendProtoWriter;
use crate::writer::protowriter::ProtoFlush;
/// Performs server-side handshake with the client until ending it with `ReadyForQuery` message.
/// Client can send `CancelRequest` message instead of `StartupMessage` to cancel the request.
/// For more info visit the [`55.2.1. Start-up`](https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-START-UP)
pub async fn do_server_handshake(
writer: &mut (impl BackendProtoWriter + ProtoFlush),
@ -27,12 +28,16 @@ pub async fn do_server_handshake(
}
}
// Wait for mandatory StartupMessage
// Wait for mandatory StartupMessage or CancelRequest
let startup_message = match &reader.peek_special_message().await? {
Some(msg @ SpecialMessage::StartupMessage(data)) => {
reader.consume_special_message(msg).await?;
data.clone()
}
Some(msg @ SpecialMessage::CancelRequest(data)) => {
reader.consume_special_message(msg).await?;
return Err(ServerHandshakeError::IsCancelRequest(data.clone()));
}
_ => {
return Err(ServerHandshakeError::MissingStartupMessage);
}

View file

@ -8,6 +8,9 @@ edition = "2021"
[dependencies]
tokio = { version = "1.35.1", features = ["full"] }
anyhow = "1.0.76"
proto = { path = "../proto" }
clap = { version = "4.4.18", features = ["derive"] }
async-trait = "0.1.74"
rand = "0.8.5"
minisql = { path = "../minisql" }
proto = { path = "../proto" }
parser = { path = "../parser" }

View file

@ -0,0 +1,34 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
pub struct ResetCancelToken {
is_canceled: Arc<AtomicBool>,
}
impl ResetCancelToken {
pub fn new() -> Self {
Self {
is_canceled: Arc::new(AtomicBool::new(false)),
}
}
pub fn is_canceled(&self) -> bool {
self.is_canceled.load(Ordering::SeqCst)
}
pub fn cancel(&self) {
self.is_canceled.store(true, Ordering::SeqCst);
}
pub fn reset(&self) {
self.is_canceled.store(false, Ordering::SeqCst);
}
}
impl Clone for ResetCancelToken {
fn clone(&self) -> Self {
Self {
is_canceled: self.is_canceled.clone(),
}
}
}

28
server/src/config.rs Normal file
View file

@ -0,0 +1,28 @@
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf;
use clap::Parser;
const LOCAL_IPV4: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
#[derive(Debug, Parser)]
#[command(author, version, about)]
pub struct Configuration {
#[arg(short, long, default_value_t = LOCAL_IPV4, help = "IP address for the server to listen on")]
address: IpAddr,
#[arg(short, long, default_value = "5432", help = "Port for the server to listen on")]
port: u16,
#[arg(short, long, help = "Path to the data file")]
file: PathBuf,
}
impl Configuration {
#[inline]
pub fn get_socket_address(&self) -> SocketAddr {
SocketAddr::new(self.address, self.port)
}
#[inline]
pub fn get_file_path(&self) -> &PathBuf {
&self.file
}
}

View file

@ -1,78 +1,139 @@
use minisql::interpreter::State;
use parser::{parse_and_validate, Error};
use std::collections::HashMap;
use std::sync::Arc;
use clap::Parser;
use tokio::io::{BufReader, BufWriter};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{Mutex, RwLock};
use minisql::interpreter::{Response, State};
use parser::parse_and_validate;
use proto::handshake::errors::ServerHandshakeError;
use proto::handshake::request::HandshakeRequest;
use proto::handshake::response::HandshakeResponse;
use proto::handshake::server::do_server_handshake;
use proto::message::backend::{
BackendMessage, ColumnDescription, CommandCompleteData, DataRowData, ErrorResponseData,
ReadyForQueryData, RowDescriptionData,
};
use proto::message::frontend::FrontendMessage;
use proto::reader::oneway::OneWayProtoReader;
use proto::reader::frontend::FrontendProtoReader;
use proto::reader::protoreader::ProtoReader;
use proto::writer::backend::BackendProtoWriter;
use proto::writer::protowriter::{ProtoFlush, ProtoWriter};
use tokio::io::{BufReader, BufWriter};
use tokio::net::{TcpListener, TcpStream};
use crate::cancellation::ResetCancelToken;
use crate::config::Configuration;
use crate::proto_wrapper::{CompleteStatus, ServerProto};
mod config;
mod proto_wrapper;
mod cancellation;
type TokenStore = Arc<Mutex<HashMap<(i32, i32), ResetCancelToken>>>;
type SharedDbState = Arc<RwLock<State>>;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let addr = "0.0.0.0:5432";
let config = Configuration::parse();
let state = Arc::new(RwLock::new(State::new()));
let tokens = Arc::new(Mutex::new(HashMap::<(i32, i32), ResetCancelToken>::new()));
let addr = config.get_socket_address();
let listener = TcpListener::bind(&addr).await?;
println!("Server started at {addr}");
loop {
let state = state.clone();
let tokens = tokens.clone();
let (socket, _) = listener.accept().await?;
println!("New client connected: {}", socket.peer_addr()?);
tokio::spawn(async move {
let reason = handle_stream(socket).await;
let reason = handle_stream(socket, state, tokens).await;
println!("Client disconnected: {reason:?}");
});
}
}
async fn handle_stream(mut stream: TcpStream) -> anyhow::Result<()> {
async fn handle_stream(mut stream: TcpStream, state: SharedDbState, tokens: TokenStore) -> anyhow::Result<()> {
let (reader, writer) = stream.split();
let mut writer = ProtoWriter::new(BufWriter::new(writer));
let mut reader = ProtoReader::new(BufReader::new(reader), 1024);
let response = HandshakeResponse::new("minisql", 123, 123);
// Create a token with random PID and key
let (pid, key, token) = create_token(&tokens).await?;
let request = do_server_handshake(&mut writer, &mut reader, response).await?;
// Handle handshake
let response = HandshakeResponse::new("minisql", pid, key);
let request = do_server_handshake(&mut writer, &mut reader, response).await;
println!("Handshake complete:\n{request:?}");
let mut state = State::new();
let result = match request {
Ok(req) => handle_connection(&mut reader, &mut writer, req, state, token).await,
Err(ServerHandshakeError::IsCancelRequest(cancel)) => handle_cancellation(cancel.pid, cancel.secret, &tokens).await,
Err(e) => Err(anyhow::anyhow!("Error during handshake: {:?}", e)),
};
// Release cancellation token
let mut tokens = tokens.lock().await;
tokens.remove(&(pid, key));
result
}
fn random_pid_key() -> (i32, i32) {
let pid = rand::random::<i32>();
let key = rand::random::<i32>();
(pid, key)
}
async fn create_token(tokens: &TokenStore) -> anyhow::Result<(i32, i32, ResetCancelToken)> {
let token = ResetCancelToken::new();
let mut tokens = tokens.lock().await;
loop {
let pid_key = random_pid_key();
if !tokens.contains_key(&pid_key) {
tokens.insert(pid_key, token.clone());
let (pid, key) = pid_key;
return Ok((pid, key, token));
}
}
}
async fn handle_cancellation(pid: i32, key: i32, tokens: &TokenStore) -> anyhow::Result<()> {
println!("Cancel request, PID: {}, Key: {}", pid, key);
let tokens = tokens.lock().await;
let token = tokens.get(&(pid, key));
match token {
Some(t) => t.cancel(),
None => return Err(anyhow::anyhow!("Invalid PID and Key cancel combination")),
}
Ok(())
}
async fn handle_connection<R, W>(reader: &mut R, writer: &mut W, request: HandshakeRequest, state: SharedDbState, token: ResetCancelToken) -> anyhow::Result<()>
where
R: FrontendProtoReader + Send,
W: BackendProtoWriter + ProtoFlush + Send,
{
println!("Client connected: {:?}", request);
loop {
println!("Waiting for next message");
let next: FrontendMessage = reader.read_proto().await?;
match next {
FrontendMessage::Terminate => {
println!("Received Terminate");
break;
}
FrontendMessage::Query(data) => {
println!("Received Query: {:?}", data);
let metadata = state.metadata();
match parse_and_validate(data.query.as_str().to_string(), &metadata) {
Ok(operation) => {
match state.interpret(operation) {
Ok(_) => {
send_query_response(&mut writer).await?;
}
Err(err) => {
send_error_response(&mut writer, &format!("error interpreting: {:?}", err)).await?;
let result = handle_query(writer, &state, data.query.into(), &token).await;
match result {
Ok(_) => {}
Err(e) => {
writer.write_error_message(&e.to_string()).await?
}
}
},
Err(Error::ParsingError(err)) => {
send_error_response(&mut writer, &format!("parsing error: {:?}", err)).await?;
}
Err(Error::ValidationError(v)) => {
send_error_response(&mut writer, &format!("validation error: {:?}", v)).await?;
}
};
send_ready_for_query(&mut writer).await?;
writer.write_ready_for_query().await?;
}
}
writer.flush().await?;
@ -81,117 +142,47 @@ async fn handle_stream(mut stream: TcpStream) -> anyhow::Result<()> {
Ok(())
}
async fn send_error_response(
writer: &mut impl BackendProtoWriter,
error_message: &str,
) -> anyhow::Result<()> {
writer
.write_proto(
ErrorResponseData {
code: b'M',
message: error_message.to_string().into(),
async fn handle_query<W>(writer: &mut W, state: &SharedDbState, query: String, token: &ResetCancelToken) -> anyhow::Result<()>
where
W: BackendProtoWriter + ProtoFlush + Send,
{
let operation = {
let state = state.read().await;
let db_schema = state.db_schema();
parse_and_validate(query, &db_schema)?
};
let mut state = state.write().await;
let response = state.interpret(operation)?;
match response {
Response::Deleted(i) => writer.write_command_complete(CompleteStatus::Delete(i)).await?,
Response::Inserted => writer.write_command_complete(CompleteStatus::Insert { oid: 0, rows: 1 }).await?,
Response::Selected(schema, mut rows) => {
match rows.next() {
Some(row) => {
writer.write_table_header(&schema, &row).await?;
writer.write_table_row(&row).await?;
let mut sent_rows = 1;
for row in rows {
sent_rows += 1;
writer.write_table_row(&row).await?;
if token.is_canceled() {
token.reset();
break;
}
}
writer.write_command_complete(CompleteStatus::Select(sent_rows)).await?;
}
_ => {
writer.write_command_complete(CompleteStatus::Select(0)).await?;
}
}
}
_ => {}
}
.into(),
)
.await?;
Ok(())
}
async fn send_ready_for_query(writer: &mut impl BackendProtoWriter) -> anyhow::Result<()> {
writer
.write_proto(BackendMessage::from(ReadyForQueryData { status: b'I' }))
.await?;
Ok(())
}
async fn send_empty_query(writer: &mut impl BackendProtoWriter) -> anyhow::Result<()> {
writer
.write_proto(BackendMessage::EmptyQueryResponse)
.await?;
Ok(())
}
async fn send_row_description(writer: &mut impl BackendProtoWriter) -> anyhow::Result<()> {
let columns = vec![
ColumnDescription {
name: "id".to_string().into(),
table_oid: 123,
column_index: 1,
type_oid: 23,
type_size: 4,
type_modifier: -1,
format_code: 0,
},
ColumnDescription {
name: "argument".to_string().into(),
table_oid: 123,
column_index: 2,
type_oid: 23,
type_size: 4,
type_modifier: -1,
format_code: 0,
},
ColumnDescription {
name: "description".to_string().into(),
table_oid: 123,
column_index: 3,
type_oid: 1043,
type_size: 32,
type_modifier: -1,
format_code: 0,
},
];
writer
.write_proto(
RowDescriptionData {
columns: columns.into(),
}
.into(),
)
.await?;
Ok(())
}
async fn send_query_response(writer: &mut impl BackendProtoWriter) -> anyhow::Result<()> {
send_row_description(writer).await?;
write_row(writer, b"0", b"1337", b"auto").await?;
write_row(writer, b"1", b"69", b"bus").await?;
write_row(writer, b"2", b"420", b"kolo").await?;
writer
.write_proto(
CommandCompleteData {
tag: "SELECT 3".to_string().into(),
}
.into(),
)
.await?;
Ok(())
}
async fn write_row(
writer: &mut impl BackendProtoWriter,
first: &[u8],
second: &[u8],
third: &[u8],
) -> anyhow::Result<()> {
let row_data = vec![
first.to_vec().into(),
second.to_vec().into(),
third.to_vec().into(),
]
.into();
writer
.write_proto(DataRowData { columns: row_data }.into())
.await?;
Ok(())
}

104
server/src/proto_wrapper.rs Normal file
View file

@ -0,0 +1,104 @@
use async_trait::async_trait;
use minisql::restricted_row::RestrictedRow;
use minisql::schema::TableSchema;
use minisql::type_system::{Value};
use proto::message::backend::{BackendMessage, ColumnDescription, CommandCompleteData, DataRowData, ErrorResponseData, ReadyForQueryData, RowDescriptionData};
use proto::message::primitive::pglist::PgList;
use proto::writer::backend::BackendProtoWriter;
pub enum CompleteStatus {
Insert {
oid: i32,
rows: i32,
},
Delete(usize),
Select(usize),
}
impl CompleteStatus {
fn to_string(&self) -> String {
match self {
CompleteStatus::Insert { oid, rows } => format!("INSERT {} {}", oid, rows),
CompleteStatus::Delete(rows) => format!("DELETE {}", rows),
CompleteStatus::Select(rows) => format!("SELECT {}", rows),
}
}
}
#[async_trait]
pub trait ServerProto {
async fn write_error_message(&mut self, error_message: &str) -> anyhow::Result<()>;
async fn write_ready_for_query(&mut self) -> anyhow::Result<()>;
async fn write_empty_query(&mut self) -> anyhow::Result<()>;
async fn write_table_header(&mut self, table_schema: &TableSchema, row: &RestrictedRow) -> anyhow::Result<()>;
async fn write_table_row(&mut self, row: &RestrictedRow) -> anyhow::Result<()>;
async fn write_command_complete(&mut self, status: CompleteStatus) -> anyhow::Result<()>;
}
#[async_trait]
impl<W> ServerProto for W where W: BackendProtoWriter + Send {
async fn write_error_message(&mut self, error_message: &str) -> anyhow::Result<()> {
self.write_proto(ErrorResponseData {
code: b'M',
message: format!("{error_message}\0").into(),
}.into()).await?;
Ok(())
}
async fn write_ready_for_query(&mut self) -> anyhow::Result<()> {
self.write_proto(ReadyForQueryData { status: b'I' }.into()).await?;
Ok(())
}
async fn write_empty_query(&mut self) -> anyhow::Result<()> {
self.write_proto(BackendMessage::EmptyQueryResponse).await?;
Ok(())
}
async fn write_table_header(&mut self, table_schema: &TableSchema, row: &RestrictedRow) -> anyhow::Result<()> {
let columns = row.iter()
.map(|(index, value)| value_to_column_description(table_schema, value, index))
.collect::<anyhow::Result<Vec<ColumnDescription>>>()?;
self.write_proto(RowDescriptionData { columns: columns.into() }.into()).await?;
Ok(())
}
async fn write_table_row(&mut self, row: &RestrictedRow) -> anyhow::Result<()> {
let values = row.iter()
.map(|(_, value)| value.as_text_bytes().into())
.collect::<Vec<PgList<u8, i32>>>();
self.write_proto(BackendMessage::DataRow(DataRowData {
columns: values.into(),
})).await?;
Ok(())
}
async fn write_command_complete(&mut self, status: CompleteStatus) -> anyhow::Result<()> {
self.write_proto(BackendMessage::CommandComplete(CommandCompleteData {
tag: status.to_string().into(),
})).await?;
Ok(())
}
}
fn value_to_column_description(schema: &TableSchema, value: &Value, index: &usize) -> anyhow::Result<ColumnDescription> {
let name = schema.column_name_from_column_position(*index)?;
let table_oid = schema.table_name().as_bytes().as_ptr() as i32;
let column_index = (*index).try_into()?;
let type_oid = value.type_oid();
let type_size = value.type_size();
Ok(ColumnDescription {
name: name.to_string().into(),
table_oid,
column_index,
type_oid,
type_size,
type_modifier: -1,
format_code: 0, // text format
})
}