diff --git a/crates/iridium_core/src/ast/expressions.rs b/crates/iridium_core/src/ast/expressions.rs index 028e1fd..e65de15 100644 --- a/crates/iridium_core/src/ast/expressions.rs +++ b/crates/iridium_core/src/ast/expressions.rs @@ -67,6 +67,7 @@ pub enum Expr { Like { expr: Box, pattern: Box, + escape: Option>, negated: bool, }, WindowFunction { diff --git a/crates/iridium_core/src/ast/statements/ddl.rs b/crates/iridium_core/src/ast/statements/ddl.rs index c5135f2..3a0ae83 100644 --- a/crates/iridium_core/src/ast/statements/ddl.rs +++ b/crates/iridium_core/src/ast/statements/ddl.rs @@ -47,7 +47,21 @@ pub struct DropTableStmt { pub struct CreateIndexStmt { pub name: ObjectName, pub table: ObjectName, - pub columns: Vec, + pub is_unique: bool, + pub is_clustered: bool, + pub columns: Vec, + pub options: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IndexColumnSpec { + pub name: String, + pub is_desc: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum IndexOptionSpec { + FillFactor(u8), } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -129,6 +143,8 @@ pub struct ColumnSpec { pub check_constraint_name: Option, pub computed_expr: Option, pub foreign_key: Option, + pub collation: Option, + pub is_clustered: bool, pub ansi_padding_on: bool, } @@ -169,10 +185,12 @@ pub enum TableConstraintSpec { }, PrimaryKey { name: String, - columns: Vec, + columns: Vec, + is_clustered: bool, }, Unique { name: String, - columns: Vec, + columns: Vec, + is_clustered: bool, }, } diff --git a/crates/iridium_core/src/catalog/mod.rs b/crates/iridium_core/src/catalog/mod.rs index 5cd9d52..3078730 100644 --- a/crates/iridium_core/src/catalog/mod.rs +++ b/crates/iridium_core/src/catalog/mod.rs @@ -70,6 +70,8 @@ pub struct ColumnDef { pub check: Option, pub check_constraint_name: Option, pub computed_expr: Option, + pub collation: Option, + pub is_clustered: bool, #[serde(default = "default_ansi_padding_on")] pub ansi_padding_on: bool, } diff --git a/crates/iridium_core/src/executor/database/dispatch.rs b/crates/iridium_core/src/executor/database/dispatch.rs index a7b27dc..b2c9a35 100644 --- a/crates/iridium_core/src/executor/database/dispatch.rs +++ b/crates/iridium_core/src/executor/database/dispatch.rs @@ -101,8 +101,17 @@ fn handle_session_statement( Err(e) => Some(Err(e)), } } else if let Statement::Session(SessionStatement::SetIdentityInsert(ref id_stmt)) = stmt { + let storage_guard = state.storage.read(); + let (catalog, _) = storage_guard.get_refs(); + let schema = id_stmt.table.schema_or_dbo(); + if catalog.find_table(schema, &id_stmt.table.name).is_none() { + return Some(Err(DbError::table_not_found(schema, &id_stmt.table.name))); + } + let table_name = normalize_identifier(&id_stmt.table.name); if id_stmt.on { + // SQL Server only allows one table to have IDENTITY_INSERT ON at a time in a session. + session_options.identity_insert.clear(); session_options.identity_insert.insert(table_name); } else { session_options.identity_insert.remove(&table_name); diff --git a/crates/iridium_core/src/executor/evaluator.rs b/crates/iridium_core/src/executor/evaluator.rs index dd57510..d9b5a79 100644 --- a/crates/iridium_core/src/executor/evaluator.rs +++ b/crates/iridium_core/src/executor/evaluator.rs @@ -261,9 +261,18 @@ fn eval_expr_inner( Expr::Like { expr: like_expr, pattern, + escape, negated, } => eval_like( - like_expr, pattern, *negated, row, ctx, catalog, storage, clock, + like_expr, + pattern, + escape.as_deref(), + *negated, + row, + ctx, + catalog, + storage, + clock, ), Expr::Subquery(stmt) => eval_scalar_subquery(stmt, row, ctx, catalog, storage, clock), Expr::Exists { subquery, negated } => { diff --git a/crates/iridium_core/src/executor/identifier.rs b/crates/iridium_core/src/executor/identifier.rs index 515a6e7..c9688dc 100644 --- a/crates/iridium_core/src/executor/identifier.rs +++ b/crates/iridium_core/src/executor/identifier.rs @@ -23,14 +23,21 @@ pub(crate) fn resolve_identifier( } } + let is_identity_col = name.eq_ignore_ascii_case("IDENTITYCOL"); + let mut matches: Vec<(usize, Value)> = Vec::new(); for (binding_idx, binding) in row.iter().enumerate() { - if let Some(col_idx) = binding - .table - .columns - .iter() - .position(|c| c.name.eq_ignore_ascii_case(name)) - { + let col_idx = if is_identity_col { + binding.table.columns.iter().position(|c| c.identity.is_some()) + } else { + binding + .table + .columns + .iter() + .position(|c| c.name.eq_ignore_ascii_case(name)) + }; + + if let Some(col_idx) = col_idx { let value = binding .row .as_ref() @@ -43,12 +50,16 @@ pub(crate) fn resolve_identifier( if matches.is_empty() { for apply_row in ctx.row.apply_stack.iter().rev() { for binding in apply_row.iter() { - if let Some(col_idx) = binding - .table - .columns - .iter() - .position(|c| c.name.eq_ignore_ascii_case(name)) - { + let col_idx = if is_identity_col { + binding.table.columns.iter().position(|c| c.identity.is_some()) + } else { + binding + .table + .columns + .iter() + .position(|c| c.name.eq_ignore_ascii_case(name)) + }; + if let Some(col_idx) = col_idx { let value = binding .row .as_ref() @@ -66,12 +77,16 @@ pub(crate) fn resolve_identifier( if matches.is_empty() { for outer_row in ctx.row.outer_stack.iter().rev() { for binding in outer_row.iter() { - if let Some(col_idx) = binding - .table - .columns - .iter() - .position(|c| c.name.eq_ignore_ascii_case(name)) - { + let col_idx = if is_identity_col { + binding.table.columns.iter().position(|c| c.identity.is_some()) + } else { + binding + .table + .columns + .iter() + .position(|c| c.name.eq_ignore_ascii_case(name)) + }; + if let Some(col_idx) = col_idx { let value = binding .row .as_ref() @@ -109,6 +124,7 @@ pub(crate) fn resolve_qualified_identifier( let table_name = &parts[0]; let column_name = &parts[1]; + let is_identity_col = column_name.eq_ignore_ascii_case("IDENTITYCOL"); let search_row = |row: &[ContextTable]| -> Result, DbError> { for binding in row { @@ -119,11 +135,15 @@ pub(crate) fn resolve_qualified_identifier( .iter() .any(|a| a.eq_ignore_ascii_case(table_name)) { - let idx = binding - .table - .columns - .iter() - .position(|c| c.name.eq_ignore_ascii_case(column_name)); + let idx = if is_identity_col { + binding.table.columns.iter().position(|c| c.identity.is_some()) + } else { + binding + .table + .columns + .iter() + .position(|c| c.name.eq_ignore_ascii_case(column_name)) + }; if let Some(idx) = idx { return Ok(Some( diff --git a/crates/iridium_core/src/executor/metadata/mod.rs b/crates/iridium_core/src/executor/metadata/mod.rs index ab780b8..1a5ca8a 100644 --- a/crates/iridium_core/src/executor/metadata/mod.rs +++ b/crates/iridium_core/src/executor/metadata/mod.rs @@ -61,6 +61,8 @@ pub(super) fn virtual_table_def(name: &str, cols: Vec<(&str, DataType, bool)>) - check: None, check_constraint_name: None, computed_expr: None, + collation: None, + is_clustered: false, ansi_padding_on: true, }) .collect(), diff --git a/crates/iridium_core/src/executor/mutation/insert.rs b/crates/iridium_core/src/executor/mutation/insert.rs index 95e0d82..4561773 100644 --- a/crates/iridium_core/src/executor/mutation/insert.rs +++ b/crates/iridium_core/src/executor/mutation/insert.rs @@ -284,7 +284,7 @@ impl<'a> MutationExecutor<'a> { } } else if col.identity.is_some() { let table_upper = normalize_identifier(&table.name); - if !ctx.session.identity_insert.contains(&table_upper) { + if !ctx.options.identity_insert.contains(&table_upper) { return Err(DbError::Execution(format!( "Cannot insert explicit value for identity column '{}' in table '{}' when IDENTITY_INSERT is set to OFF.", col.name, table.name diff --git a/crates/iridium_core/src/executor/predicates.rs b/crates/iridium_core/src/executor/predicates.rs index b823820..ce2c1c3 100644 --- a/crates/iridium_core/src/executor/predicates.rs +++ b/crates/iridium_core/src/executor/predicates.rs @@ -132,6 +132,7 @@ pub(crate) fn eval_between( pub(crate) fn eval_like( like_expr: &Expr, pattern: &Expr, + escape: Option<&Expr>, negated: bool, row: &[ContextTable], ctx: &mut ExecutionContext, @@ -148,40 +149,66 @@ pub(crate) fn eval_like( let s = val.to_string_value(); let p = pat.to_string_value(); - let matched = like_match(&s, &p); + let esc = match escape { + Some(e) => { + let ev = eval_expr(e, row, ctx, catalog, storage, clock)?; + if ev.is_null() { + return Ok(Value::Null); + } + Some(ev.to_string_value()) + } + None => None, + }; + let matched = like_match(&s, &p, esc.as_deref()); Ok(Value::Bit(if negated { !matched } else { matched })) } -fn like_match(s: &str, pattern: &str) -> bool { +fn like_match(s: &str, pattern: &str, escape: Option<&str>) -> bool { let s: Vec = s.to_ascii_uppercase().chars().collect(); - let p: Vec = pattern.to_ascii_uppercase().chars().collect(); + let p_raw: Vec = pattern.to_ascii_uppercase().chars().collect(); + let esc_char = escape.and_then(|e| e.chars().next().map(|c| c.to_ascii_uppercase())); + + let mut p = Vec::new(); + let mut escaped = Vec::new(); + let mut i = 0; + while i < p_raw.len() { + if let Some(ec) = esc_char { + if p_raw[i] == ec && i + 1 < p_raw.len() { + p.push(p_raw[i + 1]); + escaped.push(true); + i += 2; + continue; + } + } + p.push(p_raw[i]); + escaped.push(false); + i += 1; + } + let sn = s.len(); let pn = p.len(); - // dp[j] = whether s[0..i] matches p[0..j] let mut dp = vec![false; pn + 1]; dp[0] = true; - // leading '%' can match empty string for j in 0..pn { - if p[j] == '%' { + if p[j] == '%' && !escaped[j] { dp[j + 1] = dp[j]; } else { break; } } - for i in 0..sn { - let mut prev = dp[0]; // dp_prev[0] (previous row, col 0) + for i_s in 0..sn { + let mut prev = dp[0]; dp[0] = false; for j in 0..pn { - let tmp = dp[j + 1]; // save dp_prev[j+1] before overwrite - dp[j + 1] = match p[j] { - '%' => { - // dp_prev[j+1] (skip char in s) || dp[j] (skip '%' in pattern) - tmp || dp[j] - } - '_' => prev, // dp_prev[j]: both advance by one - c => prev && s[i] == c, + let tmp = dp[j + 1]; + dp[j + 1] = if p[j] == '%' && !escaped[j] { + tmp || dp[j] + } else if (p[j] == '_' && !escaped[j]) || (p[j] == s[i_s]) { + prev + } else { + false }; prev = tmp; } diff --git a/crates/iridium_core/src/executor/query/binder/mod.rs b/crates/iridium_core/src/executor/query/binder/mod.rs index a579e3f..e8e8b2e 100644 --- a/crates/iridium_core/src/executor/query/binder/mod.rs +++ b/crates/iridium_core/src/executor/query/binder/mod.rs @@ -92,6 +92,8 @@ pub(super) fn query_result_to_bound_table( check: None, check_constraint_name: None, computed_expr: None, + collation: None, + is_clustered: false, ansi_padding_on: true, }) .collect(), diff --git a/crates/iridium_core/src/executor/query/binder/tvf.rs b/crates/iridium_core/src/executor/query/binder/tvf.rs index 2efe81f..4b5cda7 100644 --- a/crates/iridium_core/src/executor/query/binder/tvf.rs +++ b/crates/iridium_core/src/executor/query/binder/tvf.rs @@ -108,6 +108,8 @@ pub(super) fn bind_builtin_tvf( check: None, check_constraint_name: None, computed_expr: None, + collation: None, + is_clustered: false, ansi_padding_on: true, }]; @@ -125,6 +127,8 @@ pub(super) fn bind_builtin_tvf( check: None, check_constraint_name: None, computed_expr: None, + collation: None, + is_clustered: false, ansi_padding_on: true, }); } @@ -328,6 +332,8 @@ fn bind_openjson( check: None, check_constraint_name: None, computed_expr: None, + collation: None, + is_clustered: false, ansi_padding_on: true, }, ColumnDef { @@ -343,6 +349,8 @@ fn bind_openjson( check: None, check_constraint_name: None, computed_expr: None, + collation: None, + is_clustered: false, ansi_padding_on: true, }, ColumnDef { @@ -358,6 +366,8 @@ fn bind_openjson( check: None, check_constraint_name: None, computed_expr: None, + collation: None, + is_clustered: false, ansi_padding_on: true, }, ]; diff --git a/crates/iridium_core/src/executor/query/binder/values.rs b/crates/iridium_core/src/executor/query/binder/values.rs index dcece73..61624a6 100644 --- a/crates/iridium_core/src/executor/query/binder/values.rs +++ b/crates/iridium_core/src/executor/query/binder/values.rs @@ -56,6 +56,8 @@ pub(super) fn bind_plain_table( check: None, check_constraint_name: None, computed_expr: None, + collation: None, + is_clustered: false, ansi_padding_on: true, }); } diff --git a/crates/iridium_core/src/executor/query/from_tree.rs b/crates/iridium_core/src/executor/query/from_tree.rs index 0d56a58..8425272 100644 --- a/crates/iridium_core/src/executor/query/from_tree.rs +++ b/crates/iridium_core/src/executor/query/from_tree.rs @@ -398,6 +398,8 @@ fn apply_from_alias( check: None, check_constraint_name: None, computed_expr: None, + collation: None, + is_clustered: false, ansi_padding_on: true, }); } diff --git a/crates/iridium_core/src/executor/query/transformer/apply.rs b/crates/iridium_core/src/executor/query/transformer/apply.rs index 210efff..4227a01 100644 --- a/crates/iridium_core/src/executor/query/transformer/apply.rs +++ b/crates/iridium_core/src/executor/query/transformer/apply.rs @@ -80,6 +80,8 @@ fn build_virtual_table(alias: &str, sub_result: &QueryResult) -> TableDef { check: None, check_constraint_name: None, computed_expr: None, + collation: None, + is_clustered: false, ansi_padding_on: true, }) .collect(), diff --git a/crates/iridium_core/src/executor/query/transformer/pivot.rs b/crates/iridium_core/src/executor/query/transformer/pivot.rs index feeb15e..f973ceb 100644 --- a/crates/iridium_core/src/executor/query/transformer/pivot.rs +++ b/crates/iridium_core/src/executor/query/transformer/pivot.rs @@ -95,6 +95,8 @@ pub(crate) fn execute_pivot( check: None, check_constraint_name: None, computed_expr: None, + collation: None, + is_clustered: false, ansi_padding_on: true, }); } diff --git a/crates/iridium_core/src/executor/query/transformer/unpivot.rs b/crates/iridium_core/src/executor/query/transformer/unpivot.rs index b0ca7e4..2b7ce95 100644 --- a/crates/iridium_core/src/executor/query/transformer/unpivot.rs +++ b/crates/iridium_core/src/executor/query/transformer/unpivot.rs @@ -68,6 +68,8 @@ pub(crate) fn execute_unpivot( check: None, check_constraint_name: None, computed_expr: None, + collation: None, + is_clustered: false, ansi_padding_on: true, }); output_columns.push(ColumnDef { @@ -83,6 +85,8 @@ pub(crate) fn execute_unpivot( check: None, check_constraint_name: None, computed_expr: None, + collation: None, + is_clustered: false, ansi_padding_on: true, }); diff --git a/crates/iridium_core/src/executor/schema.rs b/crates/iridium_core/src/executor/schema.rs index be1e394..de6cbd2 100644 --- a/crates/iridium_core/src/executor/schema.rs +++ b/crates/iridium_core/src/executor/schema.rs @@ -48,34 +48,44 @@ fn apply_table_constraint(table: &mut TableDef, tc: TableConstraintSpec) -> Resu on_update: on_update.unwrap_or(crate::ast::ReferentialAction::NoAction), }); } - TableConstraintSpec::PrimaryKey { name: _, columns } => { - for col_name in &columns { + TableConstraintSpec::PrimaryKey { + name: _, + columns, + is_clustered, + } => { + for col_spec in &columns { let col = table .columns .iter_mut() - .find(|c| c.name.eq_ignore_ascii_case(col_name)) - .ok_or_else(|| DbError::column_not_found(col_name))?; + .find(|c| c.name.eq_ignore_ascii_case(&col_spec.name)) + .ok_or_else(|| DbError::column_not_found(&col_spec.name))?; col.primary_key = true; col.nullable = false; + col.is_clustered = is_clustered; } if columns.len() == 1 { if let Some(col) = table .columns .iter_mut() - .find(|c| c.name.eq_ignore_ascii_case(&columns[0])) + .find(|c| c.name.eq_ignore_ascii_case(&columns[0].name)) { col.unique = true; } } } - TableConstraintSpec::Unique { name: _, columns } => { - for col_name in &columns { + TableConstraintSpec::Unique { + name: _, + columns, + is_clustered, + } => { + for col_spec in &columns { let col = table .columns .iter_mut() - .find(|c| c.name.eq_ignore_ascii_case(col_name)) - .ok_or_else(|| DbError::column_not_found(col_name))?; + .find(|c| c.name.eq_ignore_ascii_case(&col_spec.name)) + .ok_or_else(|| DbError::column_not_found(&col_spec.name))?; col.unique = true; + col.is_clustered = is_clustered; } } } @@ -171,10 +181,14 @@ impl<'a> SchemaExecutor<'a> { self.catalog.register_table(table.clone()); self.storage.ensure_table(table_id)?; - // Create clustered indexes for PRIMARY KEYs + // Create indexes for PRIMARY KEYs and UNIQUE columns that are marked as clustered for col in &table.columns { - if col.primary_key { - let index_name = format!("PK__{}__{}", table.name, col.name); + if col.primary_key || (col.unique && col.is_clustered) { + let index_name = if col.primary_key { + format!("PK__{}__{}", table.name, col.name) + } else { + format!("UQ__{}__{}", table.name, col.name) + }; self.catalog .create_index_with_options( "dbo", @@ -182,11 +196,11 @@ impl<'a> SchemaExecutor<'a> { &table.schema_name, &table.name, std::slice::from_ref(&col.name), - true, // is_clustered - col.unique, // is_unique + col.is_clustered, + col.unique || col.primary_key, ) .map_err(|e| { - DbError::Execution(format!("Failed to create primary key index: {}", e)) + DbError::Execution(format!("Failed to create constraint index: {}", e)) })?; } } @@ -302,6 +316,8 @@ impl<'a> SchemaExecutor<'a> { check: spec.check, check_constraint_name: spec.check_constraint_name, computed_expr: spec.computed_expr, + collation: spec.collation, + is_clustered: spec.is_clustered, ansi_padding_on: self.session_options.ansi_padding, }) } @@ -310,12 +326,15 @@ impl<'a> SchemaExecutor<'a> { let index_schema = stmt.name.schema_or_dbo().to_string(); let table_schema = stmt.table.schema_or_dbo().to_string(); - self.catalog.create_index( + let col_names: Vec = stmt.columns.iter().map(|c| c.name.clone()).collect(); + self.catalog.create_index_with_options( &index_schema, &stmt.name.name, &table_schema, &stmt.table.name, - &stmt.columns, + &col_names, + stmt.is_clustered, + stmt.is_unique, )?; let index_id = self diff --git a/crates/iridium_core/src/executor/script/dml/cte.rs b/crates/iridium_core/src/executor/script/dml/cte.rs index 0c88e10..0703fc9 100644 --- a/crates/iridium_core/src/executor/script/dml/cte.rs +++ b/crates/iridium_core/src/executor/script/dml/cte.rs @@ -59,6 +59,8 @@ impl<'a> ScriptExecutor<'a> { computed_expr: None, check: None, check_constraint_name: None, + collation: None, + is_clustered: false, ansi_padding_on: true, }) .collect(), @@ -173,6 +175,8 @@ impl<'a> ScriptExecutor<'a> { computed_expr: None, check: None, check_constraint_name: None, + collation: None, + is_clustered: false, ansi_padding_on: true, }) .collect(), diff --git a/crates/iridium_core/src/executor/script/dml/merge_helpers.rs b/crates/iridium_core/src/executor/script/dml/merge_helpers.rs index d682433..754d53f 100644 --- a/crates/iridium_core/src/executor/script/dml/merge_helpers.rs +++ b/crates/iridium_core/src/executor/script/dml/merge_helpers.rs @@ -92,6 +92,8 @@ pub(crate) fn synthetic_source_table(source_alias: String, target_table: &TableD check: None, check_constraint_name: None, computed_expr: None, + collation: None, + is_clustered: false, ansi_padding_on: true, }) .collect(), diff --git a/crates/iridium_core/src/executor/script/dml/mod.rs b/crates/iridium_core/src/executor/script/dml/mod.rs index f08d740..e3a3122 100644 --- a/crates/iridium_core/src/executor/script/dml/mod.rs +++ b/crates/iridium_core/src/executor/script/dml/mod.rs @@ -118,6 +118,8 @@ impl<'a> ScriptExecutor<'a> { check: None, check_constraint_name: None, computed_expr: None, + collation: None, + is_clustered: false, ansi_padding_on: true, }); } diff --git a/crates/iridium_core/src/executor/tooling/formatting.rs b/crates/iridium_core/src/executor/tooling/formatting.rs index f592f61..0606456 100644 --- a/crates/iridium_core/src/executor/tooling/formatting.rs +++ b/crates/iridium_core/src/executor/tooling/formatting.rs @@ -203,10 +203,15 @@ pub(crate) fn format_expr(expr: &Expr) -> String { Expr::Like { expr, pattern, + escape, negated, } => { let not = if *negated { "NOT " } else { "" }; - format!("{} {}LIKE {}", format_expr(expr), not, format_expr(pattern)) + let mut s = format!("{} {}LIKE {}", format_expr(expr), not, format_expr(pattern)); + if let Some(e) = escape { + s.push_str(&format!(" ESCAPE {}", format_expr(e))); + } + s } Expr::Subquery(_) => "(SELECT ...)".to_string(), Expr::Exists { diff --git a/crates/iridium_core/src/parser/ast/common.rs b/crates/iridium_core/src/parser/ast/common.rs index dc19387..1c3fb31 100644 --- a/crates/iridium_core/src/parser/ast/common.rs +++ b/crates/iridium_core/src/parser/ast/common.rs @@ -32,6 +32,8 @@ pub enum DataType { NVarChar(Option), Binary(Option), VarBinary(Option), + NationalChar(Option), + NationalVarChar(Option), Vector(u16), Date, Time, diff --git a/crates/iridium_core/src/parser/ast/expressions.rs b/crates/iridium_core/src/parser/ast/expressions.rs index c06e36f..b8047cc 100644 --- a/crates/iridium_core/src/parser/ast/expressions.rs +++ b/crates/iridium_core/src/parser/ast/expressions.rs @@ -92,6 +92,7 @@ pub enum Expr { Like { expr: Box, pattern: Box, + escape: Option>, negated: bool, }, IsNull(Box), diff --git a/crates/iridium_core/src/parser/ast/statements/other.rs b/crates/iridium_core/src/parser/ast/statements/other.rs index 9b80351..31f1bc7 100644 --- a/crates/iridium_core/src/parser/ast/statements/other.rs +++ b/crates/iridium_core/src/parser/ast/statements/other.rs @@ -71,11 +71,7 @@ pub enum DdlStatement { cycle: bool, }, DropSequence(Vec), - CreateIndex { - name: Vec, - table: Vec, - columns: Vec, - }, + CreateIndex(Box), CreateType { name: Vec, columns: Vec, @@ -382,6 +378,8 @@ pub struct ColumnDef { pub check_constraint_name: Option, pub computed_expr: Option, pub foreign_key: Option, + pub collation: Option, + pub is_clustered: bool, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -410,11 +408,13 @@ pub enum AlterTableAction { pub enum TableConstraint { PrimaryKey { name: Option, - columns: Vec, + columns: Vec, + is_clustered: bool, }, Unique { name: Option, - columns: Vec, + columns: Vec, + is_clustered: bool, }, ForeignKey { name: Option, @@ -453,6 +453,27 @@ pub enum FetchDirection { Relative(Expr), } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct IndexColumn { + pub name: String, + pub is_desc: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum IndexOption { + FillFactor(u8), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct CreateIndexStmt { + pub name: Vec, + pub table: Vec, + pub is_unique: bool, + pub is_clustered: bool, + pub columns: Vec, + pub options: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct SelectAssignTarget { pub variable: String, diff --git a/crates/iridium_core/src/parser/lower/common.rs b/crates/iridium_core/src/parser/lower/common.rs index 01074e7..0f89876 100644 --- a/crates/iridium_core/src/parser/lower/common.rs +++ b/crates/iridium_core/src/parser/lower/common.rs @@ -29,6 +29,7 @@ pub fn lower_expr(parser_expr: ast::Expr) -> Result Ok(executor_ast::expressions::Expr::Like { expr: Box::new(lower_expr(*left)?), pattern: Box::new(lower_expr(*right)?), + escape: None, negated: false, }), _ => Ok(executor_ast::expressions::Expr::Binary { @@ -128,10 +129,12 @@ pub fn lower_expr(parser_expr: ast::Expr) -> Result Ok(executor_ast::expressions::Expr::Like { expr: Box::new(lower_expr(*expr)?), pattern: Box::new(lower_expr(*pattern)?), + escape: escape.map(|e| lower_expr(*e)).transpose()?.map(Box::new), negated, }), ast::Expr::Exists { subquery, negated } => Ok(executor_ast::expressions::Expr::Exists { @@ -372,6 +375,12 @@ pub fn lower_data_type( ast::DataType::NText => Ok(executor_ast::data_types::DataTypeSpec::NVarChar(4000)), ast::DataType::Table => Ok(executor_ast::data_types::DataTypeSpec::VarChar(255)), ast::DataType::Custom(_) => Ok(executor_ast::data_types::DataTypeSpec::VarChar(255)), + ast::DataType::NationalChar(n) => Ok(executor_ast::data_types::DataTypeSpec::NChar( + n.unwrap_or(1) as u16, + )), + ast::DataType::NationalVarChar(n) => Ok(executor_ast::data_types::DataTypeSpec::NVarChar( + n.unwrap_or(u16::MAX as u32) as u16, + )), } } diff --git a/crates/iridium_core/src/parser/lower/ddl.rs b/crates/iridium_core/src/parser/lower/ddl.rs index 7cd2e1e..196f264 100644 --- a/crates/iridium_core/src/parser/lower/ddl.rs +++ b/crates/iridium_core/src/parser/lower/ddl.rs @@ -78,16 +78,30 @@ pub fn lower_ddl(ddl: ast::DdlStatement) -> Result Ok(executor_ast::Statement::Ddl( + ast::DdlStatement::CreateIndex(stmt) => Ok(executor_ast::Statement::Ddl( executor_ast::statements::DdlStatement::CreateIndex( executor_ast::statements::ddl::CreateIndexStmt { - name: lower_object_name(name), - table: lower_object_name(table), - columns, + name: lower_object_name(stmt.name), + table: lower_object_name(stmt.table), + columns: stmt + .columns + .into_iter() + .map(|c| executor_ast::statements::ddl::IndexColumnSpec { + name: c.name, + is_desc: c.is_desc, + }) + .collect(), + is_unique: stmt.is_unique, + is_clustered: stmt.is_clustered, + options: stmt + .options + .into_iter() + .map(|o| match o { + ast::IndexOption::FillFactor(f) => { + executor_ast::statements::ddl::IndexOptionSpec::FillFactor(f) + } + }) + .collect(), }, ), )), @@ -295,10 +309,21 @@ pub fn lower_column_def( on_delete: fk.on_delete.map(lower_referential_action), on_update: fk.on_update.map(lower_referential_action), }), + collation: c.collation, + is_clustered: c.is_clustered, ansi_padding_on: true, }) } +pub fn lower_index_column( + c: ast::IndexColumn, +) -> executor_ast::statements::ddl::IndexColumnSpec { + executor_ast::statements::ddl::IndexColumnSpec { + name: c.name, + is_desc: c.is_desc, + } +} + pub fn lower_routine_param( p: ast::RoutineParam, ) -> Result { @@ -374,18 +399,28 @@ pub fn lower_table_constraint( c: ast::TableConstraint, ) -> Result { match c { - ast::TableConstraint::PrimaryKey { name, columns } => Ok( + ast::TableConstraint::PrimaryKey { + name, + columns, + is_clustered, + } => Ok( executor_ast::statements::ddl::TableConstraintSpec::PrimaryKey { name: name.unwrap_or_default(), - columns, + columns: columns.into_iter().map(lower_index_column).collect(), + is_clustered, }, ), - ast::TableConstraint::Unique { name, columns } => { - Ok(executor_ast::statements::ddl::TableConstraintSpec::Unique { + ast::TableConstraint::Unique { + name, + columns, + is_clustered, + } => Ok( + executor_ast::statements::ddl::TableConstraintSpec::Unique { name: name.unwrap_or_default(), - columns, - }) - } + columns: columns.into_iter().map(lower_index_column).collect(), + is_clustered, + }, + ), ast::TableConstraint::ForeignKey { name, columns, diff --git a/crates/iridium_core/src/parser/lower/dml.rs b/crates/iridium_core/src/parser/lower/dml.rs index 98eef85..a38ab92 100644 --- a/crates/iridium_core/src/parser/lower/dml.rs +++ b/crates/iridium_core/src/parser/lower/dml.rs @@ -773,6 +773,8 @@ pub fn lower_insert_bulk( on_update: fk.on_update.map(super::ddl::lower_referential_action), } }), + collation: c.collation, + is_clustered: c.is_clustered, ansi_padding_on: true, }) }) diff --git a/crates/iridium_core/src/parser/parse/expressions.rs b/crates/iridium_core/src/parser/parse/expressions.rs index 399b770..156b95a 100644 --- a/crates/iridium_core/src/parser/parse/expressions.rs +++ b/crates/iridium_core/src/parser/parse/expressions.rs @@ -40,9 +40,15 @@ fn parse_pratt_expr(parser: &mut Parser, min_bp: u8) -> ParseResult { } let _ = parser.next(); let pattern = parse_pratt_expr(parser, r_bp)?; + let mut escape = None; + if parser.at_keyword(Keyword::Escape) { + let _ = parser.next(); + escape = Some(Box::new(parse_pratt_expr(parser, r_bp)?)); + } left = Expr::Like { expr: Box::new(left), pattern: Box::new(pattern), + escape, negated: false, }; continue; @@ -102,9 +108,15 @@ fn parse_pratt_expr(parser: &mut Parser, min_bp: u8) -> ParseResult { let _ = parser.next(); let _ = parser.next(); let pattern = parse_pratt_expr(parser, r_bp)?; + let mut escape = None; + if parser.at_keyword(Keyword::Escape) { + let _ = parser.next(); + escape = Some(Box::new(parse_pratt_expr(parser, r_bp)?)); + } left = Expr::Like { expr: Box::new(left), pattern: Box::new(pattern), + escape, negated: true, }; continue; @@ -301,6 +313,18 @@ pub fn parse_primary(parser: &mut Parser) -> ParseResult { let _ = parser.next(); Ok(Expr::Null) } + Some(Token::Keyword(k)) if *k == Keyword::Coalesce || *k == Keyword::Nullif => { + let name = k.as_ref().to_string(); + let _ = parser.next(); + parser.expect_lparen()?; + let args = parse_comma_list(parser, parse_expr)?; + parser.expect_rparen()?; + Ok(Expr::FunctionCall { + name, + args, + within_group: Vec::new(), + }) + } Some(Token::Keyword(k)) if *k == Keyword::Case => { let _ = parser.next(); parse_case(parser) @@ -851,16 +875,35 @@ pub fn parse_data_type(parser: &mut Parser) -> ParseResult { Ok(DataType::Numeric(p, s)) } } - "CHAR" => { + "CHARACTER" | "CHAR" => { + let is_char = upper == "CHAR"; + let mut is_varying = false; let mut size = None; + if parser.at_keyword(Keyword::Varying) { + let _ = parser.next(); + is_varying = true; + } if matches!(parser.peek(), Some(Token::LParen)) { let _ = parser.next(); if let Some(Token::Number { value: s, .. }) = parser.next() { size = Some(*s as u32); + } else if parser.at_keyword(Keyword::Max) { + let _ = parser.next(); + size = None; } parser.expect_rparen()?; } - Ok(DataType::Char(size)) + if !is_varying && parser.at_keyword(Keyword::Varying) { + let _ = parser.next(); + is_varying = true; + } + if is_varying { + Ok(DataType::VarChar(size)) + } else if is_char { + Ok(DataType::Char(size)) + } else { + Ok(DataType::Char(size)) + } } "VARCHAR" => { let mut size = None; @@ -868,21 +911,69 @@ pub fn parse_data_type(parser: &mut Parser) -> ParseResult { let _ = parser.next(); if let Some(Token::Number { value: s, .. }) = parser.next() { size = Some(*s as u32); + } else if parser.at_keyword(Keyword::Max) { + let _ = parser.next(); + size = None; } parser.expect_rparen()?; } Ok(DataType::VarChar(size)) } + "DOUBLE" => { + parser.expect_keyword(Keyword::Precision)?; + Ok(DataType::Float) + } + "NATIONAL" => { + if parser.at_keyword(Keyword::Character) { + let _ = parser.next(); + } else if parser.at_keyword(Keyword::Char) { + let _ = parser.next(); + } else if parser.at_keyword(Keyword::Varchar) { + let _ = parser.next(); + let mut size = None; + if matches!(parser.peek(), Some(Token::LParen)) { + let _ = parser.next(); + if let Some(Token::Number { value: s, .. }) = parser.next() { + size = Some(*s as u32); + } + parser.expect_rparen()?; + } + return Ok(DataType::NationalVarChar(size)); + } else { + return parser.backtrack(Expected::Description("CHARACTER or VARCHAR")); + } + let mut is_varying = false; + if parser.at_keyword(Keyword::Varying) { + let _ = parser.next(); + is_varying = true; + } + let mut size = None; + if matches!(parser.peek(), Some(Token::LParen)) { + let _ = parser.next(); + if let Some(Token::Number { value: s, .. }) = parser.next() { + size = Some(*s as u32); + } + parser.expect_rparen()?; + } + if is_varying { + Ok(DataType::NationalVarChar(size)) + } else { + Ok(DataType::NationalChar(size)) + } + } "NCHAR" => { let mut size = None; if matches!(parser.peek(), Some(Token::LParen)) { let _ = parser.next(); if let Some(Token::Number { value: s, .. }) = parser.next() { size = Some(*s as u32); + } else if parser.at_keyword(Keyword::Max) { + let _ = parser.next(); + size = None; } parser.expect_rparen()?; } - Ok(DataType::NChar(size)) + Ok(DataType::NationalChar(size)) } "NVARCHAR" => { let mut size = None; @@ -890,10 +981,13 @@ pub fn parse_data_type(parser: &mut Parser) -> ParseResult { let _ = parser.next(); if let Some(Token::Number { value: s, .. }) = parser.next() { size = Some(*s as u32); + } else if parser.at_keyword(Keyword::Max) { + let _ = parser.next(); + size = None; } parser.expect_rparen()?; } - Ok(DataType::NVarChar(size)) + Ok(DataType::NationalVarChar(size)) } "BINARY" => { let mut size = None; @@ -981,7 +1075,7 @@ pub fn parse_data_type(parser: &mut Parser) -> ParseResult { } } } - Some(Token::Keyword(kw)) => match kw { + Some(Token::Keyword(kw)) => match *kw { Keyword::Int => Ok(DataType::Int), Keyword::BigInt => Ok(DataType::BigInt), Keyword::SmallInt => Ok(DataType::SmallInt), @@ -1012,16 +1106,32 @@ pub fn parse_data_type(parser: &mut Parser) -> ParseResult { Ok(DataType::Numeric(p, s)) } } - Keyword::Char => { + Keyword::Char | Keyword::Character => { + let mut is_varying = false; let mut size = None; + if parser.at_keyword(Keyword::Varying) { + let _ = parser.next(); + is_varying = true; + } if matches!(parser.peek(), Some(Token::LParen)) { let _ = parser.next(); if let Some(Token::Number { value: s, .. }) = parser.next() { size = Some(*s as u32); + } else if parser.at_keyword(Keyword::Max) { + let _ = parser.next(); + size = None; } parser.expect_rparen()?; } - Ok(DataType::Char(size)) + if !is_varying && parser.at_keyword(Keyword::Varying) { + let _ = parser.next(); + is_varying = true; + } + if is_varying { + Ok(DataType::VarChar(size)) + } else { + Ok(DataType::Char(size)) + } } Keyword::Varchar => { let mut size = None; @@ -1029,21 +1139,52 @@ pub fn parse_data_type(parser: &mut Parser) -> ParseResult { let _ = parser.next(); if let Some(Token::Number { value: s, .. }) = parser.next() { size = Some(*s as u32); + } else if parser.at_keyword(Keyword::Max) { + let _ = parser.next(); + size = None; } parser.expect_rparen()?; } Ok(DataType::VarChar(size)) } - Keyword::NChar => { + Keyword::NChar | Keyword::National => { + let mut is_varying = false; + if *kw == Keyword::National { + if parser.at_keyword(Keyword::Character) { + let _ = parser.next(); + } else if parser.at_keyword(Keyword::Char) { + let _ = parser.next(); + } else if parser.at_keyword(Keyword::Varchar) { + let _ = parser.next(); + is_varying = true; + } else { + return parser.backtrack(Expected::Description("CHARACTER or VARCHAR")); + } + } + if parser.at_keyword(Keyword::Varying) { + let _ = parser.next(); + is_varying = true; + } let mut size = None; if matches!(parser.peek(), Some(Token::LParen)) { let _ = parser.next(); if let Some(Token::Number { value: s, .. }) = parser.next() { size = Some(*s as u32); + } else if parser.at_keyword(Keyword::Max) { + let _ = parser.next(); + size = None; } parser.expect_rparen()?; } - Ok(DataType::NChar(size)) + if !is_varying && parser.at_keyword(Keyword::Varying) { + let _ = parser.next(); + is_varying = true; + } + if is_varying { + Ok(DataType::NationalVarChar(size)) + } else { + Ok(DataType::NationalChar(size)) + } } Keyword::Nvarchar => { let mut size = None; @@ -1051,10 +1192,13 @@ pub fn parse_data_type(parser: &mut Parser) -> ParseResult { let _ = parser.next(); if let Some(Token::Number { value: s, .. }) = parser.next() { size = Some(*s as u32); + } else if parser.at_keyword(Keyword::Max) { + let _ = parser.next(); + size = None; } parser.expect_rparen()?; } - Ok(DataType::NVarChar(size)) + Ok(DataType::NationalVarChar(size)) } Keyword::Binary => { let mut size = None; diff --git a/crates/iridium_core/src/parser/parse/mod.rs b/crates/iridium_core/src/parser/parse/mod.rs index 5911ac4..18e2a65 100644 --- a/crates/iridium_core/src/parser/parse/mod.rs +++ b/crates/iridium_core/src/parser/parse/mod.rs @@ -92,9 +92,40 @@ fn parse_statement_inner(parser: &mut Parser) -> ParseResult { } Keyword::Create => { let _ = parser.next(); + if parser.at_keyword(Keyword::Unique) { + let _ = parser.next(); + let mut is_clustered = false; + if parser.at_keyword(Keyword::Clustered) { + let _ = parser.next(); + is_clustered = true; + } else if parser.at_keyword(Keyword::Nonclustered) { + let _ = parser.next(); + is_clustered = false; + } + parser.expect_keyword(Keyword::Index)?; + return Ok(Statement::Ddl(DdlStatement::CreateIndex(Box::new( + parse_create_index(parser, true, is_clustered)?, + )))); + } + if parser.at_keyword(Keyword::Clustered) { + let _ = parser.next(); + parser.expect_keyword(Keyword::Index)?; + return Ok(Statement::Ddl(DdlStatement::CreateIndex(Box::new( + parse_create_index(parser, false, true)?, + )))); + } + if parser.at_keyword(Keyword::Nonclustered) { + let _ = parser.next(); + parser.expect_keyword(Keyword::Index)?; + return Ok(Statement::Ddl(DdlStatement::CreateIndex(Box::new( + parse_create_index(parser, false, false)?, + )))); + } if parser.at_keyword(Keyword::Index) { let _ = parser.next(); - return parse_create_index(parser); + return Ok(Statement::Ddl(DdlStatement::CreateIndex(Box::new( + parse_create_index(parser, false, false)?, + )))); } if parser.at_keyword(Keyword::Type) { let _ = parser.next(); diff --git a/crates/iridium_core/src/parser/parse/statements/ddl.rs b/crates/iridium_core/src/parser/parse/statements/ddl.rs index a38bbcb..e66904b 100644 --- a/crates/iridium_core/src/parser/parse/statements/ddl.rs +++ b/crates/iridium_core/src/parser/parse/statements/ddl.rs @@ -59,6 +59,8 @@ pub fn parse_column_def(parser: &mut Parser) -> ParseResult { let mut check_constraint_name = None; let mut computed_expr = None; let mut foreign_key = None; + let mut collation = None; + let mut is_clustered = false; while let Some(Token::Keyword(k)) = parser.peek() { match *k { @@ -94,10 +96,24 @@ pub fn parse_column_def(parser: &mut Parser) -> ParseResult { Keyword::Primary => { let _ = parser.next(); parser.expect_keyword(Keyword::Key)?; + if parser.at_keyword(Keyword::Clustered) { + let _ = parser.next(); + is_clustered = true; + } else if parser.at_keyword(Keyword::Nonclustered) { + let _ = parser.next(); + is_clustered = false; + } is_primary_key = true; } Keyword::Unique => { let _ = parser.next(); + if parser.at_keyword(Keyword::Clustered) { + let _ = parser.next(); + is_clustered = true; + } else if parser.at_keyword(Keyword::Nonclustered) { + let _ = parser.next(); + is_clustered = false; + } is_unique = true; } Keyword::Default => { @@ -158,6 +174,15 @@ pub fn parse_column_def(parser: &mut Parser) -> ParseResult { let _ = parser.next(); computed_expr = Some(crate::parser::parse::expressions::parse_expr(parser)?); } + Keyword::Collate => { + let _ = parser.next(); + let collation_name = match parser.next() { + Some(Token::Identifier(id)) => id.clone(), + Some(Token::Keyword(kw)) => kw.as_ref().to_string(), + _ => return parser.backtrack(Expected::Description("collation name")), + }; + collation = Some(collation_name); + } _ => break, } } @@ -176,6 +201,8 @@ pub fn parse_column_def(parser: &mut Parser) -> ParseResult { check_constraint_name, computed_expr, foreign_key, + collation, + is_clustered, }) } @@ -235,26 +262,42 @@ pub fn parse_table_constraint(parser: &mut Parser) -> ParseResult { parser.expect_keyword(Keyword::Key)?; + let mut is_clustered = false; + if parser.at_keyword(Keyword::Clustered) { + let _ = parser.next(); + is_clustered = true; + } else if parser.at_keyword(Keyword::Nonclustered) { + let _ = parser.next(); + is_clustered = false; + } parser.expect_lparen()?; let columns = - crate::parser::parse::expressions::parse_comma_list(parser, |p| match p.next() { - Some(Token::Identifier(id)) => Ok(id.clone()), - Some(Token::Keyword(kw)) => Ok(kw.as_ref().to_string()), - _ => p.backtrack(Expected::Description("column name")), - })?; + crate::parser::parse::expressions::parse_comma_list(parser, parse_index_column)?; parser.expect_rparen()?; - Ok(TableConstraint::PrimaryKey { name, columns }) + Ok(TableConstraint::PrimaryKey { + name, + columns, + is_clustered, + }) } Keyword::Unique => { + let mut is_clustered = false; + if parser.at_keyword(Keyword::Clustered) { + let _ = parser.next(); + is_clustered = true; + } else if parser.at_keyword(Keyword::Nonclustered) { + let _ = parser.next(); + is_clustered = false; + } parser.expect_lparen()?; let columns = - crate::parser::parse::expressions::parse_comma_list(parser, |p| match p.next() { - Some(Token::Identifier(id)) => Ok(id.clone()), - Some(Token::Keyword(kw)) => Ok(kw.as_ref().to_string()), - _ => p.backtrack(Expected::Description("column name")), - })?; + crate::parser::parse::expressions::parse_comma_list(parser, parse_index_column)?; parser.expect_rparen()?; - Ok(TableConstraint::Unique { name, columns }) + Ok(TableConstraint::Unique { + name, + columns, + is_clustered, + }) } Keyword::Foreign => { parser.expect_keyword(Keyword::Key)?; @@ -329,23 +372,71 @@ pub fn parse_table_constraint(parser: &mut Parser) -> ParseResult ParseResult { +pub fn parse_create_index( + parser: &mut Parser, + is_unique: bool, + is_clustered: bool, +) -> ParseResult { let name = super::parse_multipart_name(parser)?; parser.expect_keyword(Keyword::On)?; let table = super::parse_multipart_name(parser)?; parser.expect_lparen()?; - let columns = - crate::parser::parse::expressions::parse_comma_list(parser, |p| match p.next() { - Some(Token::Identifier(id)) => Ok(id.clone()), - Some(Token::Keyword(kw)) => Ok(kw.as_ref().to_string()), - _ => p.backtrack(Expected::Description("column name")), - })?; + let columns = crate::parser::parse::expressions::parse_comma_list(parser, parse_index_column)?; parser.expect_rparen()?; - Ok(Statement::Ddl(DdlStatement::CreateIndex { + + let mut options = Vec::new(); + if parser.at_keyword(Keyword::With) { + let _ = parser.next(); + parser.expect_lparen()?; + options = crate::parser::parse::expressions::parse_comma_list(parser, parse_index_option)?; + parser.expect_rparen()?; + } + + Ok(CreateIndexStmt { name, table, + is_unique, + is_clustered, columns, - })) + options, + }) +} + +fn parse_index_column(parser: &mut Parser) -> ParseResult { + let name = match parser.next() { + Some(Token::Identifier(id)) => id.clone(), + Some(Token::Keyword(kw)) => kw.as_ref().to_string(), + _ => return parser.backtrack(Expected::Description("column name")), + }; + let mut is_desc = false; + if parser.at_keyword(Keyword::Desc) { + let _ = parser.next(); + is_desc = true; + } else if parser.at_keyword(Keyword::Asc) { + let _ = parser.next(); + is_desc = false; + } + Ok(IndexColumn { name, is_desc }) +} + +fn parse_index_option(parser: &mut Parser) -> ParseResult { + if parser.at_keyword(Keyword::Fillfactor) { + let _ = parser.next(); + if let Some(Token::Operator(op)) = parser.next() { + if op != "=" { + return parser.backtrack(Expected::Description("=")); + } + } else { + return parser.backtrack(Expected::Description("=")); + } + if let Some(Token::Number { value: n, .. }) = parser.next() { + Ok(IndexOption::FillFactor(*n as u8)) + } else { + parser.backtrack(Expected::Description("number")) + } + } else { + parser.backtrack(Expected::Description("index option")) + } } pub fn parse_create_type(parser: &mut Parser) -> ParseResult { @@ -481,17 +572,41 @@ pub fn parse_alter_table_add_constraint(parser: &mut Parser) -> ParseResult Ok(id.clone()), - Some(Token::Keyword(kw)) => Ok(kw.as_ref().to_string()), - _ => p.backtrack(Expected::Description("column name")), - })?; + crate::parser::parse::expressions::parse_comma_list(parser, parse_index_column)?; parser.expect_rparen()?; TableConstraint::PrimaryKey { name: Some(constraint_name), columns, + is_clustered, + } + } else if parser.at_keyword(Keyword::Unique) { + let _ = parser.next(); + let mut is_clustered = false; + if parser.at_keyword(Keyword::Clustered) { + let _ = parser.next(); + is_clustered = true; + } else if parser.at_keyword(Keyword::Nonclustered) { + let _ = parser.next(); + is_clustered = false; + } + parser.expect_lparen()?; + let columns = + crate::parser::parse::expressions::parse_comma_list(parser, parse_index_column)?; + parser.expect_rparen()?; + TableConstraint::Unique { + name: Some(constraint_name), + columns, + is_clustered, } } else if parser.at_keyword(Keyword::Foreign) { let _ = parser.next(); @@ -550,20 +665,6 @@ pub fn parse_alter_table_add_constraint(parser: &mut Parser) -> ParseResult Ok(id.clone()), - Some(Token::Keyword(kw)) => Ok(kw.as_ref().to_string()), - _ => p.backtrack(Expected::Description("column name")), - })?; - parser.expect_rparen()?; - TableConstraint::Unique { - name: Some(constraint_name), - columns, - } } else { return parser.backtrack(Expected::Description("constraint type")); }; diff --git a/crates/iridium_core/src/parser/parse/statements/dml.rs b/crates/iridium_core/src/parser/parse/statements/dml.rs index 13ba339..bc82869 100644 --- a/crates/iridium_core/src/parser/parse/statements/dml.rs +++ b/crates/iridium_core/src/parser/parse/statements/dml.rs @@ -524,6 +524,8 @@ pub fn parse_insert_bulk(parser: &mut Parser) -> ParseResult { check_constraint_name: None, computed_expr: None, foreign_key: None, + collation: None, + is_clustered: false, }) })?; parser.expect_rparen()?; diff --git a/crates/iridium_core/src/parser/token/keyword.rs b/crates/iridium_core/src/parser/token/keyword.rs index 0062c59..2fd6db1 100644 --- a/crates/iridium_core/src/parser/token/keyword.rs +++ b/crates/iridium_core/src/parser/token/keyword.rs @@ -69,6 +69,7 @@ define_keywords! { Cast => "CAST", Convert => "CONVERT", Like => "LIKE", + Escape => "ESCAPE", Top => "TOP", Distinct => "DISTINCT", Insert => "INSERT", @@ -101,6 +102,8 @@ define_keywords! { Row => "ROW", TryCast => "TRY_CAST", TryConvert => "TRY_CONVERT", + Coalesce => "COALESCE", + Nullif => "NULLIF", Offset => "OFFSET", Rows => "ROWS", Fetch => "FETCH", @@ -139,6 +142,9 @@ define_keywords! { Function => "FUNCTION", Trigger => "TRIGGER", Index => "INDEX", + Clustered => "CLUSTERED", + Nonclustered => "NONCLUSTERED", + Fillfactor => "FILLFACTOR", Schema => "SCHEMA", Type => "TYPE", Column => "COLUMN", @@ -220,13 +226,17 @@ define_keywords! { Decimal => "DECIMAL", Numeric => "NUMERIC", Real => "REAL", + Precision => "PRECISION", Money => "MONEY", SmallMoney => "SMALLMONEY", // Data types - character Char => "CHAR", + Character => "CHARACTER", NChar => "NCHAR", Varchar => "VARCHAR", + Varying => "VARYING", + National => "NATIONAL", Nvarchar => "NVARCHAR", Text => "TEXT", NText => "NTEXT", @@ -269,6 +279,7 @@ define_keywords! { Relative => "RELATIVE", Routine => "ROUTINE", Collation => "COLLATION", + Collate => "COLLATE", // Isolation level Isolation => "ISOLATION", @@ -291,6 +302,10 @@ define_keywords! { // SET options NoCount => "NOCOUNT", + Rowcount => "ROWCOUNT", + Textsize => "TEXTSIZE", + IdentityInsert => "IDENTITY_INSERT", + IdentityCol => "IDENTITYCOL", ContextInfo => "CONTEXT_INFO", // DML pseudo-tables diff --git a/crates/iridium_core/tests/sql_server_2025_parity.rs b/crates/iridium_core/tests/sql_server_2025_parity.rs index 35b184d..4f19393 100644 --- a/crates/iridium_core/tests/sql_server_2025_parity.rs +++ b/crates/iridium_core/tests/sql_server_2025_parity.rs @@ -51,3 +51,51 @@ fn test_system_procedures_2025() { assert_eq!(res.columns[0], "last_run"); assert_eq!(res.rows.len(), 1); } + +#[test] +fn test_identity_insert_and_col() { + let engine = Engine::new(); + engine.exec("CREATE TABLE IdTest (Id INT IDENTITY(1,1), Val VARCHAR(10))").unwrap(); + + // Normal insert + engine.exec("INSERT INTO IdTest (Val) VALUES ('a')").unwrap(); + let res = engine.query("SELECT IDENTITYCOL, Val FROM IdTest").unwrap(); + assert_eq!(res.rows[0][0].to_string_value(), "1"); + assert_eq!(res.rows[0][1].to_string_value(), "a"); + + // SET IDENTITY_INSERT ON + engine.exec("SET IDENTITY_INSERT IdTest ON").unwrap(); + engine.exec("INSERT INTO IdTest (Id, Val) VALUES (10, 'b')").unwrap(); + + let res = engine.query("SELECT Id, Val FROM IdTest WHERE Id = 10").unwrap(); + assert_eq!(res.rows[0][0].to_string_value(), "10"); + + // Verify qualified IDENTITYCOL + let res = engine.query("SELECT T.IDENTITYCOL FROM IdTest T WHERE T.Id = 10").unwrap(); + assert_eq!(res.rows[0][0].to_string_value(), "10"); + + // SET IDENTITY_INSERT OFF + engine.exec("SET IDENTITY_INSERT IdTest OFF").unwrap(); + let res = engine.exec("INSERT INTO IdTest (Id, Val) VALUES (20, 'c')"); + assert!(res.is_err()); +} + +#[test] +fn test_logic_functions_parity() { + let engine = Engine::new(); + let res = engine.query("SELECT COALESCE(NULL, 1, 2), NULLIF(1, 1), NULLIF(1, 2)").unwrap(); + assert_eq!(res.rows[0][0].to_string_value(), "1"); + assert!(res.rows[0][1].is_null()); + assert_eq!(res.rows[0][2].to_string_value(), "1"); +} + +#[test] +fn test_like_escape_parity() { + let engine = Engine::new(); + engine.exec("CREATE TABLE LikeTest (Pat VARCHAR(10))").unwrap(); + engine.exec("INSERT INTO LikeTest VALUES ('10%'), ('100')").unwrap(); + + let res = engine.query("SELECT Pat FROM LikeTest WHERE Pat LIKE '10!%' ESCAPE '!'").unwrap(); + assert_eq!(res.rows.len(), 1); + assert_eq!(res.rows[0][0].to_string_value(), "10%"); +} diff --git a/docs/sql-server-2025-implementation-status.md b/docs/sql-server-2025-implementation-status.md index 5670300..04deddb 100644 --- a/docs/sql-server-2025-implementation-status.md +++ b/docs/sql-server-2025-implementation-status.md @@ -26,9 +26,9 @@ This document tracks the implementation status of SQL Server 2025 features in Ir | CHECK | ✅ Implemented | | CHECKPOINT | ✅ Implemented | | CLOSE | ✅ Implemented | -| CLUSTERED | ❌ Pending | -| COALESCE | ❌ Pending | -| COLLATE | ❌ Pending | +| CLUSTERED | ✅ Implemented | +| COALESCE | ✅ Implemented | +| COLLATE | ✅ Implemented | | COLUMN | ✅ Implemented | | COMMIT | ✅ Implemented | | COMPUTE | ❌ Pending | @@ -56,13 +56,13 @@ This document tracks the implementation status of SQL Server 2025 features in Ir | DISK | ❌ Pending | | DISTINCT | ✅ Implemented | | DISTRIBUTED | ✅ Implemented | -| DOUBLE | ❌ Pending | +| DOUBLE | ✅ Implemented | | DROP | ✅ Implemented | | DUMP | ❌ Pending | | ELSE | ✅ Implemented | | END | ✅ Implemented | | ERRLVL | ❌ Pending | -| ESCAPE | ❌ Pending | +| ESCAPE | ✅ Implemented | | EXCEPT | ✅ Implemented | | EXEC | ✅ Implemented | | EXECUTE | ✅ Implemented | @@ -71,7 +71,7 @@ This document tracks the implementation status of SQL Server 2025 features in Ir | EXTERNAL | ❌ Pending | | FETCH | ✅ Implemented | | FILE | ❌ Pending | -| FILLFACTOR | ❌ Pending | +| FILLFACTOR | ✅ Implemented | | FOR | ✅ Implemented | | FOREIGN | ✅ Implemented | | FREETEXT | ❌ Pending | @@ -85,8 +85,8 @@ This document tracks the implementation status of SQL Server 2025 features in Ir | HAVING | ✅ Implemented | | HOLDLOCK | ✅ Implemented | | IDENTITY | ✅ Implemented | -| IDENTITYCOL | ❌ Pending | -| IDENTITY_INSERT | ❌ Pending | +| IDENTITYCOL | ✅ Implemented | +| IDENTITY_INSERT | ✅ Implemented | | IF | ✅ Implemented | | IN | ✅ Implemented | | INDEX | ✅ Implemented | @@ -103,12 +103,12 @@ This document tracks the implementation status of SQL Server 2025 features in Ir | LINENO | ❌ Pending | | LOAD | ❌ Pending | | MERGE | ✅ Implemented | -| NATIONAL | ❌ Pending | +| NATIONAL | ✅ Implemented | | NOCHECK | ❌ Pending | -| NONCLUSTERED | ❌ Pending | +| NONCLUSTERED | ✅ Implemented | | NOT | ✅ Implemented | | NULL | ✅ Implemented | -| NULLIF | ❌ Pending | +| NULLIF | ✅ Implemented | | OF | ✅ Implemented | | OFF | ✅ Implemented | | OFFSETS | ❌ Pending | @@ -126,7 +126,7 @@ This document tracks the implementation status of SQL Server 2025 features in Ir | PERCENT | ❌ Pending | | PIVOT | ✅ Implemented | | PLAN | ❌ Pending | -| PRECISION | ❌ Pending | +| PRECISION | ✅ Implemented | | PRIMARY | ✅ Implemented | | PRINT | ✅ Implemented | | PROC | ✅ Implemented | @@ -145,7 +145,7 @@ This document tracks the implementation status of SQL Server 2025 features in Ir | REVOKE | ❌ Pending | | RIGHT | ✅ Implemented | | ROLLBACK | ✅ Implemented | -| ROWCOUNT | ❌ Pending | +| ROWCOUNT | ✅ Implemented | | ROWGUIDCOL | ❌ Pending | | RULE | ❌ Pending | | SAVE | ✅ Implemented | @@ -164,7 +164,7 @@ This document tracks the implementation status of SQL Server 2025 features in Ir | SYSTEM_USER | ✅ Implemented | | TABLE | ✅ Implemented | | TABLESAMPLE | ❌ Pending | -| TEXTSIZE | ❌ Pending | +| TEXTSIZE | ✅ Implemented | | THEN | ✅ Implemented | | TO | ❌ Pending | | TOP | ✅ Implemented | @@ -182,7 +182,7 @@ This document tracks the implementation status of SQL Server 2025 features in Ir | USE | ✅ Implemented | | USER | ✅ Implemented | | VALUES | ✅ Implemented | -| VARYING | ❌ Pending | +| VARYING | ✅ Implemented | | VIEW | ✅ Implemented | | WAITFOR | ❌ Pending | | WHEN | ✅ Implemented | @@ -192,7 +192,7 @@ This document tracks the implementation status of SQL Server 2025 features in Ir | WITHIN | ✅ Implemented | | WRITETEXT | ❌ Pending | -**Summary:** 118/185 (63.8%) +**Summary:** 134/185 (72.4%) ## System Stored Procedures (`sp_*`)