diff --git a/README.md b/README.md
index 7d6973c417..814f04bb40 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ Marten also provides .NET developers with an ACID-compliant event store with use
Before getting started you will need the following in your environment:
-**1. .NET Core SDK 2.0+**
+**1. .NET Core SDK 2.1**
Available [here](https://www.microsoft.com/net/download/core)
@@ -91,13 +91,11 @@ dotnet restore src\Marten.sln
dotnet build src\Marten.sln
# running tests for a specific target framework
-dotnet test src\Marten.Testing\Marten.Testing.csproj --framework netcoreapp2.0
+dotnet run -p martenbuild.csproj -- test
# mocha tests
-npm install
-npm run test
+dotnet run -p martenbuild.csproj -- mocha
# running documentation website locally
-dotnet restore docs.csproj
-dotnet stdocs run
+dotnet run -p martenbuild.csproj -- docs
```
diff --git a/documentation/documentation/documents/configuration/full_text.md b/documentation/documentation/documents/configuration/full_text.md
new file mode 100644
index 0000000000..a8bc415d6d
--- /dev/null
+++ b/documentation/documentation/documents/configuration/full_text.md
@@ -0,0 +1,76 @@
+
+
+Full Text Indexes in Marten are built based on <[linkto:documentation/documents/configuration/gin_or_gist_index;title=Gin Indexes]> utilizing [Postgres built in Text Search functions](https://www.postgresql.org/docs/10/textsearch-controls.html). This enables the possibility to do more sophisticated searching through text fields.
+
+
+To use this feature, you will need to use PostgreSQL version 10.0 or above, as this is the first version that support text search function on jsonb column - this is also the data type that Marten use to store it's data.
+
+
+## Definining Full Text Index through Store options
+
+Full Text Indexes can be created using the fluent interface of `StoreOptions` like this:
+
+
+* one index for whole document - all document properties values will be indexed
+
+<[sample:using_whole_document_full_text_index_through_store_options_with_default]>
+
+
+If you don't specify language (regConfig) - by default it will be created with 'english' value.
+
+
+* single property - there is possibility to specify specific property to be indexed
+
+<[sample:using_a_single_property_full_text_index_through_store_options_with_default]>
+
+* single property with custom settings
+
+<[sample:using_a_single_property_full_text_index_through_store_options_with_custom_settings]>
+
+* multiple properties
+
+<[sample:using_multiple_properties_full_text_index_through_store_options_with_default]>
+
+* multiple properties with custom settings
+
+<[sample:using_multiple_properties_full_text_index_through_store_options_with_custom_settings]>
+
+* more than one index for document with different languages (regConfig)
+
+<[sample:using_more_than_one_full_text_index_through_store_options_with_different_reg_config]>
+
+## Defining Full Text Index through Attribute
+
+Full Text Indexes can be created using the `[FullTextIndex]` attribute like this:
+
+* one index for whole document - by setting attribute on the class all document properties values will be indexed
+
+<[sample:using_a_full_text_index_through_attribute_on_class_with_default]>
+
+* single property
+
+<[sample:using_a_single_property_full_text_index_through_store_options_with_default]>
+
+
+If you don't specify regConfig - by default it will be created with 'english' value.
+
+
+* single property with custom settings
+
+<[sample:using_a_single_property_full_text_index_through_store_options_with_custom_settings]>
+
+* multiple properties
+
+<[sample:using_multiple_properties_full_text_index_through_store_options_with_default]>
+
+
+To group multiple properties into single index you need to specify the same values in `IndexName` parameters.
+
+
+* one index for multiple properties with custom settings
+
+<[sample:using_multiple_properties_full_text_index_through_store_options_with_custom_settings]>
+
+* multiple indexes for multiple properties with custom settings
+
+<[sample:using_more_than_one_full_text_index_through_store_options_with_different_reg_config]>
diff --git a/documentation/documentation/documents/configuration/unique.md b/documentation/documentation/documents/configuration/unique.md
index d2f5406865..f29594c31a 100644
--- a/documentation/documentation/documents/configuration/unique.md
+++ b/documentation/documentation/documents/configuration/unique.md
@@ -6,7 +6,7 @@ Marten supports both <[linkto:documentation/documents/configuration/duplicated_f
## Definining Unique Index through Store options
-Unique Indexes can be created using the fluent interface off of `StoreOptions` like this:
+Unique Indexes can be created using the fluent interface of `StoreOptions` like this:
1. **Computed**:
* single property
diff --git a/documentation/documentation/documents/querying/linq.md b/documentation/documentation/documents/querying/linq.md
index de88737d77..07453cba71 100644
--- a/documentation/documentation/documents/querying/linq.md
+++ b/documentation/documentation/documents/querying/linq.md
@@ -211,6 +211,32 @@ Query data from all tenants using `AnyTenant` method.
Use `TenantIsOneOf` to query on a selected list of tenants.
<[sample:tenant_is_one_of]>
+## Text Search
+
+Postgres contains built in [Text Search functions](https://www.postgresql.org/docs/10/textsearch-controls.html). They enable the possibility to do more sophisticated searching through text fields. Marten gives possibility to define <[linkto:documentation/documents/configuration/full_text;title=Full Text Indexes]> and perform queries on them.
+Currently three types of full Text Search functions are supported:
+
+* regular Search (to_tsquery)
+
+<[sample:search_in_query_sample]>
+
+* plain text Search (plainto_tsquery)
+
+<[sample:plain_search_in_query_sample]>
+
+* phrase Search (phraseto_tsquery)
+
+<[sample:phrase_search_in_query_sample]>
+
+All types of Text Searches can be combined with other Linq queries
+
+<[sample:text_search_combined_with_other_query_sample]>
+
+They allow also to specify language (regConfig) of the text search query (by default `english` is being used)
+
+<[sample:text_search_with_non_default_regConfig_sample]>
+
+
## Supported Types
At this point, Marten's Linq support has been tested against these .Net types:
diff --git a/martenbuild.cs b/martenbuild.cs
index eca363da41..b8e0c3c592 100644
--- a/martenbuild.cs
+++ b/martenbuild.cs
@@ -8,7 +8,7 @@ namespace martenbuild
{
class MartenBuild
{
- private const string BUILD_VERSION = "3.1.0";
+ private const string BUILD_VERSION = "3.3.0";
static void Main(string[] args)
{
diff --git a/rakefile.rb b/rakefile.rb
index 841af27569..df52e330a6 100644
--- a/rakefile.rb
+++ b/rakefile.rb
@@ -4,7 +4,7 @@
COMPILE_TARGET = ENV['config'].nil? ? "debug" : ENV['config']
RESULTS_DIR = "results"
-BUILD_VERSION = '3.1.0'
+BUILD_VERSION = '3.3.0'
CONNECTION = ENV['connection']
diff --git a/src/Marten.Testing/Acceptance/full_text_index.cs b/src/Marten.Testing/Acceptance/full_text_index.cs
index 679baafafd..68dfb9d0a7 100644
--- a/src/Marten.Testing/Acceptance/full_text_index.cs
+++ b/src/Marten.Testing/Acceptance/full_text_index.cs
@@ -1,18 +1,184 @@
-using System;
+using System;
+using System.Collections.Generic;
using System.Linq;
-using Marten.Schema;
+using Marten.Schema;
+using Marten.Storage;
using Marten.Testing.Documents;
-using Shouldly;
+using Shouldly;
using Xunit;
namespace Marten.Testing.Acceptance
-{
+{
+ // SAMPLE: using_a_full_text_index_through_attribute_on_class_with_default
+ [FullTextIndex]
+ public class Book
+ {
+ public Guid Id { get; set; }
+
+ public string Title { get; set; }
+
+ public string Author { get; set; }
+
+ public string Information { get; set; }
+ }
+
+ // ENDSAMPLE
+
+ // SAMPLE: using_a_single_property_full_text_index_through_attribute_with_default
+ public class UserProfile
+ {
+ public Guid Id { get; set; }
+
+ [FullTextIndex]
+ public string Information { get; set; }
+ }
+
+ // ENDSAMPLE
+
+ // SAMPLE: using_a_single_property_full_text_index_through_attribute_with_custom_settings
+ public class UserDetails
+ {
+ private const string FullTextIndexName = "mt_custom_user_details_fts_idx";
+
+ public Guid Id { get; set; }
+
+ [FullTextIndex(IndexName = FullTextIndexName, RegConfig = "italian")]
+ public string Details { get; set; }
+ }
+
+ // ENDSAMPLE
+
+ // SAMPLE: using_multiple_properties_full_text_index_through_attribute_with_default
+ public class Article
+ {
+ public Guid Id { get; set; }
+
+ [FullTextIndex]
+ public string Heading { get; set; }
+
+ [FullTextIndex]
+ public string Text { get; set; }
+ }
+
+ // ENDSAMPLE
+
+ // SAMPLE: using_multiple_properties_full_text_index_through_attribute_with_custom_settings
+ public class BlogPost
+ {
+ public Guid Id { get; set; }
+
+ public string Category { get; set; }
+
+ [FullTextIndex]
+ public string EnglishText { get; set; }
+
+ [FullTextIndex(RegConfig = "italian")]
+ public string ItalianText { get; set; }
+
+ [FullTextIndex(RegConfig = "french")]
+ public string FrenchText { get; set; }
+ }
+
+ // ENDSAMPLE
+
public class full_text_index : IntegratedFixture
{
+ public void using_whole_document_full_text_index_through_store_options_with_default()
+ {
+ // SAMPLE: using_whole_document_full_text_index_through_store_options_with_default
+ var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+
+ // This creates
+ _.Schema.For().FullTextIndex();
+ });
+ // ENDSAMPLE
+ }
+
+ public void using_a_single_property_full_text_index_through_store_options_with_default()
+ {
+ // SAMPLE: using_a_single_property_full_text_index_through_store_options_with_default
+ var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+
+ // This creates
+ _.Schema.For().FullTextIndex(d => d.FirstName);
+ });
+ // ENDSAMPLE
+ }
+
+ public void using_a_single_property_full_text_index_through_store_options_with_custom_settings()
+ {
+ // SAMPLE: using_a_single_property_full_text_index_through_store_options_with_custom_settings
+ var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+
+ // This creates
+ _.Schema.For().FullTextIndex(
+ index =>
+ {
+ index.IndexName = "mt_custom_italian_user_fts_idx";
+ index.RegConfig = "italian";
+ },
+ d => d.FirstName);
+ });
+ // ENDSAMPLE
+ }
+
+ public void using_multiple_properties_full_text_index_through_store_options_with_default()
+ {
+ // SAMPLE: using_multiple_properties_full_text_index_through_store_options_with_default
+ var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+
+ // This creates
+ _.Schema.For().FullTextIndex(d => d.FirstName, d => d.LastName);
+ });
+ // ENDSAMPLE
+ }
+
+ public void using_multiple_properties_full_text_index_through_store_options_with_custom_settings()
+ {
+ // SAMPLE: using_multiple_properties_full_text_index_through_store_options_with_custom_settings
+ var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+
+ // This creates
+ _.Schema.For().FullTextIndex(
+ index =>
+ {
+ index.IndexName = "mt_custom_italian_user_fts_idx";
+ index.RegConfig = "italian";
+ },
+ d => d.FirstName, d => d.LastName);
+ });
+ // ENDSAMPLE
+ }
+
+ public void using_more_than_one_full_text_index_through_store_options_with_different_reg_config()
+ {
+ // SAMPLE: using_more_than_one_full_text_index_through_store_options_with_different_reg_config
+ var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+
+ // This creates
+ _.Schema.For()
+ .FullTextIndex(d => d.FirstName) //by default it will use "english"
+ .FullTextIndex("italian", d => d.LastName);
+ });
+ // ENDSAMPLE
+ }
+
[Fact]
- public void example()
+ public void using_full_text_query_through_query_session()
{
- // SAMPLE: using-a-full-text-index
+ // SAMPLE: using_full_text_query_through_query_session
var store = DocumentStore.For(_ =>
{
_.Connection(ConnectionSource.ConnectionString);
@@ -20,28 +186,284 @@ public void example()
// Create the full text index
_.Schema.For().FullTextIndex();
});
+ IReadOnlyList result;
- using(var session = store.OpenSession())
+ using (var session = store.OpenSession())
{
- session.Store(new User { FirstName = "Jeremy", LastName = "Miller", UserName = "jmiller"});
- session.Store(new User { FirstName = "Lindsey", LastName = "Miller", UserName = "lmiller"});
- session.Store(new User { FirstName = "Max", LastName = "Miller", UserName = "mmiller"});
- session.Store(new User { FirstName = "Frank", LastName = "Zombo", UserName = "fzombo"});
- session.Store(new User { FirstName = "Somebody", LastName = "Somewher", UserName = "somebody"});
- session.SaveChanges();
-
- var somebody = session.Search("somebody");
+ session.Store(new User { FirstName = "Jeremy", LastName = "Miller", UserName = "jmiller" });
+ session.Store(new User { FirstName = "Lindsey", LastName = "Miller", UserName = "lmiller" });
+ session.Store(new User { FirstName = "Max", LastName = "Miller", UserName = "mmiller" });
+ session.Store(new User { FirstName = "Frank", LastName = "Zombo", UserName = "fzombo" });
+ session.Store(new User { FirstName = "Somebody", LastName = "Somewher", UserName = "somebody" });
+ session.SaveChanges();
+
+ result = session.Search("somebody");
}
store.Dispose();
// ENDSAMPLE
+
+ result.Count().ShouldBe(1);
+ }
+
+ [Fact]
+ public void search_in_query_sample()
+ {
+ StoreOptions(_ => _.RegisterDocumentType());
+
+ var expectedId = Guid.NewGuid();
+
+ using (var session = theStore.OpenSession())
+ {
+ session.Store(new BlogPost { Id = expectedId, EnglishText = "somefilter" });
+ session.Store(new BlogPost { Id = Guid.NewGuid(), ItalianText = "somefilter" });
+ session.SaveChanges();
+ }
+
+ using (var session = theStore.OpenSession())
+ {
+ // SAMPLE: search_in_query_sample
+ var posts = session.Query()
+ .Where(x => x.Search("somefilter"))
+ .ToList();
+ // ENDSAMPLE
+
+ posts.Count.ShouldBe(1);
+ posts.Single().Id.ShouldBe(expectedId);
+ }
+ }
+
+ [Fact]
+ public void plain_text_search_in_query_sample()
+ {
+ StoreOptions(_ => _.RegisterDocumentType());
+
+ var expectedId = Guid.NewGuid();
+
+ using (var session = theStore.OpenSession())
+ {
+ session.Store(new BlogPost { Id = expectedId, EnglishText = "somefilter" });
+ session.Store(new BlogPost { Id = Guid.NewGuid(), ItalianText = "somefilter" });
+ session.SaveChanges();
+ }
+
+ using (var session = theStore.OpenSession())
+ {
+ // SAMPLE: plain_search_in_query_sample
+ var posts = session.Query()
+ .Where(x => x.PlainTextSearch("somefilter"))
+ .ToList();
+ // ENDSAMPLE
+
+ posts.Count.ShouldBe(1);
+ posts.Single().Id.ShouldBe(expectedId);
+ }
+ }
+
+ [Fact]
+ public void phrase_search_in_query_sample()
+ {
+ StoreOptions(_ => _.RegisterDocumentType());
+
+ var expectedId = Guid.NewGuid();
+
+ using (var session = theStore.OpenSession())
+ {
+ session.Store(new BlogPost { Id = expectedId, EnglishText = "somefilter" });
+ session.Store(new BlogPost { Id = Guid.NewGuid(), ItalianText = "somefilter" });
+ session.SaveChanges();
+ }
+
+ using (var session = theStore.OpenSession())
+ {
+ // SAMPLE: phrase_search_in_query_sample
+ var posts = session.Query()
+ .Where(x => x.PhraseSearch("somefilter"))
+ .ToList();
+ // ENDSAMPLE
+
+ posts.Count.ShouldBe(1);
+ posts.Single().Id.ShouldBe(expectedId);
+ }
+ }
+
+ [Fact]
+ public void text_search_combined_with_other_query_sample()
+ {
+ StoreOptions(_ => _.RegisterDocumentType());
+
+ var expectedId = Guid.NewGuid();
+
+ using (var session = theStore.OpenSession())
+ {
+ session.Store(new BlogPost { Id = expectedId, EnglishText = "somefilter", Category = "LifeStyle" });
+ session.Store(new BlogPost { Id = Guid.NewGuid(), EnglishText = "somefilter", Category = "Other" });
+ session.Store(new BlogPost { Id = Guid.NewGuid(), ItalianText = "somefilter", Category = "LifeStyle" });
+ session.SaveChanges();
+ }
+
+ using (var session = theStore.OpenSession())
+ {
+ // SAMPLE: text_search_combined_with_other_query_sample
+ var posts = session.Query()
+ .Where(x => x.Category == "LifeStyle")
+ .Where(x => x.PhraseSearch("somefilter"))
+ .ToList();
+ // ENDSAMPLE
+
+ posts.Count.ShouldBe(1);
+ posts.Single().Id.ShouldBe(expectedId);
+ }
+ }
+
+ [Fact]
+ public void text_search_with_non_default_regConfig_sample()
+ {
+ StoreOptions(_ => _.RegisterDocumentType());
+
+ var expectedId = Guid.NewGuid();
+
+ using (var session = theStore.OpenSession())
+ {
+ session.Store(new BlogPost { Id = Guid.NewGuid(), EnglishText = "somefilter" });
+ session.Store(new BlogPost { Id = expectedId, ItalianText = "somefilter" });
+ session.SaveChanges();
+ }
+
+ using (var session = theStore.OpenSession())
+ {
+ // SAMPLE: text_search_with_non_default_regConfig_sample
+ var posts = session.Query()
+ .Where(x => x.PhraseSearch("somefilter", "italian"))
+ .ToList();
+ // ENDSAMPLE
+
+ posts.Count.ShouldBe(1);
+ posts.Single().Id.ShouldBe(expectedId);
+ }
+ }
+
+ [Fact]
+ public void should_search_with_store_options_default_configuration()
+ {
+ SearchShouldBeSuccessfulFor(_ => _.Schema.For().FullTextIndex());
}
+ [Fact]
+ public void should_search_with_store_options_for_specific_members()
+ {
+ SearchShouldBeSuccessfulFor(_ => _.Schema.For().FullTextIndex(d => d.FirstName, d => d.LastName));
+ }
+
+ [Fact]
+ public void should_search_with_store_options_with_multipleIndexes()
+ {
+ const string frenchRegConfig = "french";
+ const string italianRegConfig = "italian";
+
+ StoreOptions(_ => _.Schema.For()
+ .FullTextIndex(italianRegConfig, d => d.FirstName)
+ .FullTextIndex(frenchRegConfig, d => d.LastName));
+
+ const string searchFilter = "Lindsey";
+
+ using (var session = theStore.OpenSession())
+ {
+ session.Store(new User { FirstName = searchFilter, LastName = "Miller", UserName = "lmiller" });
+ session.Store(new User { FirstName = "Frank", LastName = searchFilter, UserName = "fzombo" });
+
+ session.Store(new User { FirstName = "Jeremy", LastName = "Miller", UserName = "jmiller" });
+ session.Store(new User { FirstName = "Max", LastName = "Miller", UserName = "mmiller" });
+ session.Store(new User { FirstName = "Somebody", LastName = "Somewher", UserName = "somebody" });
+ session.SaveChanges();
+
+ var italianResults = session.Search(searchFilter, italianRegConfig);
+
+ italianResults.Count.ShouldBe(1);
+ italianResults.ShouldContain(u => u.FirstName == searchFilter);
+ italianResults.ShouldNotContain(u => u.LastName == searchFilter);
+
+ var frenchResults = session.Search(searchFilter, frenchRegConfig);
+
+ frenchResults.Count.ShouldBe(1);
+ frenchResults.ShouldNotContain(u => u.FirstName == searchFilter);
+ frenchResults.ShouldContain(u => u.LastName == searchFilter);
+ }
+ }
+
+ [Fact]
+ public void should_search_by_tenant_with_tenancy_conjoined()
+ {
+ StoreOptions(_ =>
+ {
+ _.Events.TenancyStyle = TenancyStyle.Conjoined;
+ _.Policies.AllDocumentsAreMultiTenanted();
+
+ _.Schema.For().FullTextIndex();
+ });
+
+ const string searchFilter = "Lindsey";
+
+ var tenants = new[] { "Tenant", "Other Tenant" };
+
+ foreach (var tenant in tenants)
+ {
+ using (var session = theStore.OpenSession(tenant))
+ {
+ session.Store(new User { FirstName = searchFilter, LastName = "Miller", UserName = "lmiller" });
+ session.Store(new User { FirstName = "Frank", LastName = "Zombo", UserName = "fzombo" });
+ session.SaveChanges();
+ }
+ }
+
+ foreach (var tenant in tenants)
+ {
+ using (var session = theStore.OpenSession(tenant))
+ {
+ var results = session.Search(searchFilter);
+
+ results.Count.ShouldBe(1);
+ results.ShouldContain(u => u.FirstName == searchFilter);
+ results.ShouldNotContain(u => u.LastName == searchFilter);
+ }
+ }
+ }
+
+ private void SearchShouldBeSuccessfulFor(Action configure)
+ {
+ StoreOptions(configure);
+
+ const string searchFilter = "Lindsey";
+
+ using (var session = theStore.OpenSession())
+ {
+ session.Store(new User { FirstName = searchFilter, LastName = "Miller", UserName = "lmiller" });
+ session.Store(new User { FirstName = "Frank", LastName = searchFilter, UserName = "fzombo" });
+
+ session.Store(new User { FirstName = "Jeremy", LastName = "Miller", UserName = "jmiller" });
+ session.Store(new User { FirstName = "Max", LastName = "Miller", UserName = "mmiller" });
+ session.Store(new User { FirstName = "Somebody", LastName = "Somewher", UserName = "somebody" });
+ session.SaveChanges();
+
+ var results = session.Search(searchFilter);
+
+ results.Count.ShouldBe(2);
+ results.ShouldContain(u => u.FirstName == searchFilter);
+ results.ShouldContain(u => u.LastName == searchFilter);
+ }
+ }
+
+ [Fact]
+ private void should_search_using_a_single_property_full_text_index_through_attribute_with_custom_settings()
+ {
+ StoreOptions(_ => _.Schema.For());
+ }
+
[Fact]
public void creating_a_full_text_index_should_create_the_index_on_the_table()
{
- StoreOptions(_=>_.Schema.For().FullTextIndex());
+ StoreOptions(_ => _.Schema.For().FullTextIndex());
var data = Target.GenerateRandomData(100).ToArray();
theStore.BulkInsert(data);
@@ -73,7 +495,7 @@ public void not_specifying_an_index_name_should_generate_default_index_name()
[Fact]
public void specifying_an_index_name_without_marten_prefix_should_prepend_prefix()
{
- StoreOptions(_ => _.Schema.For().FullTextIndex(configure: x=>x.IndexName = "doesnt_have_prefix"));
+ StoreOptions(_ => _.Schema.For().FullTextIndex(configure: x => x.IndexName = "doesnt_have_prefix"));
var data = Target.GenerateRandomData(100).ToArray();
theStore.BulkInsert(data);
@@ -87,7 +509,7 @@ public void specifying_an_index_name_without_marten_prefix_should_prepend_prefix
[Fact]
public void specifying_an_index_name_with_mixed_case_should_result_in_lower_case_name()
{
- StoreOptions(_ => _.Schema.For().FullTextIndex(configure: x=>x.IndexName = "Doesnt_Have_PreFix"));
+ StoreOptions(_ => _.Schema.For().FullTextIndex(configure: x => x.IndexName = "Doesnt_Have_PreFix"));
var data = Target.GenerateRandomData(100).ToArray();
theStore.BulkInsert(data);
@@ -101,7 +523,7 @@ public void specifying_an_index_name_with_mixed_case_should_result_in_lower_case
[Fact]
public void specifying_an_index_name_with_marten_prefix_remains_unchanged()
{
- StoreOptions(_ => _.Schema.For().FullTextIndex(configure: x=>x.IndexName = "mt_i_have_prefix"));
+ StoreOptions(_ => _.Schema.For().FullTextIndex(configure: x => x.IndexName = "mt_i_have_prefix"));
var data = Target.GenerateRandomData(100).ToArray();
theStore.BulkInsert(data);
@@ -112,11 +534,10 @@ public void specifying_an_index_name_with_marten_prefix_remains_unchanged()
ddl.ShouldContain("mt_i_have_prefix");
}
-
[Fact]
public void specifying_an_index_name_with_marten_prefix_and_mixed_case_results_in_lowercase_name()
{
- StoreOptions(_ => _.Schema.For().FullTextIndex(configure: x=>x.IndexName = "mT_I_hAve_preFIX"));
+ StoreOptions(_ => _.Schema.For().FullTextIndex(configure: x => x.IndexName = "mT_I_hAve_preFIX"));
var data = Target.GenerateRandomData(100).ToArray();
theStore.BulkInsert(data);
@@ -126,5 +547,290 @@ public void specifying_an_index_name_with_marten_prefix_and_mixed_case_results_i
ddl.ShouldContain("mt_i_have_prefix");
}
+
+ [Fact]
+ public void creating_a_full_text_index_with_custom_data_configuration_should_create_the_index_without_regConfig_in_indexname_custom_data_configuration()
+ {
+ const string DataConfig = "(data ->> 'AnotherString' || ' ' || 'test')";
+
+ StoreOptions(_ => _.Schema.For().FullTextIndex(
+ index =>
+ {
+ index.DataConfig = DataConfig;
+ }));
+
+ var data = Target.GenerateRandomData(100).ToArray();
+ theStore.BulkInsert(data);
+
+ theStore.Storage
+ .ShouldContainIndexDefinitionFor(
+ indexName: $"mt_doc_target_idx_fts",
+ dataConfig: DataConfig
+ );
+ }
+
+ [Fact]
+ public void creating_a_full_text_index_with_custom_data_configuration_and_custom_regConfig_should_create_the_index_with_custom_regConfig_in_indexname_custom_data_configuration()
+ {
+ const string DataConfig = "(data ->> 'AnotherString' || ' ' || 'test')";
+ const string RegConfig = "french";
+
+ StoreOptions(_ => _.Schema.For().FullTextIndex(
+ index =>
+ {
+ index.RegConfig = RegConfig;
+ index.DataConfig = DataConfig;
+ }));
+
+ var data = Target.GenerateRandomData(100).ToArray();
+ theStore.BulkInsert(data);
+
+ theStore.Storage
+ .ShouldContainIndexDefinitionFor(
+ indexName: $"mt_doc_target_{RegConfig}_idx_fts",
+ regConfig: RegConfig,
+ dataConfig: DataConfig
+ );
+ }
+
+ [Fact]
+ public void creating_a_full_text_index_with_custom_data_configuration_and_custom_regConfig_custom_indexName_should_create_the_index_with_custom_indexname_custom_data_configuration()
+ {
+ const string DataConfig = "(data ->> 'AnotherString' || ' ' || 'test')";
+ const string RegConfig = "french";
+ const string IndexName = "custom_index_name";
+
+ StoreOptions(_ => _.Schema.For().FullTextIndex(
+ index =>
+ {
+ index.DataConfig = DataConfig;
+ index.RegConfig = RegConfig;
+ index.IndexName = IndexName;
+ }));
+
+ var data = Target.GenerateRandomData(100).ToArray();
+ theStore.BulkInsert(data);
+
+ theStore.Storage
+ .ShouldContainIndexDefinitionFor(
+ indexName: $"mt_{IndexName}",
+ regConfig: RegConfig,
+ dataConfig: DataConfig
+ );
+ }
+
+ [Fact]
+ public void creating_a_full_text_index_with_single_member_should_create_the_index_without_regConfig_in_indexname_and_member_selectors()
+ {
+ StoreOptions(_ => _.Schema.For().FullTextIndex(d => d.String));
+
+ var data = Target.GenerateRandomData(100).ToArray();
+ theStore.BulkInsert(data);
+
+ theStore.Storage
+ .ShouldContainIndexDefinitionFor(
+ indexName: $"mt_doc_target_idx_fts",
+ dataConfig: $"((data ->> '{nameof(Target.String)}'))"
+ );
+ }
+
+ [Fact]
+ public void creating_a_full_text_index_with_multiple_members_should_create_the_index_without_regConfig_in_indexname_and_members_selectors()
+ {
+ StoreOptions(_ => _.Schema.For().FullTextIndex(d => d.String, d => d.AnotherString));
+
+ var data = Target.GenerateRandomData(100).ToArray();
+ theStore.BulkInsert(data);
+
+ theStore.Storage
+ .ShouldContainIndexDefinitionFor(
+ indexName: $"mt_doc_target_idx_fts",
+ dataConfig: $"((data ->> '{nameof(Target.String)}') || ' ' || (data ->> '{nameof(Target.AnotherString)}'))"
+ );
+ }
+
+ [Fact]
+ public void creating_a_full_text_index_with_multiple_members_and_custom_configuration_should_create_the_index_with_custom_configuration_and_members_selectors()
+ {
+ const string IndexName = "custom_index_name";
+ const string RegConfig = "french";
+
+ StoreOptions(_ => _.Schema.For().FullTextIndex(
+ index =>
+ {
+ index.IndexName = IndexName;
+ index.RegConfig = RegConfig;
+ },
+ d => d.AnotherString));
+
+ var data = Target.GenerateRandomData(100).ToArray();
+ theStore.BulkInsert(data);
+
+ theStore.Storage
+ .ShouldContainIndexDefinitionFor(
+ indexName: $"mt_{IndexName}",
+ regConfig: RegConfig,
+ dataConfig: $"((data ->> '{nameof(Target.AnotherString)}'))"
+ );
+ }
+
+ [Fact]
+ public void creating_multiple_full_text_index_with_different_regConfigs_and_custom_data_config_should_create_the_indexes_with_different_recConfigs()
+ {
+ const string frenchRegConfig = "french";
+ const string italianRegConfig = "italian";
+
+ StoreOptions(_ => _.Schema.For()
+ .FullTextIndex(frenchRegConfig, d => d.String)
+ .FullTextIndex(italianRegConfig, d => d.AnotherString));
+
+ var data = Target.GenerateRandomData(100).ToArray();
+ theStore.BulkInsert(data);
+
+ theStore.Storage
+ .ShouldContainIndexDefinitionFor(
+ indexName: $"mt_doc_target_{frenchRegConfig}_idx_fts",
+ regConfig: frenchRegConfig,
+ dataConfig: $"((data ->> '{nameof(Target.String)}'))"
+ );
+
+ theStore.Storage
+ .ShouldContainIndexDefinitionFor(
+ indexName: $"mt_doc_target_{italianRegConfig}_idx_fts",
+ regConfig: italianRegConfig,
+ dataConfig: $"((data ->> '{nameof(Target.AnotherString)}'))"
+ );
+ }
+
+ [Fact]
+ public void using_a_full_text_index_through_attribute_on_class_with_default()
+ {
+ StoreOptions(_ => _.RegisterDocumentType());
+
+ theStore.BulkInsert(new[] { new Book { Id = Guid.NewGuid(), Author = "test", Information = "test", Title = "test" } });
+
+ theStore.Storage
+ .ShouldContainIndexDefinitionFor(
+ tableName: "public.mt_doc_book",
+ indexName: $"mt_doc_book_idx_fts",
+ regConfig: FullTextIndex.DefaultRegConfig,
+ dataConfig: $"data"
+ );
+ }
+
+ [Fact]
+ public void using_a_single_property_full_text_index_through_attribute_with_default()
+ {
+ StoreOptions(_ => _.RegisterDocumentType());
+
+ theStore.BulkInsert(new[] { new UserProfile { Id = Guid.NewGuid(), Information = "test" } });
+
+ theStore.Storage
+ .ShouldContainIndexDefinitionFor(
+ tableName: "public.mt_doc_userprofile",
+ indexName: $"mt_doc_userprofile_idx_fts",
+ regConfig: FullTextIndex.DefaultRegConfig,
+ dataConfig: $"((data ->> '{nameof(UserProfile.Information)}'))"
+ );
+ }
+
+ [Fact]
+ public void using_a_single_property_full_text_index_through_attribute_with_custom_settings()
+ {
+ StoreOptions(_ => _.RegisterDocumentType());
+
+ theStore.BulkInsert(new[] { new UserDetails { Id = Guid.NewGuid(), Details = "test" } });
+
+ theStore.Storage
+ .ShouldContainIndexDefinitionFor(
+ tableName: "public.mt_doc_userdetails",
+ indexName: "mt_custom_user_details_fts_idx",
+ regConfig: "italian",
+ dataConfig: $"((data ->> '{nameof(UserDetails.Details)}'))"
+ );
+ }
+
+ [Fact]
+ public void using_multiple_properties_full_text_index_through_attribute_with_default()
+ {
+ StoreOptions(_ => _.RegisterDocumentType());
+
+ theStore.BulkInsert(new[] { new Article { Id = Guid.NewGuid(), Heading = "test", Text = "test" } });
+
+ theStore.Storage
+ .ShouldContainIndexDefinitionFor(
+ tableName: "public.mt_doc_article",
+ indexName: $"mt_doc_article_idx_fts",
+ regConfig: FullTextIndex.DefaultRegConfig,
+ dataConfig: $"((data ->> '{nameof(Article.Heading)}') || ' ' || (data ->> '{nameof(Article.Text)}'))"
+ );
+ }
+
+ [Fact]
+ public void using_multiple_properties_full_text_index_through_attribute_with_custom_settings()
+ {
+ const string frenchRegConfig = "french";
+ const string italianRegConfig = "italian";
+
+ StoreOptions(_ => _.RegisterDocumentType());
+
+ theStore.BulkInsert(new[] { new BlogPost { Id = Guid.NewGuid(), Category = "test", EnglishText = "test", FrenchText = "test", ItalianText = "test" } });
+
+ theStore.Storage
+ .ShouldContainIndexDefinitionFor(
+ tableName: "public.mt_doc_blogpost",
+ indexName: $"mt_doc_blogpost_idx_fts",
+ regConfig: FullTextIndex.DefaultRegConfig,
+ dataConfig: $"((data ->> '{nameof(BlogPost.EnglishText)}'))"
+ );
+
+ theStore.Storage
+ .ShouldContainIndexDefinitionFor(
+ tableName: "public.mt_doc_blogpost",
+ indexName: $"mt_doc_blogpost_{frenchRegConfig}_idx_fts",
+ regConfig: frenchRegConfig,
+ dataConfig: $"((data ->> '{nameof(BlogPost.FrenchText)}'))"
+ );
+
+ theStore.Storage
+ .ShouldContainIndexDefinitionFor(
+ tableName: "public.mt_doc_blogpost",
+ indexName: $"mt_doc_blogpost_{italianRegConfig}_idx_fts",
+ regConfig: italianRegConfig,
+ dataConfig: $"((data ->> '{nameof(BlogPost.ItalianText)}'))"
+ );
+ }
+ }
+
+ public static class FullTextIndexTestsExtension
+ {
+ public static void ShouldContainIndexDefinitionFor(
+ this StorageFeatures storage,
+ string tableName = "public.mt_doc_target",
+ string indexName = "mt_doc_target_idx_fts",
+ string regConfig = "english",
+ string dataConfig = null)
+ {
+ var ddl = storage.MappingFor(typeof(TDocument)).Indexes
+ .Where(x => x.IndexName == indexName)
+ .Select(x => x.ToDDL())
+ .FirstOrDefault();
+
+ ddl.ShouldNotBeNull();
+
+ ddl.ShouldContain($"CREATE INDEX {indexName}");
+ ddl.ShouldContain($"ON {tableName}");
+ ddl.ShouldContain($"to_tsvector('{regConfig}', {dataConfig})");
+
+ if (regConfig != null)
+ {
+ ddl.ShouldContain(regConfig);
+ }
+
+ if (dataConfig != null)
+ {
+ ddl.ShouldContain(dataConfig);
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/Marten/FullTextIndexAttribute.cs b/src/Marten/FullTextIndexAttribute.cs
new file mode 100644
index 0000000000..cfa73f2832
--- /dev/null
+++ b/src/Marten/FullTextIndexAttribute.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Linq;
+using System.Reflection;
+
+namespace Marten.Schema
+{
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class)]
+ public class FullTextIndexAttribute : MartenAttribute
+ {
+ public override void Modify(DocumentMapping mapping)
+ {
+ mapping.AddFullTextIndex(regConfig: RegConfig, (index) => { index.IndexName = IndexName; });
+ }
+
+ public override void Modify(DocumentMapping mapping, MemberInfo member)
+ {
+ var membersGroupedByIndexName = member.DeclaringType.GetMembers()
+ .Where(mi => mi.GetCustomAttributes().Any())
+ .Select(mi => new
+ {
+ Member = mi,
+ IndexInformation = mi.GetCustomAttributes().First()
+ })
+ .GroupBy(m => m.IndexInformation.IndexName ?? m.IndexInformation.RegConfig ?? m.Member.Name)
+ .Where(mg => mg.Any(m => m.Member == member))
+ .Single();
+
+ mapping.AddFullTextIndex(
+ membersGroupedByIndexName.Select(mg => new[] { mg.Member }).ToArray(),
+ regConfig: RegConfig,
+ indexName: IndexName);
+ }
+
+ ///
+ /// Specify the name of the index explicity
+ ///
+ public string IndexName { get; set; } = null;
+
+ ///
+ /// Specify Index type
+ ///
+ public string RegConfig = FullTextIndex.DefaultRegConfig;
+ }
+}
\ No newline at end of file
diff --git a/src/Marten/IQuerySession.cs b/src/Marten/IQuerySession.cs
index 7ab67f10b6..eb8b3353de 100644
--- a/src/Marten/IQuerySession.cs
+++ b/src/Marten/IQuerySession.cs
@@ -3,6 +3,7 @@
using System.Threading;
using System.Threading.Tasks;
using Marten.Linq;
+using Marten.Schema;
using Marten.Services.BatchQuerying;
using Marten.Storage;
using Npgsql;
@@ -79,7 +80,6 @@ public interface IQuerySession : IDisposable
///
Task LoadAsync(Guid id, CancellationToken token = default(CancellationToken));
-
// SAMPLE: querying_with_linq
///
/// Use Linq operators to query the documents
@@ -88,6 +88,7 @@ public interface IQuerySession : IDisposable
///
///
IMartenQueryable Query();
+
// ENDSAMPLE
///
@@ -110,7 +111,7 @@ public interface IQuerySession : IDisposable
Task> QueryAsync(string sql, CancellationToken token = default(CancellationToken), params object[] parameters);
///
- /// Define a batch of deferred queries and load operations to be conducted in one asynchronous request to the
+ /// Define a batch of deferred queries and load operations to be conducted in one asynchronous request to the
/// database for potentially performance
///
///
@@ -121,7 +122,6 @@ public interface IQuerySession : IDisposable
///
NpgsqlConnection Connection { get; }
-
///
/// The session specific logger for this session. Can be set for better integration
/// with custom diagnostics
@@ -138,7 +138,6 @@ public interface IQuerySession : IDisposable
///
IDocumentStore DocumentStore { get; }
-
///
/// A query that is compiled so a copy of the DbCommand can be used directly in subsequent requests.
///
@@ -158,7 +157,6 @@ public interface IQuerySession : IDisposable
/// A task for a single item query result
Task QueryAsync(ICompiledQuery query, CancellationToken token = default(CancellationToken));
-
///
/// Load or find multiple documents by id
///
@@ -264,60 +262,60 @@ public interface IQuerySession : IDisposable
/// Performs a full text search against
///
/// The text to search for. May contain lexeme patterns used by PostgreSQL for full text searching
- /// The dictionary config passed to the 'to_tsquery' function, must match the config parameter used by
+ /// The dictionary config passed to the 'to_tsquery' function, must match the config parameter used by
///
/// See: https://www.postgresql.org/docs/10/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
///
- IReadOnlyList Search(string queryText, string config = "english");
+ IReadOnlyList Search(string queryText, string regConfig = FullTextIndex.DefaultRegConfig);
///
/// Performs an asynchronous full text search against
///
/// The text to search for. May contain lexeme patterns used by PostgreSQL for full text searching
- /// The dictionary config passed to the 'to_tsquery' function, must match the config parameter used by
+ /// The dictionary config passed to the 'to_tsquery' function, must match the config parameter used by
///
/// See: https://www.postgresql.org/docs/10/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
///
- Task> SearchAsync(string queryText, string config = "english", CancellationToken token = default);
+ Task> SearchAsync(string queryText, string regConfig = FullTextIndex.DefaultRegConfig, CancellationToken token = default);
///
/// Performs a full text search against using the 'plainto_tsquery' search function
///
/// The text to search for. May contain lexeme patterns used by PostgreSQL for full text searching
- /// The dictionary config passed to the 'to_tsquery' function, must match the config parameter used by
+ /// The dictionary config passed to the 'to_tsquery' function, must match the config parameter used by
///
/// See: https://www.postgresql.org/docs/10/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
///
- IReadOnlyList PlainTextSearch(string searchTerm, string config = "english");
+ IReadOnlyList PlainTextSearch(string searchTerm, string regConfig = FullTextIndex.DefaultRegConfig);
///
/// Performs an asynchronous full text search against using the 'plainto_tsquery' function
///
/// The text to search for. May contain lexeme patterns used by PostgreSQL for full text searching
- /// The dictionary config passed to the 'to_tsquery' function, must match the config parameter used by
+ /// The dictionary config passed to the 'to_tsquery' function, must match the config parameter used by
///
/// See: https://www.postgresql.org/docs/10/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
///
- Task> PlainTextSearchAsync(string searchTerm, string config = "english", CancellationToken token = default);
+ Task> PlainTextSearchAsync(string searchTerm, string regConfig = FullTextIndex.DefaultRegConfig, CancellationToken token = default);
///
/// Performs a full text search against using the 'phraseto_tsquery' search function
///
/// The text to search for. May contain lexeme patterns used by PostgreSQL for full text searching
- /// The dictionary config passed to the 'to_tsquery' function, must match the config parameter used by
+ /// The dictionary config passed to the 'to_tsquery' function, must match the config parameter used by
///
/// See: https://www.postgresql.org/docs/10/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
///
- IReadOnlyList PhraseSearch(string searchTerm, string config = "english");
+ IReadOnlyList PhraseSearch(string searchTerm, string regConfig = FullTextIndex.DefaultRegConfig);
///
/// Performs an asynchronous full text search against using the 'phraseto_tsquery' search function
///
/// The text to search for. May contain lexeme patterns used by PostgreSQL for full text searching
- /// The dictionary config passed to the 'to_tsquery' function, must match the config parameter used by
+ /// The dictionary config passed to the 'to_tsquery' function, must match the config parameter used by
///
/// See: https://www.postgresql.org/docs/10/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
///
- Task> PhraseSearchAsync(string searchTerm, string config = "english", CancellationToken token = default);
+ Task> PhraseSearchAsync(string searchTerm, string regConfig = FullTextIndex.DefaultRegConfig, CancellationToken token = default);
}
}
\ No newline at end of file
diff --git a/src/Marten/Linq/IMartenQueryable.cs b/src/Marten/Linq/IMartenQueryable.cs
index b67eae8469..778ee29af6 100644
--- a/src/Marten/Linq/IMartenQueryable.cs
+++ b/src/Marten/Linq/IMartenQueryable.cs
@@ -15,22 +15,33 @@ public interface IMartenQueryable
{
IEnumerable Includes { get; }
- QueryStatistics Statistics { get; }
- Task> ToListAsync(CancellationToken token);
- Task AnyAsync(CancellationToken token);
- Task CountAsync(CancellationToken token);
- Task CountLongAsync(CancellationToken token);
- Task FirstAsync(CancellationToken token);
- Task FirstOrDefaultAsync(CancellationToken token);
- Task SingleAsync(CancellationToken token);
+ QueryStatistics Statistics { get; }
+
+ Task> ToListAsync(CancellationToken token);
+
+ Task AnyAsync(CancellationToken token);
+
+ Task CountAsync(CancellationToken token);
+
+ Task CountLongAsync(CancellationToken token);
+
+ Task FirstAsync(CancellationToken token);
+
+ Task FirstOrDefaultAsync(CancellationToken token);
+
+ Task SingleAsync(CancellationToken token);
+
Task SingleOrDefaultAsync(CancellationToken token);
- Task SumAsync(CancellationToken token);
- Task MinAsync(CancellationToken token);
- Task MaxAsync(CancellationToken token);
+ Task SumAsync(CancellationToken token);
+
+ Task MinAsync(CancellationToken token);
+
+ Task MaxAsync(CancellationToken token);
+
Task AverageAsync(CancellationToken token);
- /// Configure EXPLAIN options as documented in EXPLAIN documentation
+ /// Configure EXPLAIN options as documented in EXPLAIN documentation
QueryPlan Explain(FetchType fetchType = FetchType.FetchMany, Action configureExplain = null);
///
@@ -43,7 +54,6 @@ public interface IMartenQueryable
IQueryable TransformTo(string transformName);
}
-
public interface IMartenQueryable : IQueryable, IMartenQueryable
{
IMartenQueryable Include(Expression> idSource, Action callback,
diff --git a/src/Marten/Linq/IWhereFragment.cs b/src/Marten/Linq/IWhereFragment.cs
index e667167906..19ce7e5e48 100644
--- a/src/Marten/Linq/IWhereFragment.cs
+++ b/src/Marten/Linq/IWhereFragment.cs
@@ -8,7 +8,8 @@ namespace Marten.Linq
{
public interface IWhereFragment
{
- void Apply(CommandBuilder builder);
+ void Apply(CommandBuilder builder);
+
bool Contains(string sqlText);
}
@@ -28,12 +29,13 @@ public static IWhereFragment Append(this IWhereFragment fragment, IWhereFragment
}
return new CompoundWhereFragment("and", fragment, other);
-
}
public static IWhereFragment Append(this IWhereFragment fragment, IWhereFragment[] others)
{
- if (!others.Any()) return fragment;
+ if (others?.Any() == false) return fragment;
+
+ if (fragment == null) return Append(others.First(), others.Skip(1).ToArray());
foreach (var other in others)
{
@@ -52,7 +54,7 @@ public static IWhereFragment[] Flatten(this IWhereFragment fragment)
return fragment.As().Children.ToArray();
}
- return new IWhereFragment[] {fragment};
+ return new IWhereFragment[] { fragment };
}
public static string ToSql(this IWhereFragment fragment)
@@ -70,6 +72,5 @@ public static bool SpecifiesTenant(this IWhereFragment fragment)
{
return fragment.Flatten().OfType().Any();
}
-
}
}
\ No newline at end of file
diff --git a/src/Marten/Linq/MartenExpressionParser.cs b/src/Marten/Linq/MartenExpressionParser.cs
index 12c421292f..7a72dd05fd 100644
--- a/src/Marten/Linq/MartenExpressionParser.cs
+++ b/src/Marten/Linq/MartenExpressionParser.cs
@@ -8,7 +8,6 @@
using Marten.Linq.Parsing;
using Marten.Linq.SoftDeletes;
using Marten.Schema;
-using Marten.Util;
using Remotion.Linq;
using Remotion.Linq.Clauses.Expressions;
using Remotion.Linq.Clauses.ResultOperators;
@@ -66,8 +65,8 @@ public IWhereFragment ParseWhereFragment(IQueryableDocument mapping, Expression
new EnumerableContains(),
new StringEndsWith(),
new StringStartsWith(),
- new StringEquals(),
- new SimpleEqualsParser(),
+ new StringEquals(),
+ new SimpleEqualsParser(),
// Added
new IsOneOf(),
@@ -95,18 +94,23 @@ public IWhereFragment ParseWhereFragment(IQueryableDocument mapping, Expression
new MatchesSqlParser(),
// dictionaries
- new DictionaryExpressions()
+ new DictionaryExpressions(),
+
+ // full text search
+ new Search(),
+ new PhraseSearch(),
+ new PlainTextSearch()
};
private static readonly object[] _supplementalParsers = new[]
{
- new SimpleBinaryComparisonExpressionParser(),
+ new SimpleBinaryComparisonExpressionParser(),
};
private IWhereFragment buildSimpleWhereClause(IQueryableDocument mapping, BinaryExpression binary)
{
- var isValueExpressionOnRight = binary.Right.IsValueExpression();
-
+ var isValueExpressionOnRight = binary.Right.IsValueExpression();
+
var isSubQuery = isValueExpressionOnRight
? binary.Left is SubQueryExpression
: binary.Right is SubQueryExpression;
@@ -125,8 +129,8 @@ private IWhereFragment buildSimpleWhereClause(IQueryableDocument mapping, Binary
if (parser != null)
{
- var where = parser.Parse(mapping, _serializer, binary);
-
+ var where = parser.Parse(mapping, _serializer, binary);
+
return where;
}
@@ -154,4 +158,4 @@ private static object moduloByValue(BinaryExpression binary)
return moduloValueExpression != null ? moduloValueExpression.Value : 1;
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Marten/Linq/MartenQueryExecutor.cs b/src/Marten/Linq/MartenQueryExecutor.cs
index 35e31f9b8d..68f75192b0 100644
--- a/src/Marten/Linq/MartenQueryExecutor.cs
+++ b/src/Marten/Linq/MartenQueryExecutor.cs
@@ -31,7 +31,6 @@ public MartenQueryExecutor(IManagedConnection runner, DocumentStore store, IIden
public ITenant Tenant { get; }
public QueryStatistics Statistics { get; set; }
-
T IQueryExecutor.ExecuteScalar(QueryModel queryModel)
{
var handler = Store.HandlerFactory.HandlerForScalarQuery(queryModel, Includes.ToArray(),
@@ -44,7 +43,6 @@ T IQueryExecutor.ExecuteScalar(QueryModel queryModel)
return Connection.Fetch(handler, IdentityMap.ForQuery(), Statistics, Tenant);
}
-
T IQueryExecutor.ExecuteSingle(QueryModel queryModel, bool returnDefaultWhenEmpty)
{
var handler = Store.HandlerFactory.HandlerForSingleQuery(queryModel, _includes.ToArray(), Statistics,
diff --git a/src/Marten/Linq/MartenQueryable.cs b/src/Marten/Linq/MartenQueryable.cs
index 533048e751..f10e62754e 100644
--- a/src/Marten/Linq/MartenQueryable.cs
+++ b/src/Marten/Linq/MartenQueryable.cs
@@ -47,7 +47,6 @@ public IQueryable TransformTo(string transformName)
return this.Select(x => x.TransformTo(transformName));
}
-
public IEnumerable Includes
{
get
@@ -88,7 +87,6 @@ public IMartenQueryable Include(Expression> idSourc
return this;
}
-
public IMartenQueryable Include(Expression> idSource, IList list,
JoinType joinType = JoinType.Inner)
{
@@ -190,7 +188,6 @@ public LinqQuery ToLinqQuery()
return new LinqQuery(Store, query, Includes.ToArray(), Statistics);
}
-
private IQueryHandler toDiagnosticHandler(FetchType fetchType)
{
switch (fetchType)
@@ -218,7 +215,6 @@ public NpgsqlCommand BuildCommand(FetchType fetchType)
return CommandBuilder.ToCommand(Tenant, handler);
}
-
private Task executeAsync(Func, IQueryHandler> source,
CancellationToken token)
{
@@ -230,6 +226,6 @@ private Task executeAsync(Func, IQueryHandler(_store, Model, new IIncludeJoin[0], null).AppendWhere(sql);
}
@@ -187,7 +186,6 @@ public void ConfigureAggregate(CommandBuilder sql, string @operator)
AppendWhere(sql);
}
-
public IQueryHandler> ToList()
{
return new ListQueryHandler(this);
@@ -207,8 +205,6 @@ private void writeOrderClause(CommandBuilder sql)
}
}
-
-
private void writeOrderByFragment(CommandBuilder sql, Ordering clause)
{
var locator = _mapping.JsonLocator(clause.Expression);
@@ -225,7 +221,8 @@ private IWhereFragment buildWhereFragment()
var bodies = bodyClauses();
var wheres = bodies.OfType().ToArray();
- if (wheres.Length == 0) return _mapping.DefaultWhereFragment();
+ if (wheres.Length == 0)
+ return _mapping.DefaultWhereFragment();
var where = wheres.Length == 1
? _store.Parser.ParseWhereFragment(_mapping, wheres.Single().Predicate)
diff --git a/src/Marten/Linq/Parsing/FullTextSearchMethodCallParser.cs b/src/Marten/Linq/Parsing/FullTextSearchMethodCallParser.cs
new file mode 100644
index 0000000000..5660c78c61
--- /dev/null
+++ b/src/Marten/Linq/Parsing/FullTextSearchMethodCallParser.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Linq.Expressions;
+using Marten.Linq.WhereFragments;
+using Marten.Schema;
+
+namespace Marten.Linq.Parsing
+{
+ public enum FullTextSearchFunction
+ {
+ to_tsquery,
+ plainto_tsquery,
+ phraseto_tsquery
+ }
+
+ public abstract class FullTextSearchMethodCallParser : IMethodCallParser
+ {
+ private readonly string methodName;
+ private readonly FullTextSearchFunction searchFunction;
+
+ protected FullTextSearchMethodCallParser(string methodName, FullTextSearchFunction searchFunction)
+ {
+ this.methodName = methodName;
+ this.searchFunction = searchFunction;
+ }
+
+ public bool Matches(MethodCallExpression expression)
+ {
+ return expression.Method.Name == methodName
+ && expression.Method.DeclaringType == typeof(LinqExtensions);
+ }
+
+ public IWhereFragment Parse(IQueryableDocument mapping, ISerializer serializer, MethodCallExpression expression)
+ {
+ if (expression.Arguments.Count < 2 || expression.Arguments[1].Value() == null)
+ throw new ArgumentException("Search Term needs to be provided", "searchTerm");
+
+ if (expression.Arguments[1].Type != typeof(string))
+ throw new ArgumentException("Search Term needs to be string", "searchTerm");
+
+ if (expression.Arguments.Count > 2 && expression.Arguments[2].Type != typeof(string))
+ throw new ArgumentException("Reg config needs to be string", "regConfig");
+
+ var searchTerm = (string)expression.Arguments[1].Value();
+
+ var regConfig = expression.Arguments.Count > 2 ?
+ expression.Arguments[2].Value() as string : FullTextIndex.DefaultRegConfig;
+
+ return new FullTextWhereFragment(
+ mapping as DocumentMapping,
+ searchFunction,
+ searchTerm,
+ regConfig);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Marten/Linq/Parsing/PhraseSearch.cs b/src/Marten/Linq/Parsing/PhraseSearch.cs
new file mode 100644
index 0000000000..e114be6573
--- /dev/null
+++ b/src/Marten/Linq/Parsing/PhraseSearch.cs
@@ -0,0 +1,9 @@
+namespace Marten.Linq.Parsing
+{
+ public class PhraseSearch : FullTextSearchMethodCallParser
+ {
+ public PhraseSearch() : base(nameof(LinqExtensions.PhraseSearch), FullTextSearchFunction.phraseto_tsquery)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Marten/Linq/Parsing/PlainTextSearch.cs b/src/Marten/Linq/Parsing/PlainTextSearch.cs
new file mode 100644
index 0000000000..282a7adaf8
--- /dev/null
+++ b/src/Marten/Linq/Parsing/PlainTextSearch.cs
@@ -0,0 +1,9 @@
+namespace Marten.Linq.Parsing
+{
+ public class PlainTextSearch : FullTextSearchMethodCallParser
+ {
+ public PlainTextSearch() : base(nameof(LinqExtensions.PlainTextSearch), FullTextSearchFunction.plainto_tsquery)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Marten/Linq/Parsing/Search.cs b/src/Marten/Linq/Parsing/Search.cs
new file mode 100644
index 0000000000..26946c4262
--- /dev/null
+++ b/src/Marten/Linq/Parsing/Search.cs
@@ -0,0 +1,9 @@
+namespace Marten.Linq.Parsing
+{
+ public class Search : FullTextSearchMethodCallParser
+ {
+ public Search() : base(nameof(LinqExtensions.Search), FullTextSearchFunction.to_tsquery)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Marten/Linq/WhereFragments/FullTextWhereFragment.cs b/src/Marten/Linq/WhereFragments/FullTextWhereFragment.cs
new file mode 100644
index 0000000000..513f2560a0
--- /dev/null
+++ b/src/Marten/Linq/WhereFragments/FullTextWhereFragment.cs
@@ -0,0 +1,51 @@
+using System.Linq;
+using Marten.Linq.Parsing;
+using Marten.Schema;
+using Marten.Util;
+
+namespace Marten.Linq.WhereFragments
+{
+ internal class FullTextWhereFragment : IWhereFragment
+ {
+ private readonly string _regConfig;
+ private readonly string _dataConfig;
+ private readonly FullTextSearchFunction _searchFunction;
+ private readonly string _searchTerm;
+
+ private string Sql => $"to_tsvector(:argRegConfig::regconfig, {_dataConfig}) @@ {_searchFunction}(:argRegConfig::regconfig, :argSearchTerm)";
+
+ public FullTextWhereFragment(DocumentMapping mapping, FullTextSearchFunction searchFunction, string searchTerm, string regConfig = FullTextIndex.DefaultRegConfig)
+ {
+ _regConfig = regConfig;
+ _dataConfig = GetDataConfig(mapping, regConfig);
+ _searchFunction = searchFunction;
+ _searchTerm = searchTerm;
+ }
+
+ public void Apply(CommandBuilder builder)
+ {
+ builder.AddNamedParameter("argRegConfig", _regConfig);
+ builder.AddNamedParameter("argSearchTerm", _searchTerm);
+
+ builder.Append(Sql);
+ }
+
+ public bool Contains(string sqlText)
+ {
+ return Sql.Contains(sqlText);
+ }
+
+ private static string GetDataConfig(DocumentMapping mapping, string regConfig)
+ {
+ if (mapping == null)
+ return FullTextIndex.DefaultDataConfig;
+
+ return mapping
+ .Indexes
+ .OfType()
+ .Where(i => i.RegConfig == regConfig)
+ .Select(i => i.DataConfig)
+ .FirstOrDefault() ?? FullTextIndex.DefaultDataConfig;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Marten/LinqExtensions.cs b/src/Marten/LinqExtensions.cs
index 7ebfe857e1..3cb6ad624e 100644
--- a/src/Marten/LinqExtensions.cs
+++ b/src/Marten/LinqExtensions.cs
@@ -1,7 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using Baseline;
-
+using Marten.Schema;
+
namespace Marten
{
public static class LinqExtensions
@@ -110,5 +111,80 @@ public static bool TenantIsOneOf(this T variable, params string[] tenantIds)
{
return true;
}
+
+ ///
+ /// Performs a full text search against
+ ///
+ /// The text to search for. May contain lexeme patterns used by PostgreSQL for full text searching
+ ///
+ /// See: https://www.postgresql.org/docs/10/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
+ ///
+ public static bool Search(this T variable, string searchTerm)
+ {
+ return true;
+ }
+
+ ///
+ /// Performs a full text search against
+ ///
+ /// The text to search for. May contain lexeme patterns used by PostgreSQL for full text searching
+ /// The dictionary config passed to the 'to_tsquery' function, must match the config parameter used by
+ ///
+ /// See: https://www.postgresql.org/docs/10/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
+ ///
+ public static bool Search(this T variable, string searchTerm, string regConfig)
+ {
+ return true;
+ }
+
+ ///
+ /// Performs a full text search against using the 'plainto_tsquery' search function
+ ///
+ /// The text to search for. May contain lexeme patterns used by PostgreSQL for full text searching
+ ///
+ /// See: https://www.postgresql.org/docs/10/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
+ ///
+ public static bool PlainTextSearch(this T variable, string searchTerm)
+ {
+ return true;
+ }
+
+ ///
+ /// Performs a full text search against using the 'plainto_tsquery' search function
+ ///
+ /// The text to search for. May contain lexeme patterns used by PostgreSQL for full text searching
+ /// The dictionary config passed to the 'to_tsquery' function, must match the config parameter used by
+ ///
+ /// See: https://www.postgresql.org/docs/10/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
+ ///
+ public static bool PlainTextSearch(this T variable, string searchTerm, string regConfig)
+ {
+ return true;
+ }
+
+ ///
+ /// Performs a full text search against using the 'phraseto_tsquery' search function
+ ///
+ /// The text to search for. May contain lexeme patterns used by PostgreSQL for full text searching
+ ///
+ /// See: https://www.postgresql.org/docs/10/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
+ ///
+ public static bool PhraseSearch(this T variable, string searchTerm)
+ {
+ return true;
+ }
+
+ ///
+ /// Performs a full text search against using the 'phraseto_tsquery' search function
+ ///
+ /// The text to search for. May contain lexeme patterns used by PostgreSQL for full text searching
+ /// The dictionary config passed to the 'to_tsquery' function, must match the config parameter used by
+ ///
+ /// See: https://www.postgresql.org/docs/10/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
+ ///
+ public static bool PhraseSearch(this T variable, string searchTerm, string regConfig)
+ {
+ return true;
+ }
}
}
\ No newline at end of file
diff --git a/src/Marten/Marten.csproj b/src/Marten/Marten.csproj
index 2a2c33bbb5..8715ae12f8 100644
--- a/src/Marten/Marten.csproj
+++ b/src/Marten/Marten.csproj
@@ -68,6 +68,6 @@
snupkg
-
+
diff --git a/src/Marten/MartenRegistry.cs b/src/Marten/MartenRegistry.cs
index b728d18443..8cf88d170b 100644
--- a/src/Marten/MartenRegistry.cs
+++ b/src/Marten/MartenRegistry.cs
@@ -187,7 +187,20 @@ public DocumentMappingExpression UniqueIndex(params Expression
/// Creates a unique index on this data member within the JSON data storage
///
- ///
+ /// Name of the index
+ ///
+ ///
+ public DocumentMappingExpression UniqueIndex(string indexName, params Expression>[] expressions)
+ {
+ alter = m => m.UniqueIndex(indexName, expressions);
+
+ return this;
+ }
+
+ ///
+ /// Creates a unique index on this data member within the JSON data storage
+ ///
+ /// Type of the index
///
///
public DocumentMappingExpression UniqueIndex(UniqueIndexType indexType, params Expression>[] expressions)
@@ -197,6 +210,20 @@ public DocumentMappingExpression UniqueIndex(UniqueIndexType indexType, param
return this;
}
+ ///
+ /// Creates a unique index on this data member within the JSON data storage
+ ///
+ /// Type of the index
+ /// Name of the index
+ ///
+ ///
+ public DocumentMappingExpression UniqueIndex(UniqueIndexType indexType, string indexName, params Expression>[] expressions)
+ {
+ alter = m => m.UniqueIndex(indexType, indexName, expressions);
+
+ return this;
+ }
+
///
/// Creates an index on the predefined Last Modified column
///
@@ -209,9 +236,33 @@ public DocumentMappingExpression IndexLastModified(Action co
return this;
}
- public DocumentMappingExpression FullTextIndex(string config = "english", Action configure = null)
+ public DocumentMappingExpression FullTextIndex(string regConfig = Schema.FullTextIndex.DefaultRegConfig, Action configure = null)
+ {
+ alter = m => m.AddFullTextIndex(regConfig, configure);
+ return this;
+ }
+
+ public DocumentMappingExpression FullTextIndex(Action configure)
+ {
+ alter = m => m.AddFullTextIndex(Schema.FullTextIndex.DefaultRegConfig, configure);
+ return this;
+ }
+
+ public DocumentMappingExpression FullTextIndex(params Expression>[] expressions)
+ {
+ FullTextIndex(Schema.FullTextIndex.DefaultRegConfig, expressions);
+ return this;
+ }
+
+ public DocumentMappingExpression FullTextIndex(string regConfig, params Expression>[] expressions)
+ {
+ alter = m => m.FullTextIndex(regConfig, expressions);
+ return this;
+ }
+
+ public DocumentMappingExpression FullTextIndex(Action configure, params Expression>[] expressions)
{
- alter = m => m.AddFullTextIndex(config, configure);
+ alter = m => m.FullTextIndex(configure, expressions);
return this;
}
diff --git a/src/Marten/QuerySession.cs b/src/Marten/QuerySession.cs
index 41c4fc7dcb..c07b6ee19f 100644
--- a/src/Marten/QuerySession.cs
+++ b/src/Marten/QuerySession.cs
@@ -40,6 +40,7 @@ public QuerySession(DocumentStore store, IManagedConnection connection, IQueryPa
}
public ISerializer Serializer { get; }
+
public Guid? VersionFor(TDoc entity)
{
var id = _store.Storage.StorageFor(typeof(TDoc)).Identity(entity);
@@ -274,7 +275,6 @@ private IEnumerable fetchDocuments(TKey[] keys)
var resolver = storage.As>();
var cmd = storage.LoadByArrayCommand(keys);
cmd.AddTenancy(_parent.Tenant);
-
var list = new List();
@@ -353,7 +353,6 @@ public IMartenSessionLogger Logger
public int RequestCount => _connection.RequestCount;
-
~QuerySession()
{
Dispose();
@@ -398,50 +397,34 @@ public T Load(Guid id)
return loadAsync(id, token);
}
- public IReadOnlyList Search(string searchTerm, string config = "english")
- {
- return DoFullTextSearch(searchTerm, config, "to_tsquery");
- }
-
- public Task> SearchAsync(string searchTerm, string config = "english", CancellationToken token = default)
- {
- return DoFullTextSearchAsync(searchTerm, config, "to_tsquery", token);
- }
-
- public IReadOnlyList PlainTextSearch(string searchTerm, string config = "english")
+ public IReadOnlyList Search(string searchTerm, string regConfig = FullTextIndex.DefaultRegConfig)
{
- return DoFullTextSearch(searchTerm, config, "plainto_tsquery");
+ return Query().Where(d => d.Search(searchTerm, regConfig)).ToList();
}
- public Task> PlainTextSearchAsync(string searchTerm, string config = "english", CancellationToken token = default)
+ public Task> SearchAsync(string searchTerm, string regConfig = FullTextIndex.DefaultRegConfig, CancellationToken token = default)
{
- return DoFullTextSearchAsync(searchTerm, config, "plainto_tsquery", token);
+ return Query().Where(d => d.Search(searchTerm, regConfig)).ToListAsync();
}
- public IReadOnlyList PhraseSearch(string searchTerm, string config = "english")
+ public IReadOnlyList PlainTextSearch(string searchTerm, string regConfig = FullTextIndex.DefaultRegConfig)
{
- return DoFullTextSearch(searchTerm, config, "phraseto_tsquery");
+ return Query().Where(d => d.PlainTextSearch(searchTerm, regConfig)).ToList();
}
- public Task> PhraseSearchAsync(string searchTerm, string config = "english", CancellationToken token = default)
+ public Task> PlainTextSearchAsync(string searchTerm, string regConfig = FullTextIndex.DefaultRegConfig, CancellationToken token = default)
{
- return DoFullTextSearchAsync(searchTerm, config, "phraseto_tsquery", token);
+ return Query().Where(d => d.PlainTextSearch(searchTerm, regConfig)).ToListAsync();
}
- private IReadOnlyList DoFullTextSearch(string searchTerm, string config, string searchFunction)
+ public IReadOnlyList PhraseSearch(string searchTerm, string regConfig = FullTextIndex.DefaultRegConfig)
{
- assertNotDisposed();
- var sql = $"where to_tsvector('{config}', data) @@ {searchFunction}('{config}', '{searchTerm}')";
- var handler = new UserSuppliedQueryHandler(_store, sql, new object[0]);
- return _connection.Fetch(handler, _identityMap.ForQuery(), null, Tenant);
+ return Query().Where(d => d.PhraseSearch(searchTerm, regConfig)).ToList();
}
- private Task> DoFullTextSearchAsync(string searchTerm, string config, string searchFunction, CancellationToken token)
+ public Task> PhraseSearchAsync(string searchTerm, string regConfig = FullTextIndex.DefaultRegConfig, CancellationToken token = default)
{
- assertNotDisposed();
- var sql = $"where to_tsvector('{config}', data) @@ {searchFunction}('{config}', '{searchTerm}')";
- var handler = new UserSuppliedQueryHandler(_store, sql, new object[0]);
- return _connection.FetchAsync(handler, _identityMap.ForQuery(), null, Tenant, token);
+ return Query().Where(d => d.PhraseSearch(searchTerm, regConfig)).ToListAsync();
}
}
}
\ No newline at end of file
diff --git a/src/Marten/QueryableExtensions.cs b/src/Marten/QueryableExtensions.cs
index abcfca0802..4d6c1b80b3 100644
--- a/src/Marten/QueryableExtensions.cs
+++ b/src/Marten/QueryableExtensions.cs
@@ -26,7 +26,7 @@ public static Task> ToListAsync(this IQueryable queryable
return queryable.As().ToListAsync(token);
}
- #endregion
+ #endregion ToList
#region Any
@@ -36,7 +36,6 @@ public static Task AnyAsync(
{
if (source == null) throw new ArgumentNullException(nameof(source));
-
return source.As().AnyAsync(token);
}
@@ -51,7 +50,7 @@ public static Task AnyAsync(
return source.Where(predicate).AnyAsync(token);
}
- #endregion
+ #endregion Any
#region Aggregate Functions
@@ -64,7 +63,6 @@ public static Task SumAsync(
return source.Select(expression).As().SumAsync(token);
}
-
public static Task MaxAsync(
this IQueryable source, Expression> expression,
CancellationToken token = default(CancellationToken))
@@ -74,7 +72,6 @@ public static Task MaxAsync(
return source.Select(expression).As().MaxAsync(token);
}
-
public static Task MinAsync(
this IQueryable source, Expression> expression,
CancellationToken token = default(CancellationToken))
@@ -93,7 +90,7 @@ public static Task AverageAsync(
return source.Select(expression).As().AverageAsync(token);
}
- #endregion
+ #endregion Aggregate Functions
#region Count/LongCount/Sum
@@ -106,7 +103,6 @@ public static Task CountAsync(
return source.As().CountAsync(token);
}
-
public static Task CountAsync(
this IQueryable source,
Expression> predicate,
@@ -118,7 +114,6 @@ public static Task CountAsync(
return source.Where(predicate).CountAsync(token);
}
-
public static Task LongCountAsync(
this IQueryable source,
CancellationToken token = default(CancellationToken))
@@ -139,7 +134,7 @@ public static Task LongCountAsync(
return source.Where(predicate).LongCountAsync(token);
}
- #endregion
+ #endregion Count/LongCount/Sum
#region First/FirstOrDefault
@@ -183,7 +178,7 @@ public static Task FirstOrDefaultAsync(
return source.Where(predicate).FirstOrDefaultAsync(token);
}
- #endregion
+ #endregion First/FirstOrDefault
#region Single/SingleOrDefault
@@ -227,7 +222,7 @@ public static Task SingleOrDefaultAsync(
return source.Where(predicate).SingleOrDefaultAsync(token);
}
- #endregion
+ #endregion Single/SingleOrDefault
#region Shared
@@ -236,14 +231,14 @@ private static IMartenQueryable