Skip to content

Commit fe9a538

Browse files
author
Brendan Elliott
committed
Add FullTextTable class attribute & FullTextIndexed property attribute for easy FTS indexing
Add simple attribute support for full text indexing of a subset of columns in table as well as support to change the default tokenizer behavior [for the table. Porting a change that I did in a copy of SQLite.cs for a UWP Japanese dictionary app ~9 years ago that I'm now trying to port to MAUI. Added a bunch of unit tests to cover the functionality and confirmed the existing tests are all passing on Win11. Example usage: [FullTextTable] public class Article { [PrimaryKey, AutoIncrement] public int Id { get; set; } [FullTextIndexed] public string Title { get; set; } [FullTextIndexed] public string Content { get; set; } public string Author { get; set; } public DateTime PublishDate { get; set; } } [FullTextTable("JapaneseText_FT", "unicode61")] public class JapaneseText { [PrimaryKey, AutoIncrement] public int Id { get; set; } [FullTextIndexed] public string Text { get; set; } public string Notes { get; set; } }
1 parent bf1eb50 commit fe9a538

File tree

2 files changed

+665
-1
lines changed

2 files changed

+665
-1
lines changed

src/SQLite.cs

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -833,8 +833,31 @@ public int DropTable<
833833
/// </param>
834834
public int DropTable (TableMapping map)
835835
{
836+
var count = 0;
837+
838+
if (map.FullTextTableName != null && map.FullTextColumns.Length > 0) {
839+
// Drop full text index before content table
840+
var queryFullText = string.Format ("DROP TABLE IF EXISTS \"{0}\"", map.FullTextTableName);
841+
count += Execute (queryFullText);
842+
843+
// Drop full text index triggers
844+
var queryFullTextTriggerBeforeUpdate = string.Format ("DROP TRIGGER IF EXISTS \"{0}_t_bu\"", map.FullTextTableName);
845+
count += Execute (queryFullTextTriggerBeforeUpdate);
846+
847+
var queryFullTextTriggerBeforeDelete = string.Format ("DROP TRIGGER IF EXISTS \"{0}_t_bd\"", map.FullTextTableName);
848+
count += Execute (queryFullTextTriggerBeforeDelete);
849+
850+
var queryFullTextTriggerAfterUpdate = string.Format ("DROP TRIGGER IF EXISTS \"{0}_t_au\"", map.FullTextTableName);
851+
count += Execute (queryFullTextTriggerAfterUpdate);
852+
853+
var queryFullTextTriggerAfterInsert = string.Format ("DROP TRIGGER IF EXISTS \"{0}_t_ai\"", map.FullTextTableName);
854+
count += Execute (queryFullTextTriggerAfterInsert);
855+
}
856+
836857
var query = string.Format ("drop table if exists \"{0}\"", map.TableName);
837-
return Execute (query);
858+
count += Execute (query);
859+
860+
return count;
838861
}
839862

840863
/// <summary>
@@ -941,6 +964,39 @@ public CreateTableResult CreateTable (
941964
CreateIndex (indexName, index.TableName, columns, index.Unique);
942965
}
943966

967+
// Create full text indexes
968+
if (map.FullTextTableName != null) {
969+
if (map.FullTextColumns.Length == 0) {
970+
throw new Exception ("Must have at least one full text index column to specify a full text index for the table");
971+
}
972+
973+
var fullTextColsNames = map.FullTextColumns.Select (c => c.Name);
974+
// Only support FTS4 because content= options to link an FTS table to external tables is not supported in FTS3
975+
var queryFullText = "CREATE VIRTUAL TABLE IF NOT EXISTS \"" + map.FullTextTableName + "\" USING fts4(content=\"" + map.TableName + "\", ";
976+
queryFullText += string.Join (",\n", fullTextColsNames.ToArray ());
977+
queryFullText += ", tokenize=" + map.FullTextTableTokenizer;
978+
queryFullText += ")";
979+
980+
Execute (queryFullText);
981+
982+
// Create triggers to keep full text index in sync with content table
983+
var deleteOldSql = "DELETE FROM \"" + map.FullTextTableName + "\" WHERE docid=old." + map.PK.Name + ";";
984+
var insertNewSql = "INSERT INTO \"" + map.FullTextTableName + "\"(docid, " + string.Join (", ", fullTextColsNames);
985+
insertNewSql += ") VALUES (new." + map.PK.Name + ", new." + string.Join (", new.", fullTextColsNames) + ");";
986+
987+
var beforeUpdateSql = "CREATE TRIGGER IF NOT EXISTS \"" + map.FullTextTableName + "_t_bu\" BEFORE UPDATE ON \"" + map.TableName + "\" BEGIN\n" + deleteOldSql + "\nEND;";
988+
Execute (beforeUpdateSql);
989+
990+
var beforeDeleteSql = "CREATE TRIGGER IF NOT EXISTS \"" + map.FullTextTableName + "_t_bd\" BEFORE DELETE ON \"" + map.TableName + "\" BEGIN\n" + deleteOldSql + "\nEND;";
991+
Execute (beforeDeleteSql);
992+
993+
var afterUpdateSql = "CREATE TRIGGER IF NOT EXISTS \"" + map.FullTextTableName + "_t_au\" AFTER UPDATE ON \"" + map.TableName + "\" BEGIN\n" + insertNewSql + "\nEND;";
994+
Execute (afterUpdateSql);
995+
996+
var afterInsertSql = "CREATE TRIGGER IF NOT EXISTS \"" + map.FullTextTableName + "_t_ai\" AFTER INSERT ON \"" + map.TableName + "\" BEGIN\n" + insertNewSql + "\nEND;";
997+
Execute (afterInsertSql);
998+
}
999+
9441000
return result;
9451001
}
9461002

@@ -2917,6 +2973,45 @@ public class StoreAsTextAttribute : Attribute
29172973
{
29182974
}
29192975

2976+
public enum FullTextSearchModule
2977+
{
2978+
FullTextSearch3 = 0,
2979+
FullTextSearch4 = 1
2980+
}
2981+
2982+
/// <summary>
2983+
/// Attribute to specify a full-text search table created from a subset of the
2984+
/// table columns. Name is the name of the FTS table, and Tokenizer is the tokenizer
2985+
/// to use, which can be one of the built-in tokenizers: "simple", "porter", "unicode61", or (possibly) "icu".
2986+
/// </summary>
2987+
[AttributeUsage (AttributeTargets.Class)]
2988+
public class FullTextTableAttribute : Attribute
2989+
{
2990+
public string Name { get; set; }
2991+
public string Tokenizer { get; set; }
2992+
public FullTextSearchModule Module { get; set; } = FullTextSearchModule.FullTextSearch4;
2993+
2994+
public FullTextTableAttribute ()
2995+
{
2996+
}
2997+
2998+
public FullTextTableAttribute (string name)
2999+
{
3000+
Name = name;
3001+
}
3002+
3003+
public FullTextTableAttribute (string name, string tokenizer)
3004+
{
3005+
Name = name;
3006+
Tokenizer = tokenizer;
3007+
}
3008+
}
3009+
3010+
[AttributeUsage (AttributeTargets.Property)]
3011+
public class FullTextIndexedAttribute : Attribute
3012+
{
3013+
}
3014+
29203015
public class TableMapping
29213016
{
29223017
#if NET8_0_OR_GREATER
@@ -2934,13 +3029,18 @@ public class TableMapping
29343029

29353030
public string GetByPrimaryKeySql { get; private set; }
29363031

3032+
public string FullTextTableName { get; private set; }
3033+
3034+
public string FullTextTableTokenizer { get; private set; }
3035+
29373036
public CreateFlags CreateFlags { get; private set; }
29383037

29393038
internal MapMethod Method { get; private set; } = MapMethod.ByName;
29403039

29413040
readonly Column _autoPk;
29423041
readonly Column[] _insertColumns;
29433042
readonly Column[] _insertOrReplaceColumns;
3043+
Column[] _fullTextColumns;
29443044

29453045
public TableMapping (
29463046
#if NET8_0_OR_GREATER
@@ -2969,6 +3069,23 @@ public TableMapping (
29693069
TableName = (tableAttr != null && !string.IsNullOrEmpty (tableAttr.Name)) ? tableAttr.Name : MappedType.Name;
29703070
WithoutRowId = tableAttr != null ? tableAttr.WithoutRowId : false;
29713071

3072+
#if ENABLE_IL2CPP
3073+
var fullTextTableAttr = typeInfo.GetCustomAttribute<FullTextTableAttribute> ();
3074+
#elif NET8_0_OR_GREATER
3075+
var fullTextTableAttr = type.GetCustomAttributes<FullTextTableAttribute> ().FirstOrDefault ();
3076+
#else
3077+
var fullTextTableAttr =
3078+
typeInfo.CustomAttributes
3079+
.Where (x => x.AttributeType == typeof (FullTextTableAttribute))
3080+
.Select (x => (FullTextTableAttribute)Orm.InflateAttribute (x))
3081+
.FirstOrDefault ();
3082+
#endif
3083+
3084+
if (fullTextTableAttr != null) {
3085+
FullTextTableName = fullTextTableAttr.Name != null ? fullTextTableAttr.Name : MappedType.Name + "_FT";
3086+
FullTextTableTokenizer = fullTextTableAttr.Tokenizer != null ? fullTextTableAttr.Tokenizer : "unicode61"; // The default "simple" tokenizer only supports ASCII so default to "unicode61" instead
3087+
}
3088+
29723089
var members = GetPublicMembers(type);
29733090
var cols = new List<Column>(members.Count);
29743091
foreach(var m in members)
@@ -3075,6 +3192,15 @@ public Column[] InsertOrReplaceColumns {
30753192
}
30763193
}
30773194

3195+
public Column[] FullTextColumns {
3196+
get {
3197+
if (_fullTextColumns == null) {
3198+
_fullTextColumns = Columns.Where (c => c.IsInFullTextIndex).ToArray ();
3199+
}
3200+
return _fullTextColumns;
3201+
}
3202+
}
3203+
30783204
public Column FindColumnWithPropertyName (string propertyName)
30793205
{
30803206
var exact = Columns.FirstOrDefault (c => c.PropertyName == propertyName);
@@ -3111,6 +3237,8 @@ public class Column
31113237

31123238
public IEnumerable<IndexedAttribute> Indices { get; set; }
31133239

3240+
public bool IsInFullTextIndex { get; private set; }
3241+
31143242
public bool IsNullable { get; private set; }
31153243

31163244
public int? MaxStringLength { get; private set; }
@@ -3151,6 +3279,7 @@ public Column (MemberInfo member, CreateFlags createFlags = CreateFlags.None)
31513279
) {
31523280
Indices = new IndexedAttribute[] { new IndexedAttribute () };
31533281
}
3282+
IsInFullTextIndex = Orm.IsInFullTextIndex (member);
31543283
IsNullable = !(IsPK || Orm.IsMarkedNotNull (member));
31553284
MaxStringLength = Orm.MaxStringLength (member);
31563285

@@ -3454,6 +3583,11 @@ public static bool IsMarkedNotNull (MemberInfo p)
34543583
{
34553584
return p.CustomAttributes.Any (x => x.AttributeType == typeof (NotNullAttribute));
34563585
}
3586+
3587+
public static bool IsInFullTextIndex (MemberInfo p)
3588+
{
3589+
return p.CustomAttributes.Any (x => x.AttributeType == typeof (FullTextIndexedAttribute));
3590+
}
34573591
}
34583592

34593593
public partial class SQLiteCommand

0 commit comments

Comments
 (0)