=== modified file 'src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseAlbumInfo.cs'
--- src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseAlbumInfo.cs	2008-12-29 20:36:08 +0000
+++ src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseAlbumInfo.cs	2009-02-10 20:37:43 +0000
@@ -186,7 +186,7 @@
         
         [DatabaseColumn(Select = false)]
         protected string TitleLowered {
-            get { return Title == null ? null : Title.ToLower (); }
+            get { return Hyena.StringUtil.SearchKey (Title); }
         }
 
         [DatabaseColumn]
@@ -197,7 +197,7 @@
 
         [DatabaseColumn(Select = false)]
         protected string ArtistNameLowered {
-            get { return ArtistName == null ? null : ArtistName.ToLower (); }
+            get { return Hyena.StringUtil.SearchKey (ArtistName); }
         }
 
         public override string ToString ()

=== modified file 'src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseArtistInfo.cs'
--- src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseArtistInfo.cs	2008-08-19 18:05:46 +0000
+++ src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseArtistInfo.cs	2009-02-10 20:37:43 +0000
@@ -140,7 +140,7 @@
         
         [DatabaseColumn(Select = false)]
         protected string NameLowered {
-            get { return Name == null ? null : Name.ToLower (); }
+            get { return Hyena.StringUtil.SearchKey (Name); }
         }
 
         public override string ToString ()

=== modified file 'src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseTrackInfo.cs'
--- src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseTrackInfo.cs	2009-01-28 01:29:07 +0000
+++ src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseTrackInfo.cs	2009-02-10 20:37:43 +0000
@@ -398,7 +398,7 @@
         
         [DatabaseColumn(Select = false)]
         protected string TitleLowered {
-            get { return TrackTitle == null ? null : TrackTitle.ToLower (); }
+            get { return Hyena.StringUtil.SearchKey (TrackTitle); }
         }
 
         [DatabaseColumn(Select = false)]

=== modified file 'src/Core/Banshee.Services/Banshee.Database/BansheeDbFormatMigrator.cs'
--- src/Core/Banshee.Services/Banshee.Database/BansheeDbFormatMigrator.cs	2009-01-26 01:41:24 +0000
+++ src/Core/Banshee.Services/Banshee.Database/BansheeDbFormatMigrator.cs	2009-02-10 20:37:51 +0000
@@ -52,7 +52,7 @@
         // NOTE: Whenever there is a change in ANY of the database schema,
         //       this version MUST be incremented and a migration method
         //       MUST be supplied to match the new version number
-        protected const int CURRENT_VERSION = 22;
+        protected const int CURRENT_VERSION = 23;
         protected const int CURRENT_METADATA_VERSION = 4;
         
 #region Migration Driver
@@ -551,6 +551,18 @@
         
 #endregion
 
+#region Version 23
+        [DatabaseVersion (23)]
+        private bool Migrate_23 ()
+        {
+            Execute ("UPDATE CoreArtists SET NameLowered = HYENA_SEARCH_KEY(Name)");
+            Execute ("UPDATE CoreAlbums SET ArtistNameLowered = HYENA_SEARCH_KEY(ArtistName)");
+            Execute ("UPDATE CoreAlbums SET TitleLowered = HYENA_SEARCH_KEY(Title)");
+            Execute ("UPDATE CoreTracks SET TitleLowered = HYENA_SEARCH_KEY(Title)");
+            return true;
+        }
+#endregion
+
 #pragma warning restore 0169
         
 #region Fresh database setup

