Skip to main content

toolu_orm_core/fts5/
builder.rs

1//! Builder that turns columns and options into an FTS5 [`TableDef`].
2
3use crate::column::{ColumnDef, ColumnType};
4use crate::table::{TableDef, TableKind};
5
6use super::options::Fts5Options;
7
8/// The SQLite module name for full-text search tables.
9pub const FTS5_MODULE: &str = "fts5";
10
11/// Assembles `CREATE VIRTUAL TABLE … USING fts5(…)` without hand-writing the
12/// module arguments.
13///
14/// ```
15/// use toolu_orm_core::column::ColumnType;
16/// use toolu_orm_core::fts5::Fts5Table;
17///
18/// let table = Fts5Table::new("memory_fts")
19///   .unindexed_column("memory_id", ColumnType::Text)
20///   .column("body", ColumnType::Text)
21///   .tokenize("porter unicode61")
22///   .build();
23///
24/// assert_eq!(table.kind.module(), Some("fts5"));
25/// assert_eq!(table.kind.args()[0], "\"memory_id\" UNINDEXED");
26/// ```
27#[derive(Debug, Clone, Default)]
28pub struct Fts5Table {
29  name: String,
30  columns: Vec<ColumnDef>,
31  options: Fts5Options,
32}
33
34impl Fts5Table {
35  #[must_use]
36  pub fn new(name: impl Into<String>) -> Self {
37    Self {
38      name: name.into(),
39      columns: Vec::new(),
40      options: Fts5Options::default(),
41    }
42  }
43
44  /// A searchable column. FTS5 stores every column as text; `column_type` is
45  /// kept as metadata for the generated typed columns and is never rendered
46  /// into the DDL.
47  #[must_use]
48  pub fn column(self, name: impl Into<String>, column_type: ColumnType) -> Self {
49    self.push_column(name.into(), column_type, false)
50  }
51
52  /// A column stored but not indexed (`UNINDEXED`), so it is returned by
53  /// queries but never matched by `MATCH`.
54  #[must_use]
55  pub fn unindexed_column(self, name: impl Into<String>, column_type: ColumnType) -> Self {
56    self.push_column(name.into(), column_type, true)
57  }
58
59  /// The tokenizer chain, e.g. `porter unicode61 remove_diacritics 2`.
60  #[must_use]
61  pub fn tokenize(mut self, spec: impl Into<String>) -> Self {
62    self.options.tokenize = Some(spec.into());
63    self
64  }
65
66  /// Prefix index sizes, e.g. `2 3`.
67  #[must_use]
68  pub fn prefix(mut self, spec: impl Into<String>) -> Self {
69    self.options.prefix = Some(spec.into());
70    self
71  }
72
73  /// External content table; the empty string makes the table contentless.
74  #[must_use]
75  pub fn content(mut self, table: impl Into<String>) -> Self {
76    self.options.content = Some(table.into());
77    self
78  }
79
80  /// The rowid column of the external content table.
81  #[must_use]
82  pub fn content_rowid(mut self, column: impl Into<String>) -> Self {
83    self.options.content_rowid = Some(column.into());
84    self
85  }
86
87  /// `columnsize = 0` drops the per-column size index.
88  #[must_use]
89  pub fn columnsize(mut self, value: u8) -> Self {
90    self.options.columnsize = Some(value);
91    self
92  }
93
94  /// Detail level: `full`, `column`, or `none`.
95  #[must_use]
96  pub fn detail(mut self, value: impl Into<String>) -> Self {
97    self.options.detail = Some(value.into());
98    self
99  }
100
101  /// The finished definition, with the module arguments rendered into
102  /// [`TableKind::Virtual`].
103  #[must_use]
104  pub fn build(self) -> TableDef {
105    let mut args: Vec<String> = self.columns.iter().map(column_arg).collect();
106    args.extend(self.options.render());
107    TableDef {
108      name: self.name,
109      columns: self.columns,
110      indexes: Vec::new(),
111      strict: false,
112      kind: TableKind::virtual_table(FTS5_MODULE, args),
113    }
114  }
115
116  fn push_column(mut self, name: String, column_type: ColumnType, unindexed: bool) -> Self {
117    self.columns.push(ColumnDef {
118      name,
119      column_type,
120      primary_key: false,
121      not_null: false,
122      default: None,
123      unique: false,
124      references: None,
125      on_delete: None,
126      on_update: None,
127      check: None,
128      unindexed,
129    });
130    self
131  }
132}
133
134fn column_arg(column: &ColumnDef) -> String {
135  if column.unindexed {
136    format!("\"{}\" UNINDEXED", column.name)
137  } else {
138    format!("\"{}\"", column.name)
139  }
140}