Merge branch 'validation-tests' into 'main'
Tests for Validation See merge request x433485/minisql!11
This commit is contained in:
commit
85bc46c5b0
13 changed files with 595 additions and 187 deletions
|
|
@ -1,20 +1,17 @@
|
||||||
use std::num::{ParseFloatError, ParseIntError};
|
use std::num::{ParseFloatError, ParseIntError};
|
||||||
use std::str::Utf8Error;
|
use std::str::Utf8Error;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use crate::internals::row::ColumnPosition;
|
|
||||||
use crate::schema::{ColumnName, TableName};
|
use crate::schema::{ColumnName, TableName};
|
||||||
use crate::type_system::{DbType, Uuid, Value};
|
use crate::type_system::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum RuntimeError {
|
||||||
#[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}")]
|
#[error("table {0} already contains row with id {1}")]
|
||||||
AttemptingToInsertAlreadyPresentId(TableName, Uuid),
|
AttemptingToInsertAlreadyPresentId(TableName, Uuid),
|
||||||
#[error("table {0} cannot be indexed on column {1}")]
|
#[error("table {0} cannot be indexed on column {1}")]
|
||||||
AttemptToIndexNonIndexableColumn(TableName, ColumnName),
|
AttemptToIndexNonIndexableColumn(TableName, ColumnName),
|
||||||
|
#[error("table {0} already indexes column {1}")]
|
||||||
|
AttemptToIndexAlreadyIndexedColumn(TableName, ColumnName),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
|
|
||||||
|
|
@ -61,16 +61,16 @@ impl Row {
|
||||||
self.0.len()
|
self.0.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, column_position: ColumnPosition) -> Option<&Value> {
|
pub fn get(&self, column: ColumnPosition) -> Option<&Value> {
|
||||||
self.0.get(column_position)
|
self.0.get(column)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn restrict_columns(&self, columns: &Vec<ColumnPosition>) -> RestrictedRow {
|
pub fn restrict_columns(&self, columns: &Vec<ColumnPosition>) -> RestrictedRow {
|
||||||
// If the index from `columns` is non-existant in `row`, it will just ignore it.
|
// If the index from `columns` is non-existant in `row`, it will just ignore it.
|
||||||
let mut subrow: Vec<(ColumnPosition, Value)> = vec![];
|
let mut subrow: Vec<(ColumnPosition, Value)> = vec![];
|
||||||
for column_position in columns {
|
for column in columns {
|
||||||
if let Some(value) = self.get(*column_position) {
|
if let Some(value) = self.get(*column) {
|
||||||
subrow.push((*column_position, value.clone()));
|
subrow.push((*column, value.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::error::RuntimeError;
|
||||||
use crate::internals::column_index::ColumnIndex;
|
use crate::internals::column_index::ColumnIndex;
|
||||||
use crate::internals::row::{ColumnPosition, Row};
|
use crate::internals::row::{ColumnPosition, Row};
|
||||||
use crate::restricted_row::RestrictedRow;
|
use crate::restricted_row::RestrictedRow;
|
||||||
|
|
@ -55,12 +55,12 @@ impl Table {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_rows_by_value(&self, column_position: ColumnPosition, value: &Value) -> Vec<Row> {
|
fn get_rows_by_value(&self, column: ColumnPosition, value: &Value) -> Vec<Row> {
|
||||||
// brute-force search
|
// brute-force search
|
||||||
self.rows
|
self.rows
|
||||||
.values()
|
.values()
|
||||||
.filter_map(|row| {
|
.filter_map(|row| {
|
||||||
if row.get(column_position) == Some(value) {
|
if row.get(column) == Some(value) {
|
||||||
Some(row.clone())
|
Some(row.clone())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -69,21 +69,21 @@ impl Table {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_all_rows<'a>(&'a self, selected_column_positions: Vec<ColumnPosition>) -> impl Iterator<Item=RestrictedRow> + 'a {
|
pub fn select_all_rows<'a>(&'a self, selected_columns: Vec<ColumnPosition>) -> impl Iterator<Item=RestrictedRow> + 'a {
|
||||||
self.rows
|
self.rows
|
||||||
.values()
|
.values()
|
||||||
.map(move |row| row.restrict_columns(&selected_column_positions))
|
.map(move |row| row.restrict_columns(&selected_columns))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_rows_where_eq<'a>(
|
pub fn select_rows_where_eq<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
selected_column_positions: Vec<ColumnPosition>,
|
selected_columns: Vec<ColumnPosition>,
|
||||||
column_position: ColumnPosition,
|
column: ColumnPosition,
|
||||||
value: Value,
|
value: Value,
|
||||||
) -> DbResult<impl Iterator<Item=RestrictedRow> + 'a> {
|
) -> DbResult<impl Iterator<Item=RestrictedRow> + 'a> {
|
||||||
let restrict_columns_of_row = move |row: Row| row.restrict_columns(&selected_column_positions);
|
let restrict_columns_of_row = move |row: Row| row.restrict_columns(&selected_columns);
|
||||||
match value {
|
match value {
|
||||||
Value::Indexable(value) => match self.fetch_ids_from_index(column_position, &value)? {
|
Value::Indexable(value) => match self.fetch_ids_from_index(column, &value)? {
|
||||||
Some(ids) =>
|
Some(ids) =>
|
||||||
Ok(self
|
Ok(self
|
||||||
.get_rows_by_ids(ids)
|
.get_rows_by_ids(ids)
|
||||||
|
|
@ -92,14 +92,14 @@ impl Table {
|
||||||
),
|
),
|
||||||
None =>
|
None =>
|
||||||
Ok(self
|
Ok(self
|
||||||
.get_rows_by_value(column_position, &Value::Indexable(value))
|
.get_rows_by_value(column, &Value::Indexable(value))
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(restrict_columns_of_row)
|
.map(restrict_columns_of_row)
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
_ =>
|
_ =>
|
||||||
Ok(self
|
Ok(self
|
||||||
.get_rows_by_value(column_position, &value)
|
.get_rows_by_value(column, &value)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(restrict_columns_of_row)
|
.map(restrict_columns_of_row)
|
||||||
),
|
),
|
||||||
|
|
@ -109,22 +109,16 @@ impl Table {
|
||||||
// ======Insertion======
|
// ======Insertion======
|
||||||
pub fn insert_row_at(&mut self, id: Uuid, row: Row) -> DbResult<()> {
|
pub fn insert_row_at(&mut self, id: Uuid, row: Row) -> DbResult<()> {
|
||||||
if self.rows.get(&id).is_some() {
|
if self.rows.get(&id).is_some() {
|
||||||
return Err(Error::AttemptingToInsertAlreadyPresentId(
|
return Err(RuntimeError::AttemptingToInsertAlreadyPresentId(
|
||||||
self.table_name().clone(),
|
self.table_name().clone(),
|
||||||
id,
|
id,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (column_position, column_index) in &mut self.indexes {
|
for (column, column_index) in &mut self.indexes {
|
||||||
match row.get(*column_position) {
|
match &row[*column] {
|
||||||
Some(Value::Indexable(val)) => column_index.add(val.clone(), id),
|
Value::Indexable(val) => column_index.add(val.clone(), id),
|
||||||
Some(_) => {}
|
_ => {},
|
||||||
None => {
|
|
||||||
return Err(Error::ColumnPositionDoesNotExist(
|
|
||||||
self.schema.table_name().clone(), // Note that I can't simply use self.table_name() here because of rust borrowing rules.
|
|
||||||
*column_position,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,8 +130,8 @@ impl Table {
|
||||||
fn delete_row_by_id(&mut self, id: Uuid) -> usize {
|
fn delete_row_by_id(&mut self, id: Uuid) -> usize {
|
||||||
match self.rows.remove(&id) {
|
match self.rows.remove(&id) {
|
||||||
Some(row) => {
|
Some(row) => {
|
||||||
for (column_position, column_index) in &mut self.indexes {
|
for (column, column_index) in &mut self.indexes {
|
||||||
if let Value::Indexable(value) = &row[*column_position] {
|
if let Value::Indexable(value) = &row[*column] {
|
||||||
let _ = column_index.remove(value, id);
|
let _ = column_index.remove(value, id);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -155,12 +149,12 @@ impl Table {
|
||||||
total_count
|
total_count
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_rows_by_value(&mut self, column_position: ColumnPosition, value: &Value) -> usize {
|
fn delete_rows_by_value(&mut self, column: ColumnPosition, value: &Value) -> usize {
|
||||||
let matched_ids: HashSet<Uuid> = self
|
let matched_ids: HashSet<Uuid> = self
|
||||||
.rows
|
.rows
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(id, row)| {
|
.filter_map(|(id, row)| {
|
||||||
if row.get(column_position) == Some(value) {
|
if row.get(column) == Some(value) {
|
||||||
Some(*id)
|
Some(*id)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -179,50 +173,43 @@ impl Table {
|
||||||
|
|
||||||
pub fn delete_rows_where_eq(
|
pub fn delete_rows_where_eq(
|
||||||
&mut self,
|
&mut self,
|
||||||
column_position: ColumnPosition,
|
column: ColumnPosition,
|
||||||
value: Value,
|
value: Value,
|
||||||
) -> DbResult<usize> {
|
) -> DbResult<usize> {
|
||||||
match value {
|
match value {
|
||||||
Value::Indexable(value) => match self.fetch_ids_from_index(column_position, &value)? {
|
Value::Indexable(value) => match self.fetch_ids_from_index(column, &value)? {
|
||||||
Some(ids) => Ok(self.delete_rows_by_ids(ids)),
|
Some(ids) => Ok(self.delete_rows_by_ids(ids)),
|
||||||
None => Ok(self.delete_rows_by_value(column_position, &Value::Indexable(value))),
|
None => Ok(self.delete_rows_by_value(column, &Value::Indexable(value))),
|
||||||
},
|
},
|
||||||
_ => Ok(self.delete_rows_by_value(column_position, &value)),
|
_ => Ok(self.delete_rows_by_value(column, &value)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======Indexing======
|
// ======Indexing======
|
||||||
pub fn attach_index(&mut self, column_position: ColumnPosition) -> DbResult<()> {
|
pub fn attach_index(&mut self, column: ColumnPosition) -> DbResult<()> {
|
||||||
|
if self.indexes.get(&column).is_some() {
|
||||||
|
let column_name = self.schema.column_name_from_column(column).clone();
|
||||||
|
let table_name = self.schema.table_name().clone();
|
||||||
|
return Err(RuntimeError::AttemptToIndexAlreadyIndexedColumn(table_name, column_name))
|
||||||
|
}
|
||||||
let mut column_index: ColumnIndex = ColumnIndex::new();
|
let mut column_index: ColumnIndex = ColumnIndex::new();
|
||||||
update_index_from_table(&mut column_index, self, column_position)?;
|
update_index_from_table(&mut column_index, self, column)?;
|
||||||
self.indexes.insert(column_position, column_index);
|
self.indexes.insert(column, column_index);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_ids_from_index(
|
fn fetch_ids_from_index(
|
||||||
&self,
|
&self,
|
||||||
column_position: ColumnPosition,
|
column: ColumnPosition,
|
||||||
value: &IndexableValue,
|
value: &IndexableValue,
|
||||||
) -> DbResult<Option<HashSet<Uuid>>> {
|
) -> DbResult<Option<HashSet<Uuid>>> {
|
||||||
if self.schema.is_primary(column_position) {
|
if self.schema.is_primary(column) {
|
||||||
match value {
|
match value {
|
||||||
IndexableValue::Uuid(id) => Ok(Some(HashSet::from([*id]))),
|
IndexableValue::Uuid(id) => Ok(Some(HashSet::from([*id]))),
|
||||||
_ => {
|
_ => unreachable!() // SAFETY: Validation guarantees primary column has correct Uuid type.
|
||||||
// TODO: This validation step is not really necessary.
|
|
||||||
let column_name: ColumnName = self
|
|
||||||
.schema
|
|
||||||
.column_name_from_column_position(column_position)?;
|
|
||||||
let type_ = self.schema.column_type(column_position);
|
|
||||||
Err(Error::ValueDoesNotMatchExpectedType(
|
|
||||||
self.table_name().clone(),
|
|
||||||
column_name,
|
|
||||||
type_,
|
|
||||||
Value::Indexable(value.clone()),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match self.indexes.get(&column_position) {
|
match self.indexes.get(&column) {
|
||||||
Some(index) => {
|
Some(index) => {
|
||||||
// Note that we are cloning the ids here! This can be very wasteful in some cases.
|
// Note that we are cloning the ids here! This can be very wasteful in some cases.
|
||||||
// Theoretically it would be possible to return a reference,
|
// Theoretically it would be possible to return a reference,
|
||||||
|
|
@ -241,26 +228,21 @@ impl Table {
|
||||||
fn update_index_from_table(
|
fn update_index_from_table(
|
||||||
column_index: &mut ColumnIndex,
|
column_index: &mut ColumnIndex,
|
||||||
table: &Table,
|
table: &Table,
|
||||||
column_position: ColumnPosition,
|
column: ColumnPosition,
|
||||||
) -> DbResult<()> {
|
) -> DbResult<()> {
|
||||||
for (id, row) in &table.rows {
|
for (id, row) in &table.rows {
|
||||||
let value = match row.get(column_position) {
|
let value = match &row[column] {
|
||||||
Some(Value::Indexable(value)) => value.clone(),
|
Value::Indexable(value) => value.clone(),
|
||||||
Some(_) => {
|
_ => {
|
||||||
let column_name: ColumnName = table
|
let column_name: ColumnName = table
|
||||||
.schema
|
.schema
|
||||||
.column_name_from_column_position(column_position)?;
|
.column_name_from_column(column);
|
||||||
return Err(Error::AttemptToIndexNonIndexableColumn(
|
// TODO: Perhaps this should be handled in validation?
|
||||||
|
return Err(RuntimeError::AttemptToIndexNonIndexableColumn(
|
||||||
table.table_name().to_string(),
|
table.table_name().to_string(),
|
||||||
column_name,
|
column_name,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
None => {
|
|
||||||
return Err(Error::ColumnPositionDoesNotExist(
|
|
||||||
table.table_name().to_string(),
|
|
||||||
column_position,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
column_index.add(value, *id)
|
column_index.add(value, *id)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,10 +74,10 @@ impl State {
|
||||||
&mut self.tables[table_position]
|
&mut self.tables[table_position]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn attach_table(&mut self, table_name: TableName, table: Table) {
|
fn attach_table(&mut self, table: Table) {
|
||||||
let new_table_position: TablePosition = self.tables.len();
|
let new_table_position: TablePosition = self.tables.len();
|
||||||
self.table_name_position_mapping
|
self.table_name_position_mapping
|
||||||
.insert(table_name, new_table_position);
|
.insert(table.schema().table_name().clone(), new_table_position);
|
||||||
self.tables.push(table);
|
self.tables.push(table);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,9 +127,9 @@ impl State {
|
||||||
|
|
||||||
Ok(Response::Deleted(rows_affected))
|
Ok(Response::Deleted(rows_affected))
|
||||||
}
|
}
|
||||||
CreateTable(table_name, table_schema) => {
|
CreateTable(table_schema) => {
|
||||||
let table = Table::new(table_schema);
|
let table = Table::new(table_schema);
|
||||||
self.attach_table(table_name, table);
|
self.attach_table(table);
|
||||||
|
|
||||||
Ok(Response::TableCreated)
|
Ok(Response::TableCreated)
|
||||||
}
|
}
|
||||||
|
|
@ -151,17 +151,13 @@ mod tests {
|
||||||
use crate::operation::Operation;
|
use crate::operation::Operation;
|
||||||
|
|
||||||
fn users_schema() -> TableSchema {
|
fn users_schema() -> TableSchema {
|
||||||
let id: ColumnPosition = 0;
|
|
||||||
let name: ColumnPosition = 1;
|
|
||||||
let age: ColumnPosition = 2;
|
|
||||||
|
|
||||||
TableSchema::new(
|
TableSchema::new(
|
||||||
"users".to_string(),
|
"users".to_string(),
|
||||||
id,
|
"id".to_string(),
|
||||||
vec!(
|
vec!(
|
||||||
("id".to_string(), id),
|
"id".to_string(),
|
||||||
("name".to_string(), name),
|
"name".to_string(),
|
||||||
("age".to_string(), age),
|
"age".to_string(),
|
||||||
),
|
),
|
||||||
vec![DbType::Uuid, DbType::String, DbType::Int],
|
vec![DbType::Uuid, DbType::String, DbType::Int],
|
||||||
)
|
)
|
||||||
|
|
@ -174,7 +170,7 @@ mod tests {
|
||||||
let users = users_schema.table_name().clone();
|
let users = users_schema.table_name().clone();
|
||||||
|
|
||||||
state
|
state
|
||||||
.interpret(Operation::CreateTable(users.clone(), users_schema))
|
.interpret(Operation::CreateTable(users_schema))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(state.tables.len() == 1);
|
assert!(state.tables.len() == 1);
|
||||||
|
|
@ -188,11 +184,10 @@ mod tests {
|
||||||
fn test_select_empty() {
|
fn test_select_empty() {
|
||||||
let mut state = State::new();
|
let mut state = State::new();
|
||||||
let users_schema = users_schema();
|
let users_schema = users_schema();
|
||||||
let users = users_schema.table_name().clone();
|
|
||||||
let users_position = 0;
|
let users_position = 0;
|
||||||
|
|
||||||
state
|
state
|
||||||
.interpret(Operation::CreateTable(users, users_schema.clone()))
|
.interpret(Operation::CreateTable(users_schema.clone()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let response: Response = state
|
let response: Response = state
|
||||||
.interpret(Operation::Select(users_position, users_schema.all_selection(), None))
|
.interpret(Operation::Select(users_position, users_schema.all_selection(), None))
|
||||||
|
|
@ -216,7 +211,7 @@ mod tests {
|
||||||
|
|
||||||
|
|
||||||
state
|
state
|
||||||
.interpret(Operation::CreateTable("users".to_string(), users_schema.clone()))
|
.interpret(Operation::CreateTable(users_schema.clone()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (id, name, age) = (
|
let (id, name, age) = (
|
||||||
|
|
@ -268,7 +263,7 @@ mod tests {
|
||||||
let name_column: ColumnPosition = 1;
|
let name_column: ColumnPosition = 1;
|
||||||
|
|
||||||
state
|
state
|
||||||
.interpret(CreateTable(users_schema.table_name().clone(), users_schema.clone()))
|
.interpret(CreateTable(users_schema.clone()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (id0, name0, age0) = (
|
let (id0, name0, age0) = (
|
||||||
|
|
@ -385,7 +380,7 @@ mod tests {
|
||||||
let id_column: ColumnPosition = 0;
|
let id_column: ColumnPosition = 0;
|
||||||
|
|
||||||
state
|
state
|
||||||
.interpret(CreateTable(users_schema.table_name().clone(), users_schema.clone()))
|
.interpret(CreateTable(users_schema.clone()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (id0, name0, age0) = (
|
let (id0, name0, age0) = (
|
||||||
|
|
@ -459,7 +454,7 @@ mod tests {
|
||||||
let name_column: ColumnPosition = 1;
|
let name_column: ColumnPosition = 1;
|
||||||
|
|
||||||
state
|
state
|
||||||
.interpret(CreateTable(users_schema.table_name().clone(), users_schema.clone()))
|
.interpret(CreateTable(users_schema.clone()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
state
|
state
|
||||||
|
|
@ -526,26 +521,25 @@ pub fn example() {
|
||||||
|
|
||||||
let id_column: ColumnPosition = 0;
|
let id_column: ColumnPosition = 0;
|
||||||
let name_column: ColumnPosition = 1;
|
let name_column: ColumnPosition = 1;
|
||||||
let age_column: ColumnPosition = 2;
|
// let age_column: ColumnPosition = 2;
|
||||||
|
|
||||||
let users_schema: TableSchema = {
|
let users_schema: TableSchema = {
|
||||||
TableSchema::new(
|
TableSchema::new(
|
||||||
"users".to_string(),
|
"users".to_string(),
|
||||||
id_column,
|
"id".to_string(),
|
||||||
vec!(
|
vec!(
|
||||||
("id".to_string(), id_column),
|
"id".to_string(), // 0
|
||||||
("name".to_string(), name_column),
|
"name".to_string(), // 1
|
||||||
("age".to_string(), age_column),
|
"age".to_string(), // 2
|
||||||
),
|
),
|
||||||
vec![DbType::Uuid, DbType::String, DbType::Int],
|
vec![DbType::Uuid, DbType::String, DbType::Int],
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let users_position: TablePosition = 0;
|
let users_position: TablePosition = 0;
|
||||||
let users = users_schema.table_name().clone();
|
|
||||||
|
|
||||||
let mut state = State::new();
|
let mut state = State::new();
|
||||||
state
|
state
|
||||||
.interpret(Operation::CreateTable(users, users_schema.clone()))
|
.interpret(Operation::CreateTable(users_schema.clone()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (id0, name0, age0) = (
|
let (id0, name0, age0) = (
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,24 @@
|
||||||
use crate::schema::{TableName, TableSchema};
|
use crate::schema::TableSchema;
|
||||||
use crate::type_system::Value;
|
use crate::type_system::Value;
|
||||||
use crate::internals::row::ColumnPosition;
|
use crate::internals::row::ColumnPosition;
|
||||||
use crate::interpreter::TablePosition;
|
use crate::interpreter::TablePosition;
|
||||||
|
|
||||||
// Validated operation. Constructed by validation crate.
|
// Validated operation. Constructed by validation crate.
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum Operation {
|
pub enum Operation {
|
||||||
Select(TablePosition, ColumnSelection, Option<Condition>),
|
Select(TablePosition, ColumnSelection, Option<Condition>),
|
||||||
Insert(TablePosition, InsertionValues),
|
Insert(TablePosition, InsertionValues),
|
||||||
Delete(TablePosition, Option<Condition>),
|
Delete(TablePosition, Option<Condition>),
|
||||||
CreateTable(TableName, TableSchema),
|
CreateTable(TableSchema),
|
||||||
CreateIndex(TablePosition, ColumnPosition),
|
CreateIndex(TablePosition, ColumnPosition),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assumes that these are sorted by column position.
|
||||||
pub type InsertionValues = Vec<Value>;
|
pub type InsertionValues = Vec<Value>;
|
||||||
|
|
||||||
pub type ColumnSelection = Vec<ColumnPosition>;
|
pub type ColumnSelection = Vec<ColumnPosition>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum Condition {
|
pub enum Condition {
|
||||||
Eq(ColumnPosition, Value),
|
Eq(ColumnPosition, Value),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
use crate::error::Error;
|
use crate::error::RuntimeError;
|
||||||
|
|
||||||
pub type DbResult<A> = Result<A, Error>;
|
pub type DbResult<A> = Result<A, RuntimeError>;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::error::Error;
|
|
||||||
use crate::internals::row::{ColumnPosition, Row};
|
use crate::internals::row::{ColumnPosition, Row};
|
||||||
use crate::operation::{InsertionValues, ColumnSelection};
|
use crate::operation::{InsertionValues, ColumnSelection};
|
||||||
use crate::result::DbResult;
|
use crate::result::DbResult;
|
||||||
|
|
@ -8,7 +7,7 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
// Note that it is nice to split metadata from the data because
|
// 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.
|
// then you can give the metadata to the parser without giving it the data.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct TableSchema {
|
pub struct TableSchema {
|
||||||
table_name: TableName, // used for descriptive errors
|
table_name: TableName, // used for descriptive errors
|
||||||
primary_key: ColumnPosition,
|
primary_key: ColumnPosition,
|
||||||
|
|
@ -20,11 +19,15 @@ pub type TableName = String;
|
||||||
pub type ColumnName = String;
|
pub type ColumnName = String;
|
||||||
|
|
||||||
impl TableSchema {
|
impl TableSchema {
|
||||||
pub fn new(table_name: TableName, primary_key: ColumnPosition, column_name_position_map: Vec<(ColumnName, ColumnPosition)>, types: Vec<DbType>) -> Self {
|
pub fn new(table_name: TableName, primary_column_name: ColumnName, columns: Vec<ColumnName>, types: Vec<DbType>) -> Self {
|
||||||
let mut column_name_position_mapping: BiMap<ColumnName, ColumnPosition> = BiMap::new();
|
let mut column_name_position_mapping: BiMap<ColumnName, ColumnPosition> = BiMap::new();
|
||||||
for (column_name, column_position) in column_name_position_map {
|
for (column, column_name) in columns.into_iter().enumerate() {
|
||||||
column_name_position_mapping.insert(column_name, column_position);
|
column_name_position_mapping.insert(column_name, column);
|
||||||
}
|
}
|
||||||
|
let primary_key: ColumnPosition = match column_name_position_mapping.get_by_left(&primary_column_name).copied() {
|
||||||
|
Some(primary_key) => primary_key,
|
||||||
|
None => unreachable!() // SAFETY: Existence of unique primary key is ensured in validation.
|
||||||
|
};
|
||||||
Self { table_name, primary_key, column_name_position_mapping, types }
|
Self { table_name, primary_key, column_name_position_mapping, types }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,8 +35,8 @@ impl TableSchema {
|
||||||
&self.table_name
|
&self.table_name
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_type(&self, column_position: ColumnPosition) -> DbType {
|
pub fn column_type(&self, column: ColumnPosition) -> DbType {
|
||||||
self.types[column_position]
|
self.types[column]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_columns(&self) -> Vec<&ColumnName> {
|
pub fn get_columns(&self) -> Vec<&ColumnName> {
|
||||||
|
|
@ -64,23 +67,19 @@ impl TableSchema {
|
||||||
self.types.get(position).copied()
|
self.types.get(position).copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_primary(&self, column_position: ColumnPosition) -> bool {
|
pub fn is_primary(&self, column: ColumnPosition) -> bool {
|
||||||
self.primary_key == column_position
|
self.primary_key == column
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_name_from_column_position(
|
// Assumes `column` comes from a validated source.
|
||||||
&self,
|
pub fn column_name_from_column(&self, column: ColumnPosition) -> ColumnName {
|
||||||
column_position: ColumnPosition,
|
|
||||||
) -> DbResult<ColumnName> {
|
|
||||||
match self
|
match self
|
||||||
.column_name_position_mapping
|
.column_name_position_mapping
|
||||||
.get_by_right(&column_position)
|
.get_by_right(&column)
|
||||||
{
|
{
|
||||||
Some(column_name) => Ok(column_name.clone()),
|
Some(column_name) => column_name.clone(),
|
||||||
None => Err(Error::ColumnPositionDoesNotExist(
|
None => unreachable!() // SAFETY: The only way this function can get a column is from
|
||||||
self.table_name.clone(),
|
// validation, which guarantees there is such a colun.
|
||||||
column_position,
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,8 +95,8 @@ impl TableSchema {
|
||||||
|
|
||||||
let id: Uuid = match row.get(self.primary_key) {
|
let id: Uuid = match row.get(self.primary_key) {
|
||||||
Some(Value::Indexable(IndexableValue::Uuid(id))) => *id,
|
Some(Value::Indexable(IndexableValue::Uuid(id))) => *id,
|
||||||
Some(_) => unreachable!(), // SAFETY: Should be guaranteed by validation
|
Some(_) => unreachable!(), // SAFETY: Should be guaranteed by validation (type-safety)
|
||||||
None => unreachable!(), // SAFETY: Should be guaranteed by validation
|
None => unreachable!(), // SAFETY: Should be guaranteed by validation (missing columns)
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((id, row))
|
Ok((id, row))
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,18 @@ pub enum IndexableValue {
|
||||||
// TODO: what about null?
|
// TODO: what about null?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DbType {
|
||||||
|
pub fn is_indexable(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::String => true,
|
||||||
|
Self::Int => true,
|
||||||
|
Self::Number => false,
|
||||||
|
Self::Uuid => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
impl Value {
|
impl Value {
|
||||||
pub fn to_type(&self) -> DbType {
|
pub fn to_type(&self) -> DbType {
|
||||||
match self {
|
match self {
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ pub fn parse_statements<'a>(input: &'a str) -> IResult<&str, Vec<RawQuerySyntax>
|
||||||
many0(parse_statement)(input)
|
many0(parse_statement)(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_and_validate(query: String, db_schema: &DbSchema) -> Result<Operation, Error> {
|
pub fn parse_and_validate(str_query: String, db_schema: &DbSchema) -> Result<Operation, Error> {
|
||||||
let (_, op) = parse_statement(query.as_str())
|
let (_, op) = parse_statement(str_query.as_str())
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
Error::ParsingError(err.to_string())
|
Error::ParsingError(err.to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use minisql::{schema::{ColumnName, TableSchema}, type_system::DbType};
|
|
||||||
use nom::{
|
use nom::{
|
||||||
bytes::complete::tag,
|
bytes::complete::tag,
|
||||||
character::complete::{char, multispace0, multispace1},
|
character::complete::{char, multispace0, multispace1},
|
||||||
|
|
@ -8,7 +7,7 @@ use nom::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::common::{parse_table_name, parse_identifier, parse_db_type};
|
use super::common::{parse_table_name, parse_identifier, parse_db_type};
|
||||||
use crate::syntax::RawQuerySyntax;
|
use crate::syntax::{RawTableSchema, ColumnSchema, RawQuerySyntax};
|
||||||
|
|
||||||
pub fn parse_create(input: &str) -> IResult<&str, RawQuerySyntax> {
|
pub fn parse_create(input: &str) -> IResult<&str, RawQuerySyntax> {
|
||||||
let (input, _) = tag("CREATE")(input)?;
|
let (input, _) = tag("CREATE")(input)?;
|
||||||
|
|
@ -20,33 +19,21 @@ pub fn parse_create(input: &str) -> IResult<&str, RawQuerySyntax> {
|
||||||
let (input, _) = char('(')(input)?;
|
let (input, _) = char('(')(input)?;
|
||||||
let (input, _) = multispace0(input)?;
|
let (input, _) = multispace0(input)?;
|
||||||
let (input, column_definitions) = parse_column_definitions(input)?;
|
let (input, column_definitions) = parse_column_definitions(input)?;
|
||||||
let mut column_name_position_mapping = Vec::new();
|
|
||||||
let mut types: Vec<DbType> = Vec::new();
|
|
||||||
let mut primary_key = None;
|
|
||||||
for (position, (column_name, db_type, pk)) in column_definitions.iter().enumerate() {
|
|
||||||
types.push(db_type.clone());
|
|
||||||
if *pk {
|
|
||||||
primary_key = Some(position);
|
|
||||||
}
|
|
||||||
column_name_position_mapping.push((column_name.clone(), position));
|
|
||||||
}
|
|
||||||
|
|
||||||
let (input, _) = char(')')(input)?;
|
let (input, _) = char(')')(input)?;
|
||||||
let (input, _) = multispace0(input)?;
|
let (input, _) = multispace0(input)?;
|
||||||
let (input, _) = char(';')(input)?;
|
let (input, _) = char(';')(input)?;
|
||||||
let schema = TableSchema::new(
|
let schema = RawTableSchema {
|
||||||
table_name.to_string(),
|
table_name: table_name.to_string(),
|
||||||
primary_key.unwrap_or_default(),
|
columns: column_definitions,
|
||||||
column_name_position_mapping,
|
};
|
||||||
types
|
|
||||||
);
|
|
||||||
Ok((
|
Ok((
|
||||||
input,
|
input,
|
||||||
RawQuerySyntax::CreateTable(table_name.to_string(), schema),
|
RawQuerySyntax::CreateTable(schema),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_column_definitions(input: &str) -> IResult<&str, Vec<(ColumnName, DbType, bool)>> {
|
fn parse_column_definitions(input: &str) -> IResult<&str, Vec<ColumnSchema>> {
|
||||||
separated_list0(terminated(char(','), multispace0), parse_column_definition)(input)
|
separated_list0(terminated(char(','), multispace0), parse_column_definition)(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,13 +45,13 @@ fn parse_primary_key(input: &str) -> IResult<&str, &str> {
|
||||||
Ok((input, "PRIMARY KEY"))
|
Ok((input, "PRIMARY KEY"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_column_definition(input: &str) -> IResult<&str, (ColumnName, DbType, bool)> {
|
fn parse_column_definition(input: &str) -> IResult<&str, ColumnSchema> {
|
||||||
let (input, identifier) = parse_identifier(input)?;
|
let (input, identifier) = parse_identifier(input)?;
|
||||||
let (input, _) = multispace1(input)?;
|
let (input, _) = multispace1(input)?;
|
||||||
let (input, db_type) = parse_db_type(input)?;
|
let (input, db_type) = parse_db_type(input)?;
|
||||||
let (input, pk) = opt(parse_primary_key)(input).map(|(input, pk)| (input, pk.is_some()))?;
|
let (input, pk) = opt(parse_primary_key)(input).map(|(input, pk)| (input, pk.is_some()))?;
|
||||||
let (input, _) = multispace0(input)?;
|
let (input, _) = multispace0(input)?;
|
||||||
Ok((input, (identifier.to_string(), db_type, pk)))
|
Ok((input, ColumnSchema { column_name: identifier.to_string(), type_: db_type, is_primary: pk }))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -95,16 +82,23 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_create() {
|
fn test_parse_create() {
|
||||||
let (_, create) = parse_create("CREATE TABLE \"Table1\"( id UUID , column1 INT );").expect("should parse");
|
let (_, create) = parse_create("CREATE TABLE \"Table1\"( id UUID , column1 INT );").expect("should parse");
|
||||||
assert!(matches!(create, RawQuerySyntax::CreateTable(_ ,_)));
|
assert!(matches!(create, RawQuerySyntax::CreateTable(_)));
|
||||||
match create {
|
match create {
|
||||||
RawQuerySyntax::CreateTable(name, schema) => {
|
RawQuerySyntax::CreateTable(schema) => {
|
||||||
assert_eq!(name, "Table1");
|
assert_eq!(schema.table_name, "Table1");
|
||||||
assert_eq!(schema.number_of_columns(), 2);
|
assert_eq!(schema.number_of_columns(), 2);
|
||||||
assert_eq!(schema.get_column_position(&"id".to_string()).unwrap(), 0);
|
|
||||||
assert_eq!(schema.get_column_position(&"column1".to_string()).unwrap(), 1);
|
let result_id = schema.get_column(&"id".to_string());
|
||||||
|
assert!(matches!(result_id, Some(_)));
|
||||||
|
let Some(id_column) = result_id else { panic!() };
|
||||||
|
assert_eq!(id_column.column_name, "id".to_string());
|
||||||
|
|
||||||
|
let result_column1 = schema.get_column(&"column1".to_string());
|
||||||
|
assert!(matches!(result_column1, Some(_)));
|
||||||
|
let Some(column1_column) = result_column1 else { panic!() };
|
||||||
|
assert_eq!(column1_column.column_name, "column1".to_string());
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,29 @@
|
||||||
use minisql::{type_system::Value, schema::{TableSchema, ColumnName, TableName}};
|
use minisql::{type_system::{Value, DbType}, schema::{ColumnName, TableName}};
|
||||||
|
|
||||||
// TODO: Move this out into separate file and rename to something like Syntax, SyntaxTree,
|
// ===Table Schema===
|
||||||
// OperationSyntax, RawOperationSyntax
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct RawTableSchema {
|
||||||
|
pub table_name: TableName,
|
||||||
|
pub columns: Vec<ColumnSchema>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct ColumnSchema {
|
||||||
|
pub column_name: ColumnName,
|
||||||
|
pub type_: DbType,
|
||||||
|
pub is_primary: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===Query===
|
||||||
pub enum RawQuerySyntax {
|
pub enum RawQuerySyntax {
|
||||||
Select(TableName, ColumnSelection, Option<Condition>),
|
Select(TableName, ColumnSelection, Option<Condition>),
|
||||||
Insert(TableName, InsertionValues),
|
Insert(TableName, InsertionValues),
|
||||||
Delete(TableName, Option<Condition>),
|
Delete(TableName, Option<Condition>),
|
||||||
// Update(...),
|
// Update(...),
|
||||||
CreateTable(TableName, TableSchema),
|
CreateTable(RawTableSchema),
|
||||||
CreateIndex(TableName, ColumnName),
|
CreateIndex(TableName, ColumnName),
|
||||||
// DropTable(TableName),
|
// DropTable(TableName),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type InsertionValues = Vec<(ColumnName, Value)>;
|
pub type InsertionValues = Vec<(ColumnName, Value)>;
|
||||||
|
|
||||||
pub enum ColumnSelection {
|
pub enum ColumnSelection {
|
||||||
|
|
@ -34,3 +46,17 @@ pub enum Condition {
|
||||||
// Prefix(ColumnName, String),
|
// Prefix(ColumnName, String),
|
||||||
// Substring(ColumnName, String),
|
// Substring(ColumnName, String),
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
impl RawTableSchema {
|
||||||
|
pub fn number_of_columns(&self) -> usize {
|
||||||
|
self.columns.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_column(&self, column_name: &ColumnName) -> Option<ColumnSchema> {
|
||||||
|
self.columns.iter().find(|column_schema| column_name == &column_schema.column_name).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_columns(&self) -> Vec<&ColumnName> {
|
||||||
|
self.columns.iter().map(|ColumnSchema { column_name, .. }| column_name).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
use std::collections::HashSet;
|
use std::collections::{HashSet, BTreeMap};
|
||||||
use std::collections::HashMap;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::syntax;
|
use crate::syntax;
|
||||||
use crate::syntax::RawQuerySyntax;
|
use crate::syntax::{RawTableSchema, ColumnSchema, RawQuerySyntax};
|
||||||
use minisql::operation;
|
use minisql::operation;
|
||||||
use minisql::{operation::Operation, type_system::Value, schema::{TableSchema, ColumnName, TableName}, type_system::DbType, interpreter::{TablePosition, DbSchema}};
|
use minisql::{operation::Operation, type_system::Value, schema::{TableSchema, ColumnName, TableName}, type_system::DbType, interpreter::{TablePosition, DbSchema}};
|
||||||
|
|
||||||
|
|
@ -17,6 +16,12 @@ pub enum ValidationError {
|
||||||
ColumnsDoNotExist(Vec<ColumnName>),
|
ColumnsDoNotExist(Vec<ColumnName>),
|
||||||
#[error("duplicate column {0}")]
|
#[error("duplicate column {0}")]
|
||||||
DuplicateColumn(ColumnName),
|
DuplicateColumn(ColumnName),
|
||||||
|
#[error("primary key missing in table {0}")]
|
||||||
|
PrimaryKeyMissing(TableName),
|
||||||
|
#[error("multiple primary keys found in table {0}")]
|
||||||
|
MultiplePrimaryKeysFound(TableName),
|
||||||
|
#[error("attempt to index non-indexable column {1} in table {0}")]
|
||||||
|
AttemptToIndexNonIndexableColumn(TableName, ColumnName),
|
||||||
#[error("type mismatch at column `{column_name:?}` (expected {expected_type:?}, found {received_type:?})")]
|
#[error("type mismatch at column `{column_name:?}` (expected {expected_type:?}, found {received_type:?})")]
|
||||||
TypeMismatch {
|
TypeMismatch {
|
||||||
column_name: ColumnName,
|
column_name: ColumnName,
|
||||||
|
|
@ -28,8 +33,8 @@ pub enum ValidationError {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates and converts the raw syntax into a proper interpreter operation based on 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> {
|
pub fn validate_operation(syntax: RawQuerySyntax, db_schema: &DbSchema) -> Result<Operation, ValidationError> {
|
||||||
match query {
|
match syntax {
|
||||||
RawQuerySyntax::Select(table_name, column_selection, condition) => {
|
RawQuerySyntax::Select(table_name, column_selection, condition) => {
|
||||||
validate_select(table_name, column_selection, condition, db_schema)
|
validate_select(table_name, column_selection, condition, db_schema)
|
||||||
},
|
},
|
||||||
|
|
@ -39,8 +44,8 @@ pub fn validate_operation(query: RawQuerySyntax, db_schema: &DbSchema) -> Result
|
||||||
RawQuerySyntax::Delete(table_name, condition) => {
|
RawQuerySyntax::Delete(table_name, condition) => {
|
||||||
validate_delete(table_name, condition, db_schema)
|
validate_delete(table_name, condition, db_schema)
|
||||||
},
|
},
|
||||||
RawQuerySyntax::CreateTable(table_name, schema) => {
|
RawQuerySyntax::CreateTable(schema) => {
|
||||||
validate_create(table_name, schema, db_schema)
|
validate_create_table(schema, db_schema)
|
||||||
},
|
},
|
||||||
RawQuerySyntax::CreateIndex(table_name, column_name) => {
|
RawQuerySyntax::CreateIndex(table_name, column_name) => {
|
||||||
validate_create_index(table_name, column_name, db_schema)
|
validate_create_index(table_name, column_name, db_schema)
|
||||||
|
|
@ -54,31 +59,64 @@ fn validate_table_exists<'a>(db_schema: &DbSchema<'a>, table_name: &'a TableName
|
||||||
.map(|(_, table_position, table_schema)| (*table_position, *table_schema))
|
.map(|(_, table_position, table_schema)| (*table_position, *table_schema))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_create(table_name: TableName, table_schema: TableSchema, db_schema: &DbSchema) -> Result<Operation, ValidationError> {
|
fn validate_create_table(raw_table_schema: RawTableSchema, db_schema: &DbSchema) -> Result<Operation, ValidationError> {
|
||||||
|
let table_name: &TableName = &raw_table_schema.table_name;
|
||||||
if let Some(_) = get_table_schema(db_schema, &table_name) {
|
if let Some(_) = get_table_schema(db_schema, &table_name) {
|
||||||
return Err(ValidationError::TableAlreadyExists(table_name.to_string()));
|
return Err(ValidationError::TableAlreadyExists(table_name.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
find_first_duplicate(&table_schema.get_columns())
|
let table_schema: TableSchema = validate_table_schema(raw_table_schema)?;
|
||||||
|
Ok(Operation::CreateTable(table_schema))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_table_schema(raw_table_schema: RawTableSchema) -> Result<TableSchema, ValidationError> {
|
||||||
|
// check for duplicate columns
|
||||||
|
find_first_duplicate(&raw_table_schema.get_columns())
|
||||||
.map_or_else(
|
.map_or_else(
|
||||||
|| Ok(()),
|
|| Ok(()),
|
||||||
|duplicate_column| Err(ValidationError::DuplicateColumn(duplicate_column.to_string()))
|
|duplicate_column| Err(ValidationError::DuplicateColumn(duplicate_column.to_string()))
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// TODO: Ensure it has a primary key??
|
let mut primary_keys: Vec<(ColumnName, DbType)> = vec![];
|
||||||
Ok(Operation::CreateTable(table_name, table_schema))
|
let mut columns: Vec<ColumnName> = vec![];
|
||||||
|
let mut types: Vec<DbType> = vec![];
|
||||||
|
for ColumnSchema { column_name, type_, is_primary } in raw_table_schema.columns {
|
||||||
|
if is_primary {
|
||||||
|
primary_keys.push((column_name.clone(), type_))
|
||||||
|
}
|
||||||
|
columns.push(column_name);
|
||||||
|
types.push(type_);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_select(table_name: TableName, column_selection: syntax::ColumnSelection, condition: Option<syntax::Condition>, db_schema: &DbSchema) -> Result<Operation, ValidationError> {
|
// Ensure it has exactly one primary key that has correct type.
|
||||||
|
if primary_keys.len() == 0 {
|
||||||
|
return Err(ValidationError::PrimaryKeyMissing(raw_table_schema.table_name.clone()))
|
||||||
|
} else if primary_keys.len() > 1 {
|
||||||
|
return Err(ValidationError::MultiplePrimaryKeysFound(raw_table_schema.table_name.clone()))
|
||||||
|
} else {
|
||||||
|
let (primary_column_name, primary_key_type) = primary_keys[0].clone();
|
||||||
|
if primary_key_type == DbType::Uuid {
|
||||||
|
Ok(TableSchema::new(raw_table_schema.table_name, primary_column_name, columns, types))
|
||||||
|
} else {
|
||||||
|
Err(ValidationError::TypeMismatch {
|
||||||
|
column_name: raw_table_schema.table_name.clone(),
|
||||||
|
received_type: primary_key_type,
|
||||||
|
expected_type: DbType::Uuid,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)?;
|
let (table_position, schema) = validate_table_exists(db_schema, &table_name)?;
|
||||||
match column_selection {
|
match column_selection {
|
||||||
syntax::ColumnSelection::Columns(columns) => {
|
syntax::ColumnSelection::Columns(columns) => {
|
||||||
let non_existant_columns: Vec<ColumnName> =
|
let non_existant_columns: Vec<ColumnName> =
|
||||||
columns.iter().filter_map(|column|
|
columns.iter().filter_map(|column|
|
||||||
if schema.does_column_exist(&column) {
|
if schema.does_column_exist(&column) {
|
||||||
Some(column.clone())
|
|
||||||
} else {
|
|
||||||
None
|
None
|
||||||
|
} else {
|
||||||
|
Some(column.clone())
|
||||||
}).collect();
|
}).collect();
|
||||||
if non_existant_columns.len() > 0 {
|
if non_existant_columns.len() > 0 {
|
||||||
Err(ValidationError::ColumnsDoNotExist(non_existant_columns))
|
Err(ValidationError::ColumnsDoNotExist(non_existant_columns))
|
||||||
|
|
@ -96,7 +134,7 @@ pub fn validate_select(table_name: TableName, column_selection: syntax::ColumnSe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_insert(table_name: TableName, insertion_values: syntax::InsertionValues, db_schema: &DbSchema) -> Result<Operation, ValidationError> {
|
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)?;
|
let (table_position, schema) = validate_table_exists(db_schema, &table_name)?;
|
||||||
|
|
||||||
// Check for duplicate columns in insertion_values.
|
// Check for duplicate columns in insertion_values.
|
||||||
|
|
@ -120,7 +158,10 @@ pub fn validate_insert(table_name: TableName, insertion_values: syntax::Insertio
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check types and prepare for creation of InsertionValues for the interpreter
|
// Check types and prepare for creation of InsertionValues for the interpreter
|
||||||
let mut values_map: HashMap<_, Value> = HashMap::new();
|
let mut values_map: BTreeMap<_, Value> = BTreeMap::new(); // The reason for using BTreeMap
|
||||||
|
// instead of HashMap is that we need
|
||||||
|
// to get the values in a vector
|
||||||
|
// sorted by the key.
|
||||||
for (column_name, value) in insertion_values {
|
for (column_name, value) in insertion_values {
|
||||||
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 (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();
|
let value_type = value.to_type();
|
||||||
|
|
@ -130,13 +171,14 @@ pub fn validate_insert(table_name: TableName, insertion_values: syntax::Insertio
|
||||||
values_map.insert(column, value);
|
values_map.insert(column, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// These are values ordered by the column position
|
// WARNING: If you use `values_map: HashMap<_,_>`, this is not gonna sort values by key.
|
||||||
let values: operation::InsertionValues = values_map.into_values().collect();
|
let values: operation::InsertionValues = values_map.into_values().collect();
|
||||||
|
|
||||||
|
// Note that one of the values is id.
|
||||||
Ok(Operation::Insert(table_position, values))
|
Ok(Operation::Insert(table_position, values))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_delete(table_name: TableName, condition: Option<syntax::Condition>, db_schema: &DbSchema) -> Result<Operation, ValidationError> {
|
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 (table_position, schema) = validate_table_exists(db_schema, &table_name)?;
|
||||||
let validated_condition = validate_condition(condition, schema)?;
|
let validated_condition = validate_condition(condition, schema)?;
|
||||||
Ok(Operation::Delete(table_position, validated_condition))
|
Ok(Operation::Delete(table_position, validated_condition))
|
||||||
|
|
@ -162,13 +204,18 @@ fn validate_condition(condition: Option<syntax::Condition>, schema: &TableSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_create_index(table_name: TableName, column_name: ColumnName, db_schema: &DbSchema) -> Result<Operation, ValidationError> {
|
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)?;
|
let (table_position, schema) = validate_table_exists(db_schema, &table_name)?;
|
||||||
schema
|
schema
|
||||||
.get_column_position(&column_name)
|
.get_column(&column_name)
|
||||||
.map_or_else(
|
.map_or_else(
|
||||||
|| Err(ValidationError::ColumnsDoNotExist(vec![column_name.to_string()])),
|
|| Err(ValidationError::ColumnsDoNotExist(vec![column_name.to_string()])),
|
||||||
|column| Ok(Operation::CreateIndex(table_position, column))
|
|(column, type_)| {
|
||||||
|
if type_.is_indexable() {
|
||||||
|
Ok(Operation::CreateIndex(table_position, column))
|
||||||
|
} else {
|
||||||
|
Err(ValidationError::AttemptToIndexNonIndexableColumn(column_name.clone(), table_name))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,3 +238,359 @@ fn get_table_schema<'a>(db_schema: &DbSchema<'a>, table_name: &'a TableName) ->
|
||||||
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)
|
Some(table_schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::syntax::{RawTableSchema, ColumnSchema, RawQuerySyntax, ColumnSelection, Condition};
|
||||||
|
use minisql::type_system::{Value, IndexableValue};
|
||||||
|
use minisql::operation::Operation;
|
||||||
|
use minisql::operation;
|
||||||
|
use minisql::schema::TableSchema;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use RawQuerySyntax::*;
|
||||||
|
use Value::*;
|
||||||
|
use IndexableValue::*;
|
||||||
|
use Condition::*;
|
||||||
|
|
||||||
|
fn users_schema() -> TableSchema {
|
||||||
|
TableSchema::new(
|
||||||
|
"users".to_string(),
|
||||||
|
"id".to_string(),
|
||||||
|
vec!(
|
||||||
|
"id".to_string(),
|
||||||
|
"name".to_string(),
|
||||||
|
"age".to_string(),
|
||||||
|
),
|
||||||
|
vec![DbType::Uuid, DbType::String, DbType::Int],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_users_schema() -> RawTableSchema {
|
||||||
|
RawTableSchema {
|
||||||
|
table_name: "users".to_string(),
|
||||||
|
columns: vec![
|
||||||
|
ColumnSchema { column_name: "id".to_string(), type_: DbType::Uuid, is_primary: true },
|
||||||
|
ColumnSchema { column_name: "name".to_string(), type_: DbType::String, is_primary: false },
|
||||||
|
ColumnSchema { column_name: "age".to_string(), type_: DbType::Int, is_primary: false },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn db_schema(users_schema: &TableSchema) -> DbSchema {
|
||||||
|
vec![
|
||||||
|
("users".to_string(), 0, users_schema),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_db_schema() -> DbSchema<'static> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_basic() {
|
||||||
|
let db_schema: DbSchema = empty_db_schema();
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = CreateTable(raw_users_schema());
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Ok(Operation::CreateTable(_))));
|
||||||
|
|
||||||
|
let Ok(Operation::CreateTable(schema)) = result else { panic!() };
|
||||||
|
assert!(schema.table_name() == "users");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_duplicates_in_schema() {
|
||||||
|
let raw_users_schema = RawTableSchema {
|
||||||
|
table_name: "users".to_string(),
|
||||||
|
columns: vec![
|
||||||
|
ColumnSchema { column_name: "id".to_string(), type_: DbType::Uuid, is_primary: true },
|
||||||
|
ColumnSchema { column_name: "name".to_string(), type_: DbType::String, is_primary: false },
|
||||||
|
ColumnSchema { column_name: "name".to_string(), type_: DbType::Number, is_primary: false },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let db_schema: DbSchema = empty_db_schema();
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = CreateTable(raw_users_schema);
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
println!("{:?}", result);
|
||||||
|
assert!(matches!(result, Err(ValidationError::DuplicateColumn(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_primary_key_is_uuid() {
|
||||||
|
let raw_users_schema = RawTableSchema {
|
||||||
|
table_name: "users".to_string(),
|
||||||
|
columns: vec![
|
||||||
|
ColumnSchema { column_name: "id".to_string(), type_: DbType::Int, is_primary: true },
|
||||||
|
ColumnSchema { column_name: "name".to_string(), type_: DbType::String, is_primary: false },
|
||||||
|
ColumnSchema { column_name: "age".to_string(), type_: DbType::Int, is_primary: false },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let db_schema: DbSchema = empty_db_schema();
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = CreateTable(raw_users_schema);
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Err(ValidationError::TypeMismatch { .. })));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_multiple_primary_keys() {
|
||||||
|
let raw_users_schema = RawTableSchema {
|
||||||
|
table_name: "users".to_string(),
|
||||||
|
columns: vec![
|
||||||
|
ColumnSchema { column_name: "id".to_string(), type_: DbType::Int, is_primary: true },
|
||||||
|
ColumnSchema { column_name: "name".to_string(), type_: DbType::String, is_primary: true },
|
||||||
|
ColumnSchema { column_name: "age".to_string(), type_: DbType::Int, is_primary: false },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let db_schema: DbSchema = empty_db_schema();
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = CreateTable(raw_users_schema);
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Err(ValidationError::MultiplePrimaryKeysFound(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_already_exists() {
|
||||||
|
let users_schema: TableSchema = users_schema();
|
||||||
|
let db_schema: DbSchema = db_schema(&users_schema);
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = CreateTable(raw_users_schema());
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Err(ValidationError::TableAlreadyExists(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====Select====
|
||||||
|
#[test]
|
||||||
|
fn test_select_basic() {
|
||||||
|
let users_schema: TableSchema = users_schema();
|
||||||
|
let db_schema: DbSchema = db_schema(&users_schema);
|
||||||
|
let users_position = 0;
|
||||||
|
let id = 0;
|
||||||
|
let name = 1;
|
||||||
|
let age = 2;
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = Select("users".to_string(), ColumnSelection::All, None);
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Ok(Operation::Select(_, _, _))));
|
||||||
|
|
||||||
|
let Ok(Operation::Select(table_position, column_selection, condition)) = result else { panic!() };
|
||||||
|
|
||||||
|
assert!(table_position == users_position);
|
||||||
|
assert!(condition == None);
|
||||||
|
assert!(column_selection == vec![id, name, age]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_non_existent_table() {
|
||||||
|
let users_schema: TableSchema = users_schema();
|
||||||
|
let db_schema: DbSchema = db_schema(&users_schema);
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = Select("does_not_exist".to_string(), ColumnSelection::All, None);
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Err(ValidationError::TableDoesNotExist(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_eq() {
|
||||||
|
let users_schema: TableSchema = users_schema();
|
||||||
|
let db_schema: DbSchema = db_schema(&users_schema);
|
||||||
|
|
||||||
|
let users_position = 0;
|
||||||
|
let id = 0;
|
||||||
|
let name = 1;
|
||||||
|
let age = 2;
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = Select("users".to_string(), ColumnSelection::All, Some(Eq("age".to_string(), Indexable(Int(25)))));
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Ok(Operation::Select(_, _, _))));
|
||||||
|
|
||||||
|
let Ok(Operation::Select(table_position, column_selection, condition)) = result else { panic!() };
|
||||||
|
|
||||||
|
assert!(table_position == users_position);
|
||||||
|
assert!(column_selection == vec![id, name, age]);
|
||||||
|
|
||||||
|
assert!(condition == Some(operation::Condition::Eq(age, Indexable(Int(25)))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_eq_columns_selection() {
|
||||||
|
let users_schema: TableSchema = users_schema();
|
||||||
|
let db_schema: DbSchema = db_schema(&users_schema);
|
||||||
|
|
||||||
|
let users_position = 0;
|
||||||
|
let name = 1;
|
||||||
|
let age = 2;
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = Select("users".to_string(), ColumnSelection::Columns(vec!["age".to_string(), "name".to_string(), "age".to_string()]), None);
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Ok(Operation::Select(_, _, _))));
|
||||||
|
|
||||||
|
let Ok(Operation::Select(table_position, column_selection, condition)) = result else { panic!() };
|
||||||
|
|
||||||
|
assert!(table_position == users_position);
|
||||||
|
assert!(column_selection == vec![age, name, age]);
|
||||||
|
assert!(condition == None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_eq_columns_selection_nonexistent_column_selected() {
|
||||||
|
let users_schema: TableSchema = users_schema();
|
||||||
|
let db_schema: DbSchema = db_schema(&users_schema);
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = Select("users".to_string(), ColumnSelection::Columns(vec!["age".to_string(), "does_not_exist".to_string()]), None);
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Err(ValidationError::ColumnsDoNotExist(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_eq_non_existent_column() {
|
||||||
|
let users_schema: TableSchema = users_schema();
|
||||||
|
let db_schema: DbSchema = db_schema(&users_schema);
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = Select("users".to_string(), ColumnSelection::All, Some(Eq("does_not_exist".to_string(), Indexable(Int(25)))));
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Err(ValidationError::ColumnsDoNotExist(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_eq_type_error() {
|
||||||
|
let users_schema: TableSchema = users_schema();
|
||||||
|
let db_schema: DbSchema = db_schema(&users_schema);
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = Select("users".to_string(), ColumnSelection::All, Some(Eq("age".to_string(), Indexable(String("25".to_string())))));
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Err(ValidationError::TypeMismatch { .. })));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====Insert====
|
||||||
|
#[test]
|
||||||
|
fn test_insert() {
|
||||||
|
let users_schema: TableSchema = users_schema();
|
||||||
|
let db_schema: DbSchema = db_schema(&users_schema);
|
||||||
|
|
||||||
|
let users_position = 0;
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = Insert(
|
||||||
|
"users".to_string(),
|
||||||
|
vec![
|
||||||
|
("name".to_string(), Indexable(String("Alice".to_string()))),
|
||||||
|
("id".to_string(), Indexable(Uuid(0))),
|
||||||
|
("age".to_string(), Indexable(Int(25))),
|
||||||
|
]);
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Ok(Operation::Insert(_, _))));
|
||||||
|
|
||||||
|
let Ok(Operation::Insert(table_position, values)) = result else { panic!() };
|
||||||
|
|
||||||
|
assert!(table_position == users_position);
|
||||||
|
// Recall the order is
|
||||||
|
// let id = 0;
|
||||||
|
// let name = 1;
|
||||||
|
// let age = 2;
|
||||||
|
assert!(values == vec![Indexable(Uuid(0)), Indexable(String("Alice".to_string())), Indexable(Int(25))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_insert_non_existent_column() {
|
||||||
|
let users_schema: TableSchema = users_schema();
|
||||||
|
let db_schema: DbSchema = db_schema(&users_schema);
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = Insert(
|
||||||
|
"users".to_string(),
|
||||||
|
vec![
|
||||||
|
("name".to_string(), Indexable(String("Alice".to_string()))),
|
||||||
|
("id".to_string(), Indexable(Uuid(0))),
|
||||||
|
("age".to_string(), Indexable(Int(25))),
|
||||||
|
("does_not_exist".to_string(), Indexable(Int(25))),
|
||||||
|
]);
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Err(ValidationError::ColumnsDoNotExist(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_insert_ill_typed_column() {
|
||||||
|
let users_schema: TableSchema = users_schema();
|
||||||
|
let db_schema: DbSchema = db_schema(&users_schema);
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = Insert(
|
||||||
|
"users".to_string(),
|
||||||
|
vec![
|
||||||
|
("name".to_string(), Indexable(String("Alice".to_string()))),
|
||||||
|
("id".to_string(), Indexable(Uuid(0))),
|
||||||
|
("age".to_string(), Number(25.0)),
|
||||||
|
]);
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Err(ValidationError::TypeMismatch { .. })));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====Delete====
|
||||||
|
#[test]
|
||||||
|
fn test_delete_all() {
|
||||||
|
let users_schema: TableSchema = users_schema();
|
||||||
|
let db_schema: DbSchema = db_schema(&users_schema);
|
||||||
|
|
||||||
|
let users_position = 0;
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = Delete("users".to_string(), None);
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Ok(Operation::Delete(_, None))));
|
||||||
|
|
||||||
|
let Ok(Operation::Delete(table_position, _)) = result else { panic!() };
|
||||||
|
|
||||||
|
assert!(table_position == users_position);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_eq() {
|
||||||
|
let users_schema: TableSchema = users_schema();
|
||||||
|
let db_schema: DbSchema = db_schema(&users_schema);
|
||||||
|
|
||||||
|
let users_position = 0;
|
||||||
|
let age = 2;
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = Delete("users".to_string(), Some(Eq("age".to_string(), Indexable(Int(25)))));
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Ok(Operation::Delete(_, Some(operation::Condition::Eq(_, _))))));
|
||||||
|
|
||||||
|
let Ok(Operation::Delete(table_position, Some(operation::Condition::Eq(column, value)))) = result else { panic!() };
|
||||||
|
|
||||||
|
assert!(table_position == users_position);
|
||||||
|
assert!(column == age);
|
||||||
|
assert!(value == Indexable(Int(25)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====CreateIndex====
|
||||||
|
#[test]
|
||||||
|
fn test_create_index() {
|
||||||
|
let users_schema: TableSchema = users_schema();
|
||||||
|
let db_schema: DbSchema = db_schema(&users_schema);
|
||||||
|
|
||||||
|
let users_position = 0;
|
||||||
|
let age = 2;
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = CreateIndex("users".to_string(), "age".to_string());
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Ok(Operation::CreateIndex(_, _))));
|
||||||
|
|
||||||
|
let Ok(Operation::CreateIndex(table_position, column)) = result else { panic!() };
|
||||||
|
|
||||||
|
assert!(table_position == users_position);
|
||||||
|
assert!(column == age);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_index_nonexistent_column() {
|
||||||
|
let users_schema: TableSchema = users_schema();
|
||||||
|
let db_schema: DbSchema = db_schema(&users_schema);
|
||||||
|
|
||||||
|
let syntax: RawQuerySyntax = CreateIndex("users".to_string(), "does_not_exist".to_string());
|
||||||
|
let result = validate_operation(syntax, &db_schema);
|
||||||
|
assert!(matches!(result, Err(ValidationError::ColumnsDoNotExist(_))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ impl<W> ServerProto for W where W: BackendProtoWriter + Send {
|
||||||
|
|
||||||
async fn write_table_header(&mut self, table_schema: &TableSchema, row: &RestrictedRow) -> anyhow::Result<()> {
|
async fn write_table_header(&mut self, table_schema: &TableSchema, row: &RestrictedRow) -> anyhow::Result<()> {
|
||||||
let columns = row.iter()
|
let columns = row.iter()
|
||||||
.map(|(index, value)| value_to_column_description(table_schema, value, index))
|
.map(|(index, value)| value_to_column_description(table_schema, value, *index))
|
||||||
.collect::<anyhow::Result<Vec<ColumnDescription>>>()?;
|
.collect::<anyhow::Result<Vec<ColumnDescription>>>()?;
|
||||||
|
|
||||||
self.write_proto(RowDescriptionData { columns: columns.into() }.into()).await?;
|
self.write_proto(RowDescriptionData { columns: columns.into() }.into()).await?;
|
||||||
|
|
@ -84,11 +84,11 @@ impl<W> ServerProto for W where W: BackendProtoWriter + Send {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn value_to_column_description(schema: &TableSchema, value: &Value, index: &usize) -> anyhow::Result<ColumnDescription> {
|
fn value_to_column_description(schema: &TableSchema, value: &Value, index: usize) -> anyhow::Result<ColumnDescription> {
|
||||||
let name = schema.column_name_from_column_position(*index)?;
|
let name = schema.column_name_from_column(index);
|
||||||
|
|
||||||
let table_oid = schema.table_name().as_bytes().as_ptr() as i32;
|
let table_oid = schema.table_name().as_bytes().as_ptr() as i32;
|
||||||
let column_index = (*index).try_into()?;
|
let column_index = index.try_into()?;
|
||||||
let type_oid = value.type_oid();
|
let type_oid = value.type_oid();
|
||||||
let type_size = value.type_size();
|
let type_size = value.type_size();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue