sqlrite/sql/parser/create.rs
1use sqlparser::ast::{ColumnOption, CreateTable, DataType, ObjectName, ObjectNamePart, Statement};
2
3use crate::error::{Result, SQLRiteError};
4
5/// True when an `ObjectName` resolves to a single identifier `VECTOR`
6/// (case-insensitive). Phase 7a adds the `VECTOR(N)` column type as a
7/// sqlparser `DataType::Custom` — the engine recognizes it via this
8/// helper so the regular DataType match arm above stays uncluttered.
9fn is_vector_type(name: &ObjectName) -> bool {
10 name.0.len() == 1
11 && match &name.0[0] {
12 ObjectNamePart::Identifier(ident) => ident.value.eq_ignore_ascii_case("VECTOR"),
13 // Function-form ObjectNamePart shouldn't appear in a CREATE TABLE
14 // column type position. If it ever does, treat it as not-a-vector
15 // and the outer match falls through to the "Invalid" arm.
16 _ => false,
17 }
18}
19
20/// Parses the dimension out of the `Custom` args for `VECTOR(N)`.
21/// `args` is the `Vec<String>` sqlparser hands back for parenthesized
22/// type arguments — for `VECTOR(384)` that's `["384"]`. Validates that
23/// exactly one positive-integer argument was supplied.
24fn parse_vector_dim(args: &[String]) -> std::result::Result<usize, String> {
25 match args {
26 [] => Err("VECTOR requires a dimension, e.g. `VECTOR(384)`".to_string()),
27 [single] => {
28 let trimmed = single.trim();
29 match trimmed.parse::<usize>() {
30 Ok(d) if d > 0 => Ok(d),
31 Ok(_) => Err(format!("VECTOR dimension must be ≥ 1 (got `{trimmed}`)")),
32 Err(_) => Err(format!(
33 "VECTOR dimension must be a positive integer (got `{trimmed}`)"
34 )),
35 }
36 }
37 many => Err(format!(
38 "VECTOR takes exactly one dimension argument (got {})",
39 many.len()
40 )),
41 }
42}
43
44/// The schema for each SQL column in every table is represented by
45/// the following structure after parsed and tokenized
46#[derive(PartialEq, Debug)]
47pub struct ParsedColumn {
48 /// Name of the column
49 pub name: String,
50 /// Datatype of the column in String format
51 pub datatype: String,
52 /// Value representing if column is PRIMARY KEY
53 pub is_pk: bool,
54 /// Value representing if column was declared with the NOT NULL Constraint
55 pub not_null: bool,
56 /// Value representing if column was declared with the UNIQUE Constraint
57 pub is_unique: bool,
58}
59
60/// The following structure represents a CREATE TABLE query already parsed
61/// and broken down into name and a Vector of `ParsedColumn` metadata
62///
63#[derive(Debug)]
64pub struct CreateQuery {
65 /// name of table after parking and tokenizing of query
66 pub table_name: String,
67 /// Vector of `ParsedColumn` type with column metadata information
68 pub columns: Vec<ParsedColumn>,
69}
70
71impl CreateQuery {
72 pub fn new(statement: &Statement) -> Result<CreateQuery> {
73 match statement {
74 // Confirming the Statement is sqlparser::ast:Statement::CreateTable
75 Statement::CreateTable(CreateTable {
76 name,
77 columns,
78 constraints,
79 ..
80 }) => {
81 let table_name = name;
82 let mut parsed_columns: Vec<ParsedColumn> = vec![];
83
84 // Iterating over the columns returned form the Parser::parse:sql
85 // in the mod sql
86 for col in columns {
87 let name = col.name.to_string();
88
89 // Checks if columm already added to parsed_columns, if so, returns an error
90 if parsed_columns.iter().any(|col| col.name == name) {
91 return Err(SQLRiteError::Internal(format!(
92 "Duplicate column name: {}",
93 &name
94 )));
95 }
96
97 // Parsing each column for it data type
98 // For now only accepting basic data types
99 let datatype: String = match &col.data_type {
100 DataType::TinyInt(_)
101 | DataType::SmallInt(_)
102 | DataType::Int2(_)
103 | DataType::Int(_)
104 | DataType::Int4(_)
105 | DataType::Int8(_)
106 | DataType::Integer(_)
107 | DataType::BigInt(_) => "Integer".to_string(),
108 DataType::Boolean => "Bool".to_string(),
109 DataType::Text => "Text".to_string(),
110 DataType::Varchar(_bytes) => "Text".to_string(),
111 DataType::Real => "Real".to_string(),
112 DataType::Float(_precision) => "Real".to_string(),
113 DataType::Double(_) => "Real".to_string(),
114 DataType::Decimal(_) => "Real".to_string(),
115 // Phase 7e — `JSON` parses as a unit variant in
116 // sqlparser's DataType enum. JSONB is treated as
117 // an alias (matches PostgreSQL's permissive
118 // behaviour); both store as text under the hood.
119 DataType::JSON | DataType::JSONB => "Json".to_string(),
120 // Phase 7a — `VECTOR(N)` parses as Custom("VECTOR", ["N"]).
121 // sqlparser's SQLite dialect doesn't have a built-in
122 // Vector variant; Custom is what unrecognized type
123 // names + their parenthesized args fall through to.
124 DataType::Custom(name, args) if is_vector_type(name) => {
125 match parse_vector_dim(args) {
126 Ok(dim) => format!("vector({dim})"),
127 Err(e) => {
128 return Err(SQLRiteError::General(format!(
129 "Invalid VECTOR column '{}': {e}",
130 col.name
131 )));
132 }
133 }
134 }
135 other => {
136 eprintln!("not matched on custom type: {other:?}");
137 "Invalid".to_string()
138 }
139 };
140
141 // checking if column is PRIMARY KEY
142 let mut is_pk: bool = false;
143 // chekcing if column is UNIQUE
144 let mut is_unique: bool = false;
145 // chekcing if column is NULLABLE
146 let mut not_null: bool = false;
147 for column_option in &col.options {
148 match &column_option.option {
149 ColumnOption::PrimaryKey(_) => {
150 // For now, only Integer and Text types can be PRIMARY KEY and Unique
151 // Therefore Indexed.
152 if datatype != "Real" && datatype != "Bool" {
153 // Checks if table being created already has a PRIMARY KEY, if so, returns an error
154 if parsed_columns.iter().any(|col| col.is_pk) {
155 return Err(SQLRiteError::Internal(format!(
156 "Table '{}' has more than one primary key",
157 &table_name
158 )));
159 }
160 is_pk = true;
161 is_unique = true;
162 not_null = true;
163 }
164 }
165 ColumnOption::Unique(_) => {
166 // For now, only Integer and Text types can be UNIQUE
167 // Therefore Indexed.
168 if datatype != "Real" && datatype != "Bool" {
169 is_unique = true;
170 }
171 }
172 ColumnOption::NotNull => {
173 not_null = true;
174 }
175 _ => (),
176 };
177 }
178
179 parsed_columns.push(ParsedColumn {
180 name,
181 datatype: datatype.to_string(),
182 is_pk,
183 not_null,
184 is_unique,
185 });
186 }
187 // TODO: Handle constraints,
188 // Default value and others.
189 for constraint in constraints {
190 println!("{constraint:?}");
191 }
192 Ok(CreateQuery {
193 table_name: table_name.to_string(),
194 columns: parsed_columns,
195 })
196 }
197
198 _ => Err(SQLRiteError::Internal("Error parsing query".to_string())),
199 }
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use crate::sql::*;
207
208 #[test]
209 fn create_table_validate_tablename_test() {
210 let sql_input = String::from(
211 "CREATE TABLE contacts (
212 id INTEGER PRIMARY KEY,
213 first_name TEXT NOT NULL,
214 last_name TEXT NOT NULl,
215 email TEXT NOT NULL UNIQUE
216 );",
217 );
218 let expected_table_name = String::from("contacts");
219
220 let dialect = SQLiteDialect {};
221 let mut ast = Parser::parse_sql(&dialect, &sql_input).unwrap();
222
223 assert!(ast.len() == 1, "ast has more then one Statement");
224
225 let query = ast.pop().unwrap();
226
227 // Initialy only implementing some basic SQL Statements
228 if let Statement::CreateTable(_) = query {
229 let result = CreateQuery::new(&query);
230 match result {
231 Ok(payload) => {
232 assert_eq!(payload.table_name, expected_table_name);
233 }
234 Err(_) => panic!("an error occured during parsing CREATE TABLE Statement"),
235 }
236 }
237 }
238}