=== modified file 'src/Core/Banshee.Services/Banshee.Query/BansheeQuery.cs'
--- src/Core/Banshee.Services/Banshee.Query/BansheeQuery.cs	2009-01-26 02:09:07 +0000
+++ src/Core/Banshee.Services/Banshee.Query/BansheeQuery.cs	2009-02-10 20:37:51 +0000
@@ -221,7 +221,7 @@
         public static QueryField LicenseUriField = new QueryField (
             "licenseuri", "LicenseUri",
             // Translators: noun
-            Catalog.GetString ("License"), "CoreTracks.LicenseUri", false,
+            Catalog.GetString ("License"), "CoreTracks.LicenseUri", typeof(ExactStringQueryValue),
             // Translators: These are unique search fields (and nouns).  Please, no spaces. Blank ok.
             Catalog.GetString ("license"), Catalog.GetString ("licensed"), Catalog.GetString ("under"),
             "license", "licensed", "under"
@@ -261,7 +261,7 @@
 
         public static QueryField UriField = new QueryField (
             "uri", "Uri",
-            Catalog.GetString ("File Location"), "CoreTracks.Uri",
+            Catalog.GetString ("File Location"), "CoreTracks.Uri", typeof(ExactStringQueryValue),
             // Translators: These are unique search fields.  Please, no spaces. Blank ok.
             Catalog.GetString ("uri"), Catalog.GetString ("path"), Catalog.GetString ("file"), Catalog.GetString ("location"),
             "uri", "path", "file", "location"
@@ -277,7 +277,7 @@
 
         public static QueryField MimeTypeField = new QueryField (
             "mimetype", "MimeType",
-            Catalog.GetString ("Mime Type"), "CoreTracks.MimeType {0} OR CoreTracks.Uri {0}",
+            Catalog.GetString ("Mime Type"), "CoreTracks.MimeType {0} OR CoreTracks.Uri {0}", typeof(ExactStringQueryValue),
             // Translators: These are unique search fields.  Please, no spaces. Blank ok.
             Catalog.GetString ("type"), Catalog.GetString ("mimetype"), Catalog.GetString ("format"), Catalog.GetString ("ext"),
             "type", "mimetype", "format", "ext", "mime"

=== modified file 'src/Libraries/Hyena/Hyena.Data.Sqlite/SqliteUtils.cs'
--- src/Libraries/Hyena/Hyena.Data.Sqlite/SqliteUtils.cs	2009-01-28 01:29:07 +0000
+++ src/Libraries/Hyena/Hyena.Data.Sqlite/SqliteUtils.cs	2009-02-10 20:37:51 +0000
@@ -29,6 +29,7 @@
 using System;
 using System.Reflection;
 using System.Text;
+using Mono.Data.Sqlite;
 
 namespace Hyena.Data.Sqlite
 {
@@ -122,4 +123,12 @@
             return builder.ToString ();
         }
     }
+    
+    [SqliteFunction (Name = "HYENA_SEARCH_KEY", FuncType = FunctionType.Scalar, Arguments = 1)]
+    internal class SearchKeyFunction : SqliteFunction
+    {
+        public override object Invoke (object[] args) {
+            return Hyena.StringUtil.SearchKey (args[0] as string);
+        }
+    }
 }

=== added file 'src/Libraries/Hyena/Hyena.Query/ExactStringQueryValue.cs'
--- src/Libraries/Hyena/Hyena.Query/ExactStringQueryValue.cs	1970-01-01 00:00:00 +0000
+++ src/Libraries/Hyena/Hyena.Query/ExactStringQueryValue.cs	2009-02-10 20:48:07 +0000
@@ -0,0 +1,39 @@
+//
+// ExactStringQueryValue.cs
+//
+// Authors:
+//   John Millikin <jmillikin@gmail.com>
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+
+namespace Hyena.Query
+{
+    // A query value that requires the string match exactly
+    public class ExactStringQueryValue : StringQueryValue
+    {
+        public override string ToSql ()
+        {
+            return String.IsNullOrEmpty (value) ? null : value.Replace ("'", "''").ToLower ();
+        }
+    }
+}

=== modified file 'src/Libraries/Hyena/Hyena.Query/QueryField.cs'
--- src/Libraries/Hyena/Hyena.Query/QueryField.cs	2008-12-29 22:18:50 +0000
+++ src/Libraries/Hyena/Hyena.Query/QueryField.cs	2009-02-10 21:00:08 +0000
@@ -150,23 +150,11 @@
             StringBuilder sb = new StringBuilder ();
 
             if (no_custom_format) {
-                if (qv is StringQueryValue) {
-                    if (column_lowered) {
-                        // The column is pre-lowered, no need to call lower() in SQL
-                        sb.AppendFormat ("{0} {1}", Column, String.Format (op.SqlFormat, value.ToLower ()));
-                    } else {
-                        // Match string values literally and against a lower'd version.  Mostly a workaround
-                        // the fact that Sqlite's lower() method only works for ASCII (meaning even with this,
-                        // we're not getting 100% case-insensitive matching).
-                        sb.AppendFormat ("({0} {1} {3} LOWER({0}) {2})", Column,
-                            String.Format (op.SqlFormat, value),
-                            String.Format (op.SqlFormat, value.ToLower ()),
-                            op.IsNot ? "AND" : "OR"
-                        );
-                    }
-                } else {
-                    sb.AppendFormat ("{0} {1}", Column, String.Format (op.SqlFormat, value));
+                string column_with_key = Column;
+                if (qv is StringQueryValue && !(column_lowered || qv is ExactStringQueryValue)) {
+                    column_with_key = String.Format ("HYENA_SEARCH_KEY({0})", Column);
                 }
+                sb.AppendFormat ("{0} {1}", column_with_key, String.Format (op.SqlFormat, value));
 
                 if (op.IsNot) {
                     return String.Format ("({0} OR {1} IS NULL)", sb.ToString (), Column);

=== modified file 'src/Libraries/Hyena/Hyena.Query/StringQueryValue.cs'
--- src/Libraries/Hyena/Hyena.Query/StringQueryValue.cs	2008-07-20 23:05:30 +0000
+++ src/Libraries/Hyena/Hyena.Query/StringQueryValue.cs	2009-02-10 20:37:51 +0000
@@ -79,7 +79,9 @@
 
         public override string ToSql ()
         {
-            return String.IsNullOrEmpty (value) ? null : value.Replace ("'", "''");
+            // SearchKey() removes ' anyway, but it's escaped again so proper
+            // SQL behavior isn't dependent on search behavior.
+            return Hyena.StringUtil.SearchKey (value).Replace ("'", "''");
         }
     }
 }

=== modified file 'src/Libraries/Hyena/Hyena.Query/Tests/QueryTests.cs'
--- src/Libraries/Hyena/Hyena.Query/Tests/QueryTests.cs	2008-08-16 09:47:38 +0000
+++ src/Libraries/Hyena/Hyena.Query/Tests/QueryTests.cs	2009-02-10 21:25:56 +0000
@@ -86,6 +86,11 @@
             qv = new StringQueryValue (); qv.ParseUserQuery ("foo 'bar'");
             Assert.AreEqual ("foo 'bar'", qv.Value);
             Assert.AreEqual ("foo 'bar'", qv.ToUserQuery ());
+            Assert.AreEqual ("foo bar", qv.ToSql ());
+    
+            qv = new ExactStringQueryValue (); qv.ParseUserQuery ("foo 'bar'");
+            Assert.AreEqual ("foo 'bar'", qv.Value);
+            Assert.AreEqual ("foo 'bar'", qv.ToUserQuery ());
             Assert.AreEqual ("foo ''bar''", qv.ToSql ());
     
             qv = new IntegerQueryValue (); qv.ParseUserQuery ("22");

=== modified file 'src/Libraries/Hyena/Hyena/StringUtil.cs'
--- src/Libraries/Hyena/Hyena/StringUtil.cs	2009-01-21 20:28:36 +0000
+++ src/Libraries/Hyena/Hyena/StringUtil.cs	2009-02-10 21:20:34 +0000
@@ -27,6 +27,7 @@
 //
 
 using System;
+using System.Collections.Generic;
 using System.Text;
 using System.Globalization;
 using System.Text.RegularExpressions;
@@ -160,5 +161,43 @@
             else
                 return (int)num + 1;
         }
+        
+        // A mapping of non-Latin characters to be considered the same as
+        // a Latin equivalent.
+        private static Dictionary<char, char> BuildSpecialCases ()
+        {
+            Dictionary<char, char> dict = new Dictionary<char, char>();
+            dict['\u00f8'] = 'o';
+            dict['\u0142'] = 'l';
+            return dict;
+        }
+        private static Dictionary<char, char> ignored_special_cases = BuildSpecialCases ();
+        
+        //  Removes accents from Latin characters, and some kinds of punctuation.
+        public static string SearchKey (string val)
+        {
+            if (String.IsNullOrEmpty (val)) { return val; }
+            
+            val = val.ToLower ();
+            StringBuilder sb = new StringBuilder ();
+            UnicodeCategory category;
+            bool prev_latin = false;
+            
+            foreach (char c in val.Normalize (NormalizationForm.FormKD)) {
+                category = Char.GetUnicodeCategory (c);
+                if (ignored_special_cases.ContainsKey (c)) {
+                    sb.Append (ignored_special_cases[c]);
+                }
+                
+                // Skip punctuation
+                else if (category == UnicodeCategory.OtherPunctuation) {}
+                else if(!(prev_latin && category == UnicodeCategory.NonSpacingMark)) {
+                    sb.Append (c);
+                }
+                prev_latin = (c >= 'a' && c <= 'z');
+            }
+            
+            return sb.ToString ().Normalize (NormalizationForm.FormKC);
+        }
     }
 }

