Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Alter column fulltext option #4637

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ etcd-client = { version = "0.13" }
fst = "0.4.7"
futures = "0.3"
futures-util = "0.3"
greptime-proto = { git = "https://github.com/GreptimeTeam/greptime-proto.git", rev = "c437b55725b7f5224fe9d46db21072b4a682ee4b" }
greptime-proto = { git = "https://github.com/irenjj/greptime-proto.git", rev = "6df0c7ba6d0c9acfcf155483d19889ffed3ef99d" }
humantime = "2.1"
humantime-serde = "1.1"
itertools = "0.10"
Expand Down
11 changes: 9 additions & 2 deletions src/common/grpc-expr/src/alter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use api::helper::ColumnDataTypeWrapper;
use api::v1::add_column_location::LocationType;
use api::v1::alter_expr::Kind;
use api::v1::{
column_def, AddColumnLocation as Location, AlterExpr, ChangeColumnTypes, CreateTableExpr,
DropColumns, RenameTable, SemanticType,
column_def, AddColumnLocation as Location, AlterExpr, ChangeColumnTypes, ChangeFulltext,
CreateTableExpr, DropColumns, RenameTable, SemanticType,
};
use common_query::AddColumnLocation;
use datatypes::schema::{ColumnSchema, RawSchema};
Expand Down Expand Up @@ -92,6 +92,13 @@ pub fn alter_expr_to_request(table_id: TableId, expr: AlterExpr) -> Result<Alter
Kind::RenameTable(RenameTable { new_table_name }) => {
AlterKind::RenameTable { new_table_name }
}
Kind::ChangeFulltext(ChangeFulltext {
column_name,
options,
}) => AlterKind::ChangeFulltext {
column_name,
options,
},
};

let request = AlterTableRequest {
Expand Down
1 change: 1 addition & 0 deletions src/common/meta/src/ddl/alter_table/region_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ fn create_proto_alter_kind(
})))
}
Kind::RenameTable(_) => Ok(None),
Kind::ChangeFulltext(x) => Ok(Some(alter_request::Kind::ChangeFulltext(x.clone()))),
}
}

Expand Down
1 change: 1 addition & 0 deletions src/common/meta/src/ddl/alter_table/update_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ impl AlterTableProcedure {
new_info.name = new_table_name.to_string();
}
AlterKind::DropColumns { .. } | AlterKind::ChangeColumnTypes { .. } => {}
AlterKind::ChangeFulltext { .. } => {}
}

Ok(new_info)
Expand Down
78 changes: 78 additions & 0 deletions src/datatypes/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ pub use crate::schema::column_schema::{
pub use crate::schema::constraint::ColumnDefaultConstraint;
pub use crate::schema::raw::RawSchema;

const COLUMN_FULLTEXT_OPT_KEY_ANALYZER: &str = "analyzer";
const COLUMN_FULLTEXT_OPT_KEY_CASE_SENSITIVE: &str = "case_sensitive";

/// Key used to store version number of the schema in metadata.
pub const VERSION_KEY: &str = "greptime:version";

Expand Down Expand Up @@ -300,6 +303,72 @@ fn validate_timestamp_index(column_schemas: &[ColumnSchema], timestamp_index: us
Ok(())
}

/// Parses the provided options to configure full-text search behavior.
///
/// This function checks for specific keys in the provided `HashMap`:
/// - `COLUMN_FULLTEXT_OPT_KEY_ANALYZER`: Defines the analyzer to use (e.g., "english", "chinese").
/// - `COLUMN_FULLTEXT_OPT_KEY_CASE_SENSITIVE`: Defines whether the full-text search should be case-sensitive ("true" or "false").
///
/// If the provided options contain valid values for the full-text keys, a configured `FulltextOptions`
/// object is returned. If the options are invalid or missing, the function returns `None`.
///
/// # Parameters:
/// - `options`: A reference to a `HashMap<String, String>` containing the full-text options.
///
/// # Returns:
/// - `Some(FulltextOptions)` if valid options are provided.
/// - `None` if the options are invalid or do not contain full-text related keys.
pub fn parse_fulltext_options(options: &HashMap<String, String>) -> Option<FulltextOptions> {
let mut fulltext = FulltextOptions::default();
let mut is_fulltext_key_exist = false;
v0y4g3r marked this conversation as resolved.
Show resolved Hide resolved

// Check and parse the "analyzer" option
if let Some(analyzer) = options.get(COLUMN_FULLTEXT_OPT_KEY_ANALYZER) {
match analyzer.to_ascii_lowercase().as_str() {
"english" => {
fulltext.enable = true;
fulltext.analyzer = FulltextAnalyzer::English;
}
"chinese" => {
fulltext.enable = true;
fulltext.analyzer = FulltextAnalyzer::Chinese;
}
_ => {
// If the analyzer is invalid, return None to indicate failure
return None;
}
}
is_fulltext_key_exist = true;
}

// Check and parse the "case_sensitive" option
if let Some(case_sensitive) = options.get(COLUMN_FULLTEXT_OPT_KEY_CASE_SENSITIVE) {
match case_sensitive.to_ascii_lowercase().as_str() {
"true" => {
fulltext.enable = true;
fulltext.case_sensitive = true;
}
"false" => {
fulltext.enable = true;
fulltext.case_sensitive = false;
}
_ => {
// If case sensitivity is invalid, return None
return None;
}
}
is_fulltext_key_exist = true;
}

// If any fulltext-related key exists, return the constructed FulltextOptions
if is_fulltext_key_exist {
Some(fulltext)
} else {
// Return None if no valid fulltext keys are found
None
}
}

pub type SchemaRef = Arc<Schema>;

impl TryFrom<Arc<ArrowSchema>> for Schema {
Expand Down Expand Up @@ -484,4 +553,13 @@ mod tests {
.build()
.is_err());
}

#[test]
fn test_validate_fulltext_options() {
let options = HashMap::from([(String::from("analyzer"), String::from("English"))]);
assert!(parse_fulltext_options(&options).is_some());

let options = HashMap::from([(String::from("analyzer"), String::from("Chinglish"))]);
assert!(parse_fulltext_options(&options).is_some());
}
}
15 changes: 11 additions & 4 deletions src/datatypes/src/schema/column_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,12 +256,19 @@ impl ColumnSchema {
}

pub fn with_fulltext_options(mut self, options: FulltextOptions) -> Result<Self> {
self.metadata.insert(
FULLTEXT_KEY.to_string(),
serde_json::to_string(&options).context(error::SerializeSnafu)?,
);
self.set_fulltext_options(options)?;
Ok(self)
}

pub fn set_fulltext_options(&mut self, options: FulltextOptions) -> Result<()> {
if self.data_type == ConcreteDataType::string_datatype() {
v0y4g3r marked this conversation as resolved.
Show resolved Hide resolved
self.metadata.insert(
FULLTEXT_KEY.to_string(),
serde_json::to_string(&options).context(error::SerializeSnafu)?,
);
}
Ok(())
}
}

impl TryFrom<&Field> for ColumnSchema {
Expand Down
13 changes: 10 additions & 3 deletions src/operator/src/expr_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ use api::helper::ColumnDataTypeWrapper;
use api::v1::alter_expr::Kind;
use api::v1::column_def::options_from_column_schema;
use api::v1::{
AddColumn, AddColumns, AlterExpr, ChangeColumnType, ChangeColumnTypes, ColumnDataType,
ColumnDataTypeExtension, CreateFlowExpr, CreateTableExpr, CreateViewExpr, DropColumn,
DropColumns, ExpireAfter, RenameTable, SemanticType, TableName,
AddColumn, AddColumns, AlterExpr, ChangeColumnType, ChangeColumnTypes, ChangeFulltext,
ColumnDataType, ColumnDataTypeExtension, CreateFlowExpr, CreateTableExpr, CreateViewExpr,
DropColumn, DropColumns, ExpireAfter, RenameTable, SemanticType, TableName,
};
use common_error::ext::BoxedError;
use common_grpc_expr::util::ColumnExpr;
Expand Down Expand Up @@ -483,6 +483,13 @@ pub(crate) fn to_alter_expr(
AlterTableOperation::RenameTable { new_table_name } => Kind::RenameTable(RenameTable {
new_table_name: new_table_name.to_string(),
}),
AlterTableOperation::AlterColumnFulltext {
column_name,
options,
} => Kind::ChangeFulltext(ChangeFulltext {
column_name: column_name.value.to_string(),
options: options.clone().into_map(),
}),
};

Ok(AlterExpr {
Expand Down
74 changes: 66 additions & 8 deletions src/sql/src/parsers/alter_parser.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
Expand All @@ -12,6 +10,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::collections::HashMap;

use common_query::AddColumnLocation;
use snafu::ResultExt;
use sqlparser::keywords::Keyword;
Expand All @@ -22,6 +22,7 @@ use crate::error::{self, Result};
use crate::parser::ParserContext;
use crate::statements::alter::{AlterTable, AlterTableOperation};
use crate::statements::statement::Statement;
use crate::util::parse_option_string;

impl<'a> ParserContext<'a> {
pub(crate) fn parse_alter(&mut self) -> Result<Statement> {
Expand Down Expand Up @@ -74,13 +75,36 @@ impl<'a> ParserContext<'a> {
)));
}
} else if self.consume_token("MODIFY") {
let _ = self.parser.parse_keyword(Keyword::COLUMN);
if !self.parser.parse_keyword(Keyword::COLUMN) {
return Err(ParserError::ParserError(format!(
"expect keyword COLUMN after ALTER TABLE MODIFY, found {}",
self.parser.peek_token()
)));
}
let column_name = Self::canonicalize_identifier(self.parser.parse_identifier(false)?);
let target_type = self.parser.parse_data_type()?;

AlterTableOperation::ChangeColumnType {
column_name,
target_type,
if self.parser.parse_keyword(Keyword::SET) {
let _ = self.parser.parse_keyword(Keyword::FULLTEXT);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can check the parse result to avoid receiving unexpected input


let options = self
.parser
.parse_options(Keyword::WITH)?
.into_iter()
.map(parse_option_string)
.collect::<Result<HashMap<String, String>>>()
.unwrap();

AlterTableOperation::AlterColumnFulltext {
column_name,
options: options.into(),
}
} else {
let target_type = self.parser.parse_data_type()?;

AlterTableOperation::ChangeColumnType {
column_name,
target_type,
}
}
} else if self.parser.parse_keyword(Keyword::RENAME) {
let new_table_name_obj_raw = self.parse_object_name()?;
Expand All @@ -96,7 +120,7 @@ impl<'a> ParserContext<'a> {
AlterTableOperation::RenameTable { new_table_name }
} else {
return Err(ParserError::ParserError(format!(
"expect keyword ADD or DROP or MODIFY or RENAME after ALTER TABLE, found {}",
"expect keyword ADD or DROP or MODIFY or RENAME or SET after ALTER TABLE, found {}",
self.parser.peek_token()
)));
};
Expand Down Expand Up @@ -406,4 +430,38 @@ mod tests {
_ => unreachable!(),
}
}

#[test]
fn test_parse_alter_change_fulltext() {
let sql = "ALTER TABLE test MODIFY COLUMN message SET FULLTEXT WITH(analyzer = 'Chinese', case_sensitive = 'true');";
let mut result =
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
.unwrap();
assert_eq!(1, result.len());

let statement = result.remove(0);
assert_matches!(statement, Statement::Alter { .. });
match statement {
Statement::Alter(alter_table) => {
assert_eq!("test", alter_table.table_name().0[0].value);

let alter_operation = alter_table.alter_operation();
assert_matches!(
alter_operation,
AlterTableOperation::AlterColumnFulltext { .. }
);
match alter_operation {
AlterTableOperation::AlterColumnFulltext {
column_name,
options,
} => {
assert_eq!(column_name.value, r#"message"#);
assert_eq!(options.get("analyzer").unwrap(), "Chinese");
}
_ => unreachable!(),
}
}
_ => unreachable!(),
}
}
}
17 changes: 17 additions & 0 deletions src/sql/src/statements/alter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ use common_query::AddColumnLocation;
use sqlparser::ast::{ColumnDef, DataType, Ident, ObjectName, TableConstraint};
use sqlparser_derive::{Visit, VisitMut};

use crate::statements::OptionMap;

#[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut)]
pub struct AlterTable {
table_name: ObjectName,
Expand Down Expand Up @@ -71,6 +73,11 @@ pub enum AlterTableOperation {
DropColumn { name: Ident },
/// `RENAME <new_table_name>`
RenameTable { new_table_name: String },
/// `SET COLUMN <column_name> FULLTEXT WITH`
AlterColumnFulltext {
column_name: Ident,
options: OptionMap,
},
}

impl Display for AlterTableOperation {
Expand All @@ -97,6 +104,16 @@ impl Display for AlterTableOperation {
} => {
write!(f, r#"MODIFY COLUMN {column_name} {target_type}"#)
}
AlterTableOperation::AlterColumnFulltext {
column_name,
options,
} => {
write!(
f,
r#"SET COLUMN {column_name} FULLTEXT WITH {:?}"#,
options.to_str_map()
)
}
}
}
}
Expand Down
Loading