fix ups + rewrite parser
This commit is contained in:
parent
206ccb66ad
commit
41f6a1b4c3
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"rust-analyzer.linkedProjects": [
|
|
||||||
"./squirrel-server/Cargo.toml",
|
|
||||||
"./squirrel-client/Cargo.toml",
|
|
||||||
"./squirrel-server/Cargo.toml",
|
|
||||||
"./squirrel-server/Cargo.toml"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
use std::net::{TcpStream};
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
use std::str::from_utf8;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::TcpStream;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
match TcpStream::connect("localhost:5433") {
|
match TcpStream::connect("localhost:5433") {
|
||||||
|
@ -23,9 +22,12 @@ fn main() {
|
||||||
let response_size: usize = usize::from_le_bytes(response_size_buffer);
|
let response_size: usize = usize::from_le_bytes(response_size_buffer);
|
||||||
let mut response_buffer = vec![0 as u8; response_size];
|
let mut response_buffer = vec![0 as u8; response_size];
|
||||||
stream.read_exact(&mut response_buffer).unwrap();
|
stream.read_exact(&mut response_buffer).unwrap();
|
||||||
println!("{}", String::from_utf8(response_buffer).expect("a utf-8 string"));
|
println!(
|
||||||
|
"{}",
|
||||||
|
String::from_utf8(response_buffer).expect("a utf-8 string")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Failed to connect: {}", e);
|
println!("Failed to connect: {}", e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
{"rustc_fingerprint":15117991565403657335,"outputs":{"10376369925670944939":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/nickorlow/.rustup/toolchains/stable-x86_64-apple-darwin\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_feature=\"ssse3\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""},"4614504638168534921":{"success":true,"status":"","code":0,"stdout":"rustc 1.66.1 (90743e729 2023-01-10)\nbinary: rustc\ncommit-hash: 90743e7298aca107ddaa0c202a4d3604e29bfeb6\ncommit-date: 2023-01-10\nhost: x86_64-apple-darwin\nrelease: 1.66.1\nLLVM version: 15.0.2\n","stderr":""},"15697416045686424142":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n","stderr":""}},"successes":{}}
|
{"rustc_fingerprint":17949831956763062573,"outputs":{"15729799797837862367":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/nickorlow/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"4614504638168534921":{"success":true,"status":"","code":0,"stdout":"rustc 1.69.0 (84c898d65 2023-04-16)\nbinary: rustc\ncommit-hash: 84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc\ncommit-date: 2023-04-16\nhost: x86_64-unknown-linux-gnu\nrelease: 1.69.0\nLLVM version: 15.0.7\n","stderr":""}},"successes":{}}
|
Binary file not shown.
|
@ -1 +1 @@
|
||||||
/Users/nickorlow/programming/personal/SQUIRREL/squirrel-client/target/debug/squirrel-client: /Users/nickorlow/programming/personal/SQUIRREL/squirrel-client/src/main.rs
|
/home/nickorlow/programming/personal/squirrel/squirrel-client/target/debug/squirrel-client: /home/nickorlow/programming/personal/squirrel/squirrel-client/src/main.rs
|
||||||
|
|
80
squirrel-server/Cargo.lock
generated
80
squirrel-server/Cargo.lock
generated
|
@ -5,3 +5,83 @@ version = 3
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "SQUIRREL"
|
name = "SQUIRREL"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"fancy-regex",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.72"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bit-set"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
|
||||||
|
dependencies = [
|
||||||
|
"bit-vec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bit-vec"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fancy-regex"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2"
|
||||||
|
dependencies = [
|
||||||
|
"bit-set",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
|
||||||
|
|
|
@ -6,3 +6,6 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1.0.72"
|
||||||
|
fancy-regex = "0.11.0"
|
||||||
|
regex = "1.9.1"
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
id integer 0
|
|
||||||
name varchar 24
|
|
|
@ -1,42 +1,125 @@
|
||||||
use std::thread;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::net::{TcpListener, TcpStream, Shutdown};
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
use core::str::Split;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::io::{BufRead, BufReader, Read, Write};
|
||||||
|
use std::net::{Shutdown, TcpListener, TcpStream};
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
mod parser;
|
mod parser;
|
||||||
pub use parser::command::Command;
|
pub use parser::command::Command;
|
||||||
|
|
||||||
mod table;
|
mod table;
|
||||||
use parser::command::CreateCommand;
|
use parser::command::{CreateCommand, InsertCommand, SelectCommand};
|
||||||
pub use table::datatypes::Datatype;
|
pub use table::datatypes::Datatype;
|
||||||
pub use table::table::TableDefinition;
|
pub use table::table::{ColumnDefinition, TableDefinition};
|
||||||
|
|
||||||
|
use crate::parser::command::InsertItem;
|
||||||
|
|
||||||
const BUFFER_SIZE: usize = 500;
|
const BUFFER_SIZE: usize = 500;
|
||||||
|
|
||||||
|
fn handle_create(command: CreateCommand) -> ::anyhow::Result<TableDefinition> {
|
||||||
/*
|
let mut file = fs::File::create(format!(
|
||||||
CREATE TABLE [IF NOT EXISTS] table_name (
|
"./data/tabledefs/{}",
|
||||||
column1 datatype(length) column_contraint,
|
command.table_definition.name
|
||||||
column2 datatype(length) column_contraint,
|
))?;
|
||||||
column3 datatype(length) column_contraint,
|
|
||||||
table_constraints
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
fn handle_create(command: CreateCommand) -> Result<TableDefinition, String> {
|
|
||||||
println!("Creating table with name: {}", command.table_definition.name);
|
|
||||||
let mut file = fs::File::create(format!("./data/tabledefs/{}", command.table_definition.name)).unwrap();
|
|
||||||
|
|
||||||
for column in &command.table_definition.column_defs {
|
for column in &command.table_definition.column_defs {
|
||||||
println!("creating col: {} {} {}", column.name, column.data_type.as_str(), column.length);
|
let line = format!(
|
||||||
let line = format!("{} {} {} \n", column.name, column.data_type.as_str(), column.length);
|
"{} {} {} \n",
|
||||||
|
column.name,
|
||||||
|
column.data_type.as_str(),
|
||||||
|
column.length
|
||||||
|
);
|
||||||
file.write_all(line.as_bytes()).unwrap();
|
file.write_all(line.as_bytes()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(command.table_definition);
|
return Ok(command.table_definition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_tabledef(table_name: String) -> ::anyhow::Result<TableDefinition> {
|
||||||
|
let file = fs::File::open(format!("./data/tabledefs/{}", table_name))?;
|
||||||
|
|
||||||
|
let mut column_defs = vec![];
|
||||||
|
|
||||||
|
for line in BufReader::new(file).lines() {
|
||||||
|
let line_str = line?;
|
||||||
|
let parts: Vec<&str> = line_str.split(" ").collect();
|
||||||
|
let col_def = ColumnDefinition {
|
||||||
|
name: parts[0].to_string(),
|
||||||
|
data_type: Datatype::from_str(parts[1]).unwrap(),
|
||||||
|
length: parts[2].parse::<u16>()?.into(),
|
||||||
|
};
|
||||||
|
column_defs.push(col_def);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(TableDefinition {
|
||||||
|
name: table_name,
|
||||||
|
column_defs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_insert(command: InsertCommand) -> ::anyhow::Result<()> {
|
||||||
|
let mut file = fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.append(true)
|
||||||
|
.open(format!("./data/blobs/{}", command.table_name))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let tabledef = read_tabledef(command.table_name).unwrap();
|
||||||
|
|
||||||
|
for col_def in &tabledef.column_defs {
|
||||||
|
if let Some(insert_item) = command.items.get(&col_def.name) {
|
||||||
|
let bytes = col_def
|
||||||
|
.data_type
|
||||||
|
.to_bytes(insert_item.column_value.clone())?;
|
||||||
|
file.write_all(&bytes)?;
|
||||||
|
if bytes.len() < col_def.length {
|
||||||
|
let length = col_def.length - bytes.len();
|
||||||
|
let empty_bytes = vec![0; length];
|
||||||
|
file.write_all(&empty_bytes)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"ERROR: INSERT statement is missing data for column '{}'",
|
||||||
|
col_def.name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_select(command: SelectCommand) -> ::anyhow::Result<String> {
|
||||||
|
let mut file = fs::File::open(format!("./data/blobs/{}", command.table_name))?;
|
||||||
|
let tabledef = read_tabledef(command.table_name).unwrap();
|
||||||
|
let mut response = String::new();
|
||||||
|
|
||||||
|
response += "| ";
|
||||||
|
for col_def in &tabledef.column_defs {
|
||||||
|
response += format!("{} | ", col_def.name).as_str();
|
||||||
|
}
|
||||||
|
response += "\n";
|
||||||
|
response += "-----------\n";
|
||||||
|
let mut buf: Vec<u8> = vec![0; tabledef.get_byte_size()];
|
||||||
|
while file.read_exact(buf.as_mut_slice()).is_ok() {
|
||||||
|
response += "| ";
|
||||||
|
let mut idx = 0;
|
||||||
|
for col_def in &tabledef.column_defs {
|
||||||
|
let len = if col_def.length > 0 {
|
||||||
|
col_def.length
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
|
let str_val = col_def.data_type.from_bytes(&buf[idx..(idx + len)])?;
|
||||||
|
response += format!("{} | ", str_val).as_str();
|
||||||
|
idx += len;
|
||||||
|
}
|
||||||
|
response += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
fn run_command(query: String) -> String {
|
fn run_command(query: String) -> String {
|
||||||
let response: String;
|
let response: String;
|
||||||
if query.chars().nth(0).unwrap() == '\\' {
|
if query.chars().nth(0).unwrap() == '\\' {
|
||||||
|
@ -44,7 +127,7 @@ fn run_command(query: String) -> String {
|
||||||
return String::from("Slash commands are not yet supported in SQUIRREL");
|
return String::from("Slash commands are not yet supported in SQUIRREL");
|
||||||
}
|
}
|
||||||
|
|
||||||
let command_result: Result<Command, String> = Command::from_string(query);
|
let command_result: ::anyhow::Result<Command> = Command::from_string(query);
|
||||||
|
|
||||||
if command_result.is_ok() {
|
if command_result.is_ok() {
|
||||||
let command: Command = command_result.unwrap();
|
let command: Command = command_result.unwrap();
|
||||||
|
@ -57,10 +140,21 @@ fn run_command(query: String) -> String {
|
||||||
String::from("Table created.")
|
String::from("Table created.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => { String::from("Invalid command") }
|
Command::Insert(insert_command) => {
|
||||||
|
let result = handle_insert(insert_command);
|
||||||
|
if result.is_err() {
|
||||||
|
String::from(result.err().unwrap().to_string())
|
||||||
|
} else {
|
||||||
|
String::from("Data inserted.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::Select(select_command) => {
|
||||||
|
return handle_select(select_command).unwrap();
|
||||||
|
}
|
||||||
|
_ => String::from("Invalid command"),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
response = command_result.err().unwrap();
|
response = command_result.err().unwrap().to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
@ -71,33 +165,19 @@ fn handle_client(mut stream: TcpStream) {
|
||||||
|
|
||||||
while match stream.read(&mut data) {
|
while match stream.read(&mut data) {
|
||||||
Ok(size) => {
|
Ok(size) => {
|
||||||
let mut query_string = String::from_utf8(data.to_vec()).expect("A UTF-8 string");
|
let query_string = String::from_utf8(data.to_vec()).expect("A UTF-8 string");
|
||||||
println!("Received: {}", query_string);
|
let response: String = run_command(query_string);
|
||||||
|
|
||||||
let mut i = 0;
|
|
||||||
for c in query_string.chars() {
|
|
||||||
if c == ';' {
|
|
||||||
query_string = query_string.get(0..i).unwrap().to_string();
|
|
||||||
i = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: String;
|
|
||||||
if i == 0 {
|
|
||||||
response = run_command(query_string);
|
|
||||||
} else {
|
|
||||||
response = String::from("No semicolon.");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut response_data_size = response.len().to_le_bytes();
|
let mut response_data_size = response.len().to_le_bytes();
|
||||||
stream.write(&mut response_data_size).unwrap(); // send length of message
|
stream.write(&mut response_data_size).unwrap(); // send length of message
|
||||||
stream.write(response.as_bytes()).unwrap(); // send message
|
stream.write(response.as_bytes()).unwrap(); // send message
|
||||||
true
|
true
|
||||||
},
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
println!("An error occurred, terminating connection with {}", stream.peer_addr().unwrap());
|
println!(
|
||||||
|
"An error occurred, terminating connection with {}",
|
||||||
|
stream.peer_addr().unwrap()
|
||||||
|
);
|
||||||
stream.shutdown(Shutdown::Both).unwrap();
|
stream.shutdown(Shutdown::Both).unwrap();
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -105,10 +185,10 @@ fn handle_client(mut stream: TcpStream) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
fs::remove_dir_all("./data")?;
|
//fs::remove_dir_all("./data")?;
|
||||||
fs::create_dir("./data")?;
|
let _ensure_data_exists = fs::create_dir("./data");
|
||||||
fs::create_dir("./data/tabledefs")?;
|
let _ensure_tabledefs_exists = fs::create_dir("./data/tabledefs");
|
||||||
fs::create_dir("./data/blobs")?;
|
let _ensure_blob_exists = fs::create_dir("./data/blobs");
|
||||||
let listener = TcpListener::bind("0.0.0.0:5433")?;
|
let listener = TcpListener::bind("0.0.0.0:5433")?;
|
||||||
|
|
||||||
for stream in listener.incoming() {
|
for stream in listener.incoming() {
|
||||||
|
@ -120,3 +200,80 @@ fn main() -> std::io::Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_statement() -> anyhow::Result<()> {
|
||||||
|
let empty_statement = "";
|
||||||
|
let regular_statement = "INSERT INTO users (id, name) VALUES (1, \"Test\");";
|
||||||
|
let extra_ws_statement =
|
||||||
|
"INSERT INTO users (id, name) VALUES (1, \"Test\") ;";
|
||||||
|
let min_ws_statement = "INSERT INTO users(id, name) VALUES(1, \"Test\");";
|
||||||
|
let str_comma_statement = "INSERT INTO users(id, name) VALUES(1, \"Firstname, Lastname\");";
|
||||||
|
|
||||||
|
let expected_output = Command::Insert(InsertCommand {
|
||||||
|
table_name: "users".to_string(),
|
||||||
|
items: HashMap::from([
|
||||||
|
(
|
||||||
|
"id".to_string(),
|
||||||
|
InsertItem {
|
||||||
|
column_name: "id".to_string(),
|
||||||
|
column_value: "1".to_string(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"name".to_string(),
|
||||||
|
InsertItem {
|
||||||
|
column_name: "name".to_string(),
|
||||||
|
column_value: "\"Test\"".to_string(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
let expected_output_comma = Command::Insert(InsertCommand {
|
||||||
|
table_name: "users".to_string(),
|
||||||
|
items: HashMap::from([
|
||||||
|
(
|
||||||
|
"id".to_string(),
|
||||||
|
InsertItem {
|
||||||
|
column_name: "id".to_string(),
|
||||||
|
column_value: "1".to_string(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"name".to_string(),
|
||||||
|
InsertItem {
|
||||||
|
column_name: "name".to_string(),
|
||||||
|
column_value: "\"Firstname, Lastname\"".to_string(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Command::from_string(String::from(empty_statement)).is_ok(),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Command::from_string(String::from(regular_statement))?,
|
||||||
|
expected_output
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Command::from_string(String::from(extra_ws_statement))?,
|
||||||
|
expected_output
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Command::from_string(String::from(min_ws_statement))?,
|
||||||
|
expected_output
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Command::from_string(String::from(str_comma_statement))?,
|
||||||
|
expected_output_comma
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -1,65 +1,349 @@
|
||||||
use crate::{TableDefinition, Datatype};
|
use std::collections::{HashMap, HashSet};
|
||||||
use crate::table::table::Column;
|
|
||||||
|
|
||||||
|
use crate::table::table::ColumnDefinition;
|
||||||
|
use crate::{Datatype, TableDefinition};
|
||||||
|
use anyhow::anyhow;
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
Select,
|
Select(SelectCommand),
|
||||||
Create(CreateCommand),
|
Create(CreateCommand),
|
||||||
Insert,
|
Insert(InsertCommand),
|
||||||
Delete
|
Delete,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub struct CreateCommand {
|
pub struct CreateCommand {
|
||||||
pub table_definition: TableDefinition,
|
pub table_definition: TableDefinition,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub fn from_string(command_str: String) -> Result<Command, String> {
|
pub struct InsertCommand {
|
||||||
let mut parts = command_str.split(' ');
|
pub table_name: String,
|
||||||
|
pub items: HashMap<String, InsertItem>,
|
||||||
|
}
|
||||||
|
|
||||||
match parts.nth(0).unwrap() {
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
"CREATE" => {
|
pub struct SelectCommand {
|
||||||
let object = String::from(parts.nth(0).unwrap());
|
pub table_name: String,
|
||||||
if object.eq_ignore_ascii_case("TABLE") {
|
// TODO Later: pub column_names: Vec<String>,
|
||||||
let mut column_definitions: Vec<Column> = vec![];
|
}
|
||||||
|
|
||||||
let column_def_begin_idx = command_str.chars().position(|c| c == '(').unwrap() + 1;
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
let column_def_end_idx = command_str.chars().position(|c| c == ')').unwrap();
|
pub struct InsertItem {
|
||||||
let coldef_str = command_str.get(column_def_begin_idx..column_def_end_idx).unwrap().to_string();
|
pub column_name: String,
|
||||||
let col_strs = coldef_str.split(',');
|
pub column_value: String,
|
||||||
|
}
|
||||||
|
|
||||||
for col_str in col_strs {
|
enum CreateParserState {
|
||||||
println!("{}", col_str);
|
FindObject,
|
||||||
let mut parts = col_str.split_ascii_whitespace();
|
FindTableName,
|
||||||
let mut col: Column = Column {
|
FindColumnName,
|
||||||
length: 0,
|
FindColumnDefinitions,
|
||||||
name: parts.nth(0).unwrap().to_string(),
|
FindColumnDatatype,
|
||||||
data_type: Datatype::from_str(parts.nth(0).unwrap()).unwrap()
|
FindColumnDefinitionEnd,
|
||||||
};
|
FindColumnLength,
|
||||||
let len = parts.nth(0);
|
FindSemicolon,
|
||||||
if len.is_some() {
|
}
|
||||||
if col.data_type.has_len() {
|
|
||||||
col.length = len.unwrap().parse().unwrap();
|
enum SelectParserState {
|
||||||
|
FindWildcard, // Temporary, col selection coming soon
|
||||||
|
FindFrom,
|
||||||
|
FindTableName,
|
||||||
|
FindSemicolon,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum InsertParserState {
|
||||||
|
FindIntoKeyword,
|
||||||
|
FindTableName,
|
||||||
|
FindColumnListBegin,
|
||||||
|
FindColumnName,
|
||||||
|
FindColumnNameEnd,
|
||||||
|
FindValuesKeyword,
|
||||||
|
FindValuesListBegin,
|
||||||
|
FindValue,
|
||||||
|
FindValueEnd,
|
||||||
|
FindSemicolon,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tokenizer(text: String) -> Vec<String> {
|
||||||
|
let parts = HashSet::from([' ', ',', ';', '(', ')']);
|
||||||
|
let mut tokens: Vec<String> = vec![];
|
||||||
|
let mut cur_str = String::new();
|
||||||
|
let mut in_quotes = false;
|
||||||
|
|
||||||
|
for cur_char in text.chars() {
|
||||||
|
if cur_char == '\"' {
|
||||||
|
in_quotes = !in_quotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !in_quotes && parts.contains(&cur_char) {
|
||||||
|
if cur_str.len() != 0 {
|
||||||
|
tokens.push(cur_str);
|
||||||
|
cur_str = String::new();
|
||||||
|
}
|
||||||
|
if cur_char != ' ' {
|
||||||
|
tokens.push(cur_char.to_string());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(format!("ERROR: Datatype '{}' does not accept a length parameter", col.data_type.as_str()));
|
cur_str.push(cur_char);
|
||||||
}
|
}
|
||||||
} else if col.data_type.has_len() {
|
|
||||||
return Err(format!("ERROR: Datatype '{}' requires a length parameter", col.data_type.as_str()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
column_definitions.push(col);
|
return tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
fn parse_insert_command(tokens: &mut Vec<String>) -> ::anyhow::Result<Command> {
|
||||||
|
let mut state: InsertParserState = InsertParserState::FindIntoKeyword;
|
||||||
|
|
||||||
|
let mut table_name = String::new();
|
||||||
|
let mut column_name = String::new();
|
||||||
|
let mut column_val = String::new();
|
||||||
|
|
||||||
|
let mut column_list: Vec<String> = vec![];
|
||||||
|
let mut value_list: Vec<String> = vec![];
|
||||||
|
|
||||||
|
while let Some(token) = &tokens.pop() {
|
||||||
|
match state {
|
||||||
|
InsertParserState::FindIntoKeyword => {
|
||||||
|
if !token.eq_ignore_ascii_case("INTO") {
|
||||||
|
return Err(anyhow!("Expected to find INTO at or near '{}'", token));
|
||||||
|
} else {
|
||||||
|
state = InsertParserState::FindTableName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InsertParserState::FindTableName => {
|
||||||
|
table_name = token.to_string();
|
||||||
|
state = InsertParserState::FindColumnListBegin;
|
||||||
|
}
|
||||||
|
InsertParserState::FindColumnListBegin => {
|
||||||
|
if token != "(" {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Unexpected token at or near '{}'. Expected start of column list",
|
||||||
|
token
|
||||||
|
));
|
||||||
|
}
|
||||||
|
state = InsertParserState::FindColumnName;
|
||||||
|
}
|
||||||
|
InsertParserState::FindColumnName => {
|
||||||
|
column_name = token.to_string();
|
||||||
|
state = InsertParserState::FindColumnNameEnd;
|
||||||
|
}
|
||||||
|
InsertParserState::FindColumnNameEnd => {
|
||||||
|
if token == "," {
|
||||||
|
state = InsertParserState::FindColumnName;
|
||||||
|
} else if token == ")" {
|
||||||
|
state = InsertParserState::FindValuesKeyword;
|
||||||
|
} else {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Unexpected token at or near '{}'. Expected comma or rparen.",
|
||||||
|
token
|
||||||
|
));
|
||||||
|
}
|
||||||
|
column_list.push(column_name.clone());
|
||||||
|
}
|
||||||
|
InsertParserState::FindValuesKeyword => {
|
||||||
|
if token != "VALUES" {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Unexpected token at or near '{}'. Expected 'VALUES'.",
|
||||||
|
token
|
||||||
|
));
|
||||||
|
}
|
||||||
|
state = InsertParserState::FindValuesListBegin;
|
||||||
|
}
|
||||||
|
InsertParserState::FindValuesListBegin => {
|
||||||
|
if token != "(" {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Unexpected token at or near '{}'. Expected start of values list",
|
||||||
|
token
|
||||||
|
));
|
||||||
|
}
|
||||||
|
state = InsertParserState::FindValue;
|
||||||
|
}
|
||||||
|
InsertParserState::FindValue => {
|
||||||
|
column_val = token.to_string();
|
||||||
|
state = InsertParserState::FindValueEnd;
|
||||||
|
}
|
||||||
|
InsertParserState::FindValueEnd => {
|
||||||
|
if token == "," {
|
||||||
|
state = InsertParserState::FindValue;
|
||||||
|
} else if token == ")" {
|
||||||
|
state = InsertParserState::FindSemicolon;
|
||||||
|
} else {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Unexpected token at or near '{}'. Expected comma or rparen.",
|
||||||
|
token
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
value_list.push(column_val.clone());
|
||||||
|
}
|
||||||
|
InsertParserState::FindSemicolon => {
|
||||||
|
if token != ";" {
|
||||||
|
return Err(anyhow!("Expected semicolon at or near '{}'", token));
|
||||||
|
} else {
|
||||||
|
let mut insert_item_list: HashMap<String, InsertItem> = HashMap::new();
|
||||||
|
for item in column_list.iter().zip(&mut value_list.iter_mut()) {
|
||||||
|
let (col_name, value) = item;
|
||||||
|
|
||||||
|
insert_item_list.insert(
|
||||||
|
col_name.clone().trim().to_string(),
|
||||||
|
InsertItem {
|
||||||
|
column_name: col_name.trim().to_string(),
|
||||||
|
column_value: value.trim().to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(Command::Insert(InsertCommand {
|
||||||
|
table_name,
|
||||||
|
items: insert_item_list,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(anyhow!("Unexpected end of input"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_select_command(tokens: &mut Vec<String>) -> ::anyhow::Result<Command> {
|
||||||
|
let mut state: SelectParserState = SelectParserState::FindWildcard;
|
||||||
|
|
||||||
|
// intermediate tmp vars
|
||||||
|
let mut table_name = String::new();
|
||||||
|
|
||||||
|
while let Some(token) = &tokens.pop() {
|
||||||
|
match state {
|
||||||
|
SelectParserState::FindWildcard => {
|
||||||
|
if token != "*" {
|
||||||
|
return Err(anyhow!("Expected to find selection at or near '{}' (SQUIRREL does not support column seletion)", token));
|
||||||
|
} else {
|
||||||
|
state = SelectParserState::FindFrom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SelectParserState::FindFrom => {
|
||||||
|
if !token.eq_ignore_ascii_case("FROM") {
|
||||||
|
return Err(anyhow!("Expected to find FROM at or near '{}'", token));
|
||||||
|
} else {
|
||||||
|
state = SelectParserState::FindTableName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SelectParserState::FindTableName => {
|
||||||
|
table_name = token.to_string();
|
||||||
|
state = SelectParserState::FindSemicolon;
|
||||||
|
}
|
||||||
|
SelectParserState::FindSemicolon => {
|
||||||
|
if token != ";" {
|
||||||
|
return Err(anyhow!("Expected semicolon at or near '{}'", token));
|
||||||
|
} else {
|
||||||
|
return Ok(Command::Select(SelectCommand { table_name }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(anyhow!("Unexpected end of input"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_create_command(tokens: &mut Vec<String>) -> ::anyhow::Result<Command> {
|
||||||
|
let mut state: CreateParserState = CreateParserState::FindObject;
|
||||||
|
let mut col_defs: Vec<ColumnDefinition> = vec![];
|
||||||
|
|
||||||
|
// intermediate tmp vars
|
||||||
|
let mut table_name = String::new();
|
||||||
|
let mut data_type: Option<Datatype> = None;
|
||||||
|
let mut length = 0;
|
||||||
|
let mut col_name = String::new();
|
||||||
|
|
||||||
|
while let Some(token) = &tokens.pop() {
|
||||||
|
match state {
|
||||||
|
CreateParserState::FindObject => match token.to_uppercase().as_str() {
|
||||||
|
"TABLE" => {
|
||||||
|
state = CreateParserState::FindTableName;
|
||||||
|
}
|
||||||
|
_ => return Err(anyhow!("Can't create object of type '{}'", token.as_str())),
|
||||||
|
},
|
||||||
|
CreateParserState::FindTableName => {
|
||||||
|
state = CreateParserState::FindColumnDefinitions;
|
||||||
|
table_name = token.to_string();
|
||||||
|
}
|
||||||
|
CreateParserState::FindColumnDefinitions => {
|
||||||
|
if token != "(" {
|
||||||
|
return Err(anyhow!("Could not find column list"));
|
||||||
|
} else {
|
||||||
|
state = CreateParserState::FindColumnName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CreateParserState::FindColumnName => {
|
||||||
|
col_name = token.to_string();
|
||||||
|
state = CreateParserState::FindColumnDatatype;
|
||||||
|
}
|
||||||
|
CreateParserState::FindColumnDatatype => {
|
||||||
|
let dtype = Datatype::from_str(&token).unwrap();
|
||||||
|
if dtype.has_len() {
|
||||||
|
state = CreateParserState::FindColumnLength;
|
||||||
|
} else {
|
||||||
|
state = CreateParserState::FindColumnDefinitionEnd;
|
||||||
|
}
|
||||||
|
data_type = Some(dtype);
|
||||||
|
}
|
||||||
|
CreateParserState::FindColumnLength => {
|
||||||
|
length = token.parse()?;
|
||||||
|
state = CreateParserState::FindColumnDefinitionEnd;
|
||||||
|
}
|
||||||
|
CreateParserState::FindColumnDefinitionEnd => {
|
||||||
|
let column_def = ColumnDefinition {
|
||||||
|
data_type: data_type.unwrap(),
|
||||||
|
length,
|
||||||
|
name: col_name,
|
||||||
|
};
|
||||||
|
|
||||||
|
length = 0;
|
||||||
|
col_name = String::new();
|
||||||
|
data_type = None;
|
||||||
|
|
||||||
|
col_defs.push(column_def);
|
||||||
|
|
||||||
|
match token.as_str() {
|
||||||
|
"," => {
|
||||||
|
state = CreateParserState::FindColumnName;
|
||||||
|
}
|
||||||
|
")" => {
|
||||||
|
state = CreateParserState::FindSemicolon;
|
||||||
|
}
|
||||||
|
_ => return Err(anyhow!("Expected end")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CreateParserState::FindSemicolon => {
|
||||||
|
if token != ";" {
|
||||||
|
return Err(anyhow!("Expected semicolon at or near '{}'", token));
|
||||||
|
} else {
|
||||||
return Ok(Command::Create(CreateCommand {
|
return Ok(Command::Create(CreateCommand {
|
||||||
table_definition: TableDefinition {
|
table_definition: TableDefinition {
|
||||||
name: String::from(parts.nth(0).unwrap()),
|
name: table_name,
|
||||||
column_defs: column_definitions
|
column_defs: col_defs,
|
||||||
}
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
return Err(format!("ERROR: syntax error at or near '{}'", object));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
_ => { Err(String::from("Unable to parse command")) }
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(anyhow!("Unexpected end of input"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_string(command_str: String) -> ::anyhow::Result<Command> {
|
||||||
|
let mut tokens: Vec<String> = tokenizer(command_str);
|
||||||
|
tokens.reverse();
|
||||||
|
if let Some(token) = tokens.pop() {
|
||||||
|
return match token.to_uppercase().as_str() {
|
||||||
|
"CREATE" => Self::parse_create_command(&mut tokens),
|
||||||
|
"INSERT" => Self::parse_insert_command(&mut tokens),
|
||||||
|
"SELECT" => Self::parse_select_command(&mut tokens),
|
||||||
|
_ => Err(anyhow!("Unknown command '{}'", token)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return Err(anyhow!("Unexpected end of statement"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub enum Datatype {
|
pub enum Datatype {
|
||||||
Integer,
|
Integer,
|
||||||
CharacterVarying,
|
CharacterVarying,
|
||||||
|
@ -7,25 +8,60 @@ impl Datatype {
|
||||||
pub fn as_str(&self) -> &'static str {
|
pub fn as_str(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Datatype::CharacterVarying => "varchar",
|
Datatype::CharacterVarying => "varchar",
|
||||||
Datatype::Integer => "integer"
|
Datatype::Integer => "integer",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_len(&self) -> bool {
|
pub fn has_len(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Datatype::CharacterVarying => true,
|
Datatype::CharacterVarying => true,
|
||||||
Datatype::Integer => false
|
Datatype::Integer => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_bytes(&self, data_val: String) -> ::anyhow::Result<Vec<u8>> {
|
||||||
|
match self {
|
||||||
|
Datatype::CharacterVarying => {
|
||||||
|
// Ensure string is formatted properly
|
||||||
|
if !data_val.starts_with('\"') || !data_val.ends_with('\"') {
|
||||||
|
return Err(::anyhow::anyhow!(
|
||||||
|
"ERROR: Unable to parse value for type CharacterVarying"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut str_bytes = data_val.as_bytes().to_vec();
|
||||||
|
|
||||||
|
// Remove dquotes
|
||||||
|
str_bytes.remove(0);
|
||||||
|
str_bytes.remove(str_bytes.len() - 1);
|
||||||
|
return Ok(str_bytes);
|
||||||
|
}
|
||||||
|
Datatype::Integer => {
|
||||||
|
let val = data_val.parse::<u8>()?;
|
||||||
|
return Ok(vec![val]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bytes(&self, data_val: &[u8]) -> ::anyhow::Result<String> {
|
||||||
|
match self {
|
||||||
|
Datatype::CharacterVarying => {
|
||||||
|
let str_val = String::from_utf8(data_val.to_vec())?;
|
||||||
|
return Ok(str_val);
|
||||||
|
}
|
||||||
|
Datatype::Integer => {
|
||||||
|
let val = data_val.first().unwrap();
|
||||||
|
return Ok(format!("{}", val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn from_str(string: &str) -> Result<Datatype, String> {
|
pub fn from_str(string: &str) -> Result<Datatype, String> {
|
||||||
match string {
|
match string {
|
||||||
"varchar" => return Ok(Datatype::CharacterVarying),
|
"varchar" => return Ok(Datatype::CharacterVarying),
|
||||||
"character varying" => return Ok(Datatype::CharacterVarying),
|
"character varying" => return Ok(Datatype::CharacterVarying),
|
||||||
"integer" => return Ok(Datatype::Integer),
|
"integer" => return Ok(Datatype::Integer),
|
||||||
"int" => return Ok(Datatype::Integer),
|
"int" => return Ok(Datatype::Integer),
|
||||||
"int4" => return Ok(Datatype::Integer),
|
"int8" => return Ok(Datatype::Integer),
|
||||||
_ => {return Err(String::from("Undefined data type"))}
|
_ => return Err(String::from("Undefined data type")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,12 +1,31 @@
|
||||||
use crate::Datatype;
|
use crate::Datatype;
|
||||||
|
|
||||||
pub struct Column {
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub struct ColumnDefinition {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub data_type: Datatype,
|
pub data_type: Datatype,
|
||||||
pub length: u16 // used for char(n), varchar(n)
|
pub length: usize, // used for char(n), varchar(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub struct TableDefinition {
|
pub struct TableDefinition {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub column_defs: Vec<Column>,
|
pub column_defs: Vec<ColumnDefinition>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableDefinition {
|
||||||
|
pub fn get_byte_size(&self) -> usize {
|
||||||
|
let mut sum: usize = 0;
|
||||||
|
for col_def in self.column_defs.iter() {
|
||||||
|
// TODO HACK FIXME
|
||||||
|
// We should keep track of length
|
||||||
|
// even for built-in datatypes.
|
||||||
|
sum += if col_def.length > 0 {
|
||||||
|
col_def.length
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue