Skip to content

Commit ff7012c

Browse files
authored
Support for creating functional indexes in PostgreSQL and MySQL (#869)
* feat: implement functional index for MySQL and Postgres query builders * refactor: refactor index column handling in IndexBuilder trait and implementations * test: remove unused table creation statement in create_9 test * docs: add example for functional index in create.rs * refactor: update name method in IndexColumn to handle Expr variant
1 parent 15b1506 commit ff7012c

File tree

9 files changed

+316
-17
lines changed

9 files changed

+316
-17
lines changed

src/backend/index_builder.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,23 @@ pub trait IndexBuilder: QuotedBuilder + TableRefBuilder {
5656
}
5757
}
5858

59+
#[doc(hidden)]
60+
/// Write the index column with table column.
61+
fn prepare_index_column_with_table_column(
62+
&self,
63+
column: &IndexColumnTableColumn,
64+
sql: &mut dyn SqlWriter,
65+
) {
66+
column.name.prepare(sql.as_writer(), self.quote());
67+
self.write_column_index_prefix(&column.prefix, sql);
68+
if let Some(order) = &column.order {
69+
match order {
70+
IndexOrder::Asc => write!(sql, " ASC").unwrap(),
71+
IndexOrder::Desc => write!(sql, " DESC").unwrap(),
72+
}
73+
}
74+
}
75+
5976
#[doc(hidden)]
6077
/// Write the column index prefix.
6178
fn prepare_index_columns(&self, columns: &[IndexColumn], sql: &mut dyn SqlWriter) {
@@ -64,13 +81,11 @@ pub trait IndexBuilder: QuotedBuilder + TableRefBuilder {
6481
if !first {
6582
write!(sql, ", ").unwrap();
6683
}
67-
col.name.prepare(sql.as_writer(), self.quote());
68-
self.write_column_index_prefix(&col.prefix, sql);
69-
if let Some(order) = &col.order {
70-
match order {
71-
IndexOrder::Asc => write!(sql, " ASC").unwrap(),
72-
IndexOrder::Desc => write!(sql, " DESC").unwrap(),
84+
match col {
85+
IndexColumn::TableColumn(column) => {
86+
self.prepare_index_column_with_table_column(column, sql);
7387
}
88+
IndexColumn::Expr(_) => panic!("Not supported"),
7489
}
7590
false
7691
});

src/backend/mysql/index.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,31 @@ impl IndexBuilder for MysqlQueryBuilder {
117117
write!(sql, "FULLTEXT ").unwrap();
118118
}
119119
}
120+
121+
fn prepare_index_columns(&self, columns: &[IndexColumn], sql: &mut dyn SqlWriter) {
122+
write!(sql, "(").unwrap();
123+
columns.iter().fold(true, |first, col| {
124+
if !first {
125+
write!(sql, ", ").unwrap();
126+
}
127+
match col {
128+
IndexColumn::TableColumn(column) => {
129+
self.prepare_index_column_with_table_column(column, sql);
130+
}
131+
IndexColumn::Expr(column) => {
132+
write!(sql, "(").unwrap();
133+
self.prepare_simple_expr(&column.expr, sql);
134+
write!(sql, ")").unwrap();
135+
if let Some(order) = &column.order {
136+
match order {
137+
IndexOrder::Asc => write!(sql, " ASC").unwrap(),
138+
IndexOrder::Desc => write!(sql, " DESC").unwrap(),
139+
}
140+
}
141+
}
142+
}
143+
false
144+
});
145+
write!(sql, ")").unwrap();
146+
}
120147
}

src/backend/postgres/index.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,33 @@ impl IndexBuilder for PostgresQueryBuilder {
140140
}
141141
}
142142

143+
fn prepare_index_columns(&self, columns: &[IndexColumn], sql: &mut dyn SqlWriter) {
144+
write!(sql, "(").unwrap();
145+
columns.iter().fold(true, |first, col| {
146+
if !first {
147+
write!(sql, ", ").unwrap();
148+
}
149+
match col {
150+
IndexColumn::TableColumn(column) => {
151+
self.prepare_index_column_with_table_column(column, sql);
152+
}
153+
IndexColumn::Expr(column) => {
154+
write!(sql, "(").unwrap();
155+
self.prepare_simple_expr(&column.expr, sql);
156+
write!(sql, ")").unwrap();
157+
if let Some(order) = &column.order {
158+
match order {
159+
IndexOrder::Asc => write!(sql, " ASC").unwrap(),
160+
IndexOrder::Desc => write!(sql, " DESC").unwrap(),
161+
}
162+
}
163+
}
164+
}
165+
false
166+
});
167+
write!(sql, ")").unwrap();
168+
}
169+
143170
fn prepare_filter(&self, condition: &ConditionHolder, sql: &mut dyn SqlWriter) {
144171
self.prepare_condition(condition, "WHERE", sql);
145172
}

src/index/common.rs

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use crate::types::*;
1+
use crate::expr::SimpleExpr;
2+
use crate::{types::*, FunctionCall};
23

34
/// Specification of a table index
45
#[derive(Default, Debug, Clone)]
@@ -8,12 +9,33 @@ pub struct TableIndex {
89
}
910

1011
#[derive(Debug, Clone)]
11-
pub struct IndexColumn {
12+
pub enum IndexColumn {
13+
TableColumn(IndexColumnTableColumn),
14+
Expr(IndexColumnExpr),
15+
}
16+
17+
#[derive(Debug, Clone)]
18+
pub struct IndexColumnTableColumn {
1219
pub(crate) name: DynIden,
1320
pub(crate) prefix: Option<u32>,
1421
pub(crate) order: Option<IndexOrder>,
1522
}
1623

24+
#[derive(Debug, Clone)]
25+
pub struct IndexColumnExpr {
26+
pub(crate) expr: SimpleExpr,
27+
pub(crate) order: Option<IndexOrder>,
28+
}
29+
30+
impl IndexColumn {
31+
pub(crate) fn name(&self) -> Option<&DynIden> {
32+
match self {
33+
IndexColumn::TableColumn(IndexColumnTableColumn { name, .. }) => Some(name),
34+
IndexColumn::Expr(_) => None,
35+
}
36+
}
37+
}
38+
1739
#[derive(Debug, Clone)]
1840
pub enum IndexOrder {
1941
Asc,
@@ -35,11 +57,11 @@ where
3557
I: IntoIden,
3658
{
3759
fn into_index_column(self) -> IndexColumn {
38-
IndexColumn {
60+
IndexColumn::TableColumn(IndexColumnTableColumn {
3961
name: self.into_iden(),
4062
prefix: None,
4163
order: None,
42-
}
64+
})
4365
}
4466
}
4567

@@ -48,11 +70,11 @@ where
4870
I: IntoIden,
4971
{
5072
fn into_index_column(self) -> IndexColumn {
51-
IndexColumn {
73+
IndexColumn::TableColumn(IndexColumnTableColumn {
5274
name: self.0.into_iden(),
5375
prefix: Some(self.1),
5476
order: None,
55-
}
77+
})
5678
}
5779
}
5880

@@ -61,11 +83,11 @@ where
6183
I: IntoIden,
6284
{
6385
fn into_index_column(self) -> IndexColumn {
64-
IndexColumn {
86+
IndexColumn::TableColumn(IndexColumnTableColumn {
6587
name: self.0.into_iden(),
6688
prefix: None,
6789
order: Some(self.1),
68-
}
90+
})
6991
}
7092
}
7193

@@ -74,11 +96,47 @@ where
7496
I: IntoIden,
7597
{
7698
fn into_index_column(self) -> IndexColumn {
77-
IndexColumn {
99+
IndexColumn::TableColumn(IndexColumnTableColumn {
78100
name: self.0.into_iden(),
79101
prefix: Some(self.1),
80102
order: Some(self.2),
81-
}
103+
})
104+
}
105+
}
106+
107+
impl IntoIndexColumn for FunctionCall {
108+
fn into_index_column(self) -> IndexColumn {
109+
IndexColumn::Expr(IndexColumnExpr {
110+
expr: self.into(),
111+
order: None,
112+
})
113+
}
114+
}
115+
116+
impl IntoIndexColumn for (FunctionCall, IndexOrder) {
117+
fn into_index_column(self) -> IndexColumn {
118+
IndexColumn::Expr(IndexColumnExpr {
119+
expr: self.0.into(),
120+
order: Some(self.1),
121+
})
122+
}
123+
}
124+
125+
impl IntoIndexColumn for SimpleExpr {
126+
fn into_index_column(self) -> IndexColumn {
127+
IndexColumn::Expr(IndexColumnExpr {
128+
expr: self,
129+
order: None,
130+
})
131+
}
132+
}
133+
134+
impl IntoIndexColumn for (SimpleExpr, IndexOrder) {
135+
fn into_index_column(self) -> IndexColumn {
136+
IndexColumn::Expr(IndexColumnExpr {
137+
expr: self.0,
138+
order: Some(self.1),
139+
})
82140
}
83141
}
84142

@@ -106,7 +164,7 @@ impl TableIndex {
106164
pub fn get_column_names(&self) -> Vec<String> {
107165
self.columns
108166
.iter()
109-
.map(|col| col.name.to_string())
167+
.filter_map(|col| col.name().map(|name| name.to_string()))
110168
.collect()
111169
}
112170

src/index/create.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,22 @@ use super::common::*;
187187
/// r#"CREATE INDEX "idx-font-name-include-language" ON "font" ("name") INCLUDE ("language")"#
188188
/// )
189189
/// ```
190+
///
191+
/// Functional Index
192+
/// ```
193+
/// use sea_query::{tests_cfg::*, *};
194+
///
195+
/// let index = Index::create()
196+
/// .name("idx-character-area")
197+
/// .table(Character::Table)
198+
/// .col(Expr::col(Character::SizeH).mul(Expr::col(Character::SizeW)))
199+
/// .to_owned();
200+
///
201+
/// assert_eq!(
202+
/// index.to_string(PostgresQueryBuilder),
203+
/// r#"CREATE INDEX "idx-character-area" ON "character" (("size_h" * "size_w"))"#
204+
/// )
205+
/// ```
190206
#[derive(Default, Debug, Clone)]
191207
pub struct IndexCreateStatement {
192208
pub(crate) table: Option<TableRef>,

tests/mysql/index.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,35 @@ fn create_4() {
5353
);
5454
}
5555

56+
#[test]
57+
fn create_5() {
58+
assert_eq!(
59+
Index::create()
60+
.name("idx-character-area")
61+
.table(Character::Table)
62+
.col(Expr::col(Character::SizeH).mul(Expr::col(Character::SizeW)))
63+
.to_string(MysqlQueryBuilder),
64+
"CREATE INDEX `idx-character-area` ON `character` ((`size_h` * `size_w`))"
65+
)
66+
}
67+
68+
#[test]
69+
fn create_6() {
70+
assert_eq!(
71+
Index::create()
72+
.name("idx-character-character-area-desc-created_at")
73+
.table(Character::Table)
74+
.col(Func::upper(Expr::col(Character::Character)))
75+
.col((
76+
Expr::col(Character::SizeH).mul(Expr::col(Character::SizeW)),
77+
IndexOrder::Desc,
78+
))
79+
.col(Character::CreatedAt)
80+
.to_string(MysqlQueryBuilder),
81+
"CREATE INDEX `idx-character-character-area-desc-created_at` ON `character` ((UPPER(`character`)), (`size_h` * `size_w`) DESC, `created_at`)"
82+
)
83+
}
84+
5685
#[test]
5786
fn drop_1() {
5887
assert_eq!(

tests/mysql/table.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,60 @@ fn create_11() {
264264
);
265265
}
266266

267+
#[test]
268+
fn create_12() {
269+
assert_eq!(
270+
Table::create()
271+
.table(Char::Table)
272+
.if_not_exists()
273+
.col(
274+
ColumnDef::new(Char::Id)
275+
.integer()
276+
.not_null()
277+
.auto_increment()
278+
.primary_key(),
279+
)
280+
.col(ColumnDef::new(Char::FontSize).integer().not_null())
281+
.col(ColumnDef::new(Char::Character).string_len(255).not_null())
282+
.col(ColumnDef::new(Char::SizeW).unsigned().not_null())
283+
.col(ColumnDef::new(Char::SizeH).unsigned().not_null())
284+
.col(
285+
ColumnDef::new(Char::FontId)
286+
.integer()
287+
.default(Value::Int(None)),
288+
)
289+
.col(
290+
ColumnDef::new(Char::CreatedAt)
291+
.timestamp()
292+
.default(Expr::current_timestamp())
293+
.not_null(),
294+
)
295+
.index(
296+
Index::create()
297+
.name("idx-character-area")
298+
.table(Character::Table)
299+
.col(Expr::col(Character::SizeH).mul(Expr::col(Character::SizeW))),
300+
)
301+
.engine("InnoDB")
302+
.character_set("utf8mb4")
303+
.collate("utf8mb4_unicode_ci")
304+
.to_string(MysqlQueryBuilder),
305+
[
306+
"CREATE TABLE IF NOT EXISTS `character` (",
307+
"`id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,",
308+
"`font_size` int NOT NULL,",
309+
"`character` varchar(255) NOT NULL,",
310+
"`size_w` int UNSIGNED NOT NULL,",
311+
"`size_h` int UNSIGNED NOT NULL,",
312+
"`font_id` int DEFAULT NULL,",
313+
"`created_at` timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,",
314+
"KEY `idx-character-area` ((`size_h` * `size_w`))",
315+
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
316+
]
317+
.join(" ")
318+
);
319+
}
320+
267321
#[test]
268322
fn drop_1() {
269323
assert_eq!(

0 commit comments

Comments
 (0)