=== modified file 'src/Libraries/Hyena/Hyena/Tests/StringUtilTests.cs'
--- src/Libraries/Hyena/Hyena/Tests/StringUtilTests.cs	2008-09-25 20:35:57 +0000
+++ src/Libraries/Hyena/Hyena/Tests/StringUtilTests.cs	2009-02-10 21:20:52 +0000
@@ -150,6 +150,68 @@
 href=http://lkjdflkjdflkjj>baz foo< /a> bar"));
         }
     }
+    
+    [TestFixture]
+    public class SearchKeyTests
+    {
+        private void TestSearchKey (string before, string after)
+        {
+            Assert.AreEqual (after, StringUtil.SearchKey (before));
+        }
+        
+        [Test]
+        public void TestEmpty ()
+        {
+            TestSearchKey ("", "");
+            TestSearchKey (null, null);
+        }
+        
+        // Test that resulting search keys are in lower-case
+        [Test]
+        public void TestLowercase ()
+        {
+            TestSearchKey ("A", "a");
+            TestSearchKey ("\u0104", "a");
+        }
+        
+        // Test that combining diacritics are removed from Latin characters.
+        [Test]
+        public void TestRemoveDiacritics ()
+        {
+            TestSearchKey ("\u00e9", "e");
+            TestSearchKey ("e\u0301", "e");
+            
+            TestSearchKey ("\u014d", "o");
+            TestSearchKey ("o\u0304", "o");
+        }
+        
+        // Test that combining diacritics are preserved, and combined, for non-Latin characters.
+        [Test]
+        public void TestPreserveDiacritics ()
+        {
+            TestSearchKey ("\u304c", "\u304c");
+            TestSearchKey ("\u304b\u3099", "\u304c");
+        }
+        
+        // Test that some non-Latin characters are converted to Latin counterparts.
+        [Test]
+        public void TestEquivalents ()
+        {
+            TestSearchKey ("\u00f8", "o");
+            TestSearchKey ("\u0142", "l");
+        }
+        
+        // Test that some kinds of punctuation are removed.
+        [Test]
+        public void TestRemovePunctuation ()
+        {
+            TestSearchKey ("'", "");
+            TestSearchKey ("\"", "");
+            TestSearchKey ("!", "");
+            TestSearchKey ("?", "");
+            TestSearchKey ("/", "");
+        }
+    }
 }
 
 #endif

=== modified file 'src/Libraries/Hyena/Makefile.am'
--- src/Libraries/Hyena/Makefile.am	2009-01-28 01:29:07 +0000
+++ src/Libraries/Hyena/Makefile.am	2009-02-10 20:37:51 +0000
@@ -58,6 +58,7 @@
 	Hyena.Json/TokenType.cs \
 	Hyena.Query/AliasedObjectSet.cs \
 	Hyena.Query/DateQueryValue.cs \
+	Hyena.Query/ExactStringQueryValue.cs \
 	Hyena.Query/FileSizeQueryValue.cs \
 	Hyena.Query/IntegerKeyedObjectQueryValue.cs \
 	Hyena.Query/IntegerQueryValue.cs \

