diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..97f37dc --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "1.2.6", + "commands": [ + "csharpier" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/.csharpierignore b/.csharpierignore new file mode 100644 index 0000000..48bafbc --- /dev/null +++ b/.csharpierignore @@ -0,0 +1 @@ +worktrees/ diff --git a/Directory.Build.props b/Directory.Build.props index b8bc638..326abfd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,4 @@ - enable 2.0.1 @@ -19,5 +18,4 @@ - diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/AdvancedQueryTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/AdvancedQueryTests.cs index 7341c20..81a5d16 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/AdvancedQueryTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/AdvancedQueryTests.cs @@ -3,13 +3,15 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class AdvancedQueryTests(ParadeDbFixture fixture) { +public class AdvancedQueryTests(ParadeDbFixture fixture) +{ [Fact] - public async Task Parse_TantivySyntax_FiltersByField() { + public async Task Parse_TantivySyntax_FiltersByField() + { await using var ctx = fixture.CreateDbContext(); - var results = await ctx.Articles - .Where(a => EF.Functions.Parse(a.Id, "title:transformer")) + var results = await ctx + .Articles.Where(a => EF.Functions.Parse(a.Id, "title:transformer")) .Select(a => a.Title) .ToListAsync(); @@ -18,13 +20,16 @@ public async Task Parse_TantivySyntax_FiltersByField() { } [Fact] - public async Task MoreLikeThis_ExecutesAndReturnsRelatedArticle() { + public async Task MoreLikeThis_ExecutesAndReturnsRelatedArticle() + { await using var ctx = fixture.CreateDbContext(); - var seed = await ctx.Articles.SingleAsync(a => a.Title == "Introduction to neural networks"); + var seed = await ctx.Articles.SingleAsync(a => + a.Title == "Introduction to neural networks" + ); - var related = await ctx.Articles - .Where(a => EF.Functions.MoreLikeThis(a.Id, seed.Id)) + var related = await ctx + .Articles.Where(a => EF.Functions.MoreLikeThis(a.Id, seed.Id)) .Select(a => a.Title) .ToListAsync(); @@ -35,15 +40,19 @@ public async Task MoreLikeThis_ExecutesAndReturnsRelatedArticle() { } [Fact] - public async Task JsonSearch_BooleanQuery_CombinesParseAndTerm() { + public async Task JsonSearch_BooleanQuery_CombinesParseAndTerm() + { await using var ctx = fixture.CreateDbContext(); - var query = ParadeDbJsonQuery.Boolean(b => b.Must( - ParadeDbJsonQuery.Parse("neural"), - ParadeDbJsonQuery.Term("category", "machine-learning"))); + var query = ParadeDbJsonQuery.Boolean(b => + b.Must( + ParadeDbJsonQuery.Parse("neural"), + ParadeDbJsonQuery.Term("category", "machine-learning") + ) + ); - var results = await ctx.Articles - .JsonSearch(a => a.Id, query) + var results = await ctx + .Articles.JsonSearch(a => a.Id, query) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/BasicSearchTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/BasicSearchTests.cs index b7930fd..19482ff 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/BasicSearchTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/BasicSearchTests.cs @@ -3,13 +3,15 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class BasicSearchTests(ParadeDbFixture fixture) { +public class BasicSearchTests(ParadeDbFixture fixture) +{ [Fact] - public async Task Matches_OrOperator_ReturnsMatchingDocuments() { + public async Task Matches_OrOperator_ReturnsMatchingDocuments() + { await using var ctx = fixture.CreateDbContext(); - var results = await ctx.Articles - .Where(a => EF.Functions.Matches(a.Content, "neural networks")) + var results = await ctx + .Articles.Where(a => EF.Functions.Matches(a.Content, "neural networks")) .Select(a => a.Title) .ToListAsync(); @@ -18,11 +20,12 @@ public async Task Matches_OrOperator_ReturnsMatchingDocuments() { } [Fact] - public async Task MatchesAll_AndOperator_RequiresAllTerms() { + public async Task MatchesAll_AndOperator_RequiresAllTerms() + { await using var ctx = fixture.CreateDbContext(); - var results = await ctx.Articles - .Where(a => EF.Functions.MatchesAll(a.Content, "attention transformers")) + var results = await ctx + .Articles.Where(a => EF.Functions.MatchesAll(a.Content, "attention transformers")) .Select(a => a.Title) .ToListAsync(); @@ -31,14 +34,15 @@ public async Task MatchesAll_AndOperator_RequiresAllTerms() { } [Fact] - public async Task MatchesPhrase_RequiresExactOrder() { + public async Task MatchesPhrase_RequiresExactOrder() + { await using var ctx = fixture.CreateDbContext(); - var withExactPhrase = await ctx.Articles - .Where(a => EF.Functions.MatchesPhrase(a.Content, "neural networks")) + var withExactPhrase = await ctx + .Articles.Where(a => EF.Functions.MatchesPhrase(a.Content, "neural networks")) .CountAsync(); - var withReversedPhrase = await ctx.Articles - .Where(a => EF.Functions.MatchesPhrase(a.Content, "networks neural")) + var withReversedPhrase = await ctx + .Articles.Where(a => EF.Functions.MatchesPhrase(a.Content, "networks neural")) .CountAsync(); Assert.Equal(1, withExactPhrase); @@ -46,11 +50,12 @@ public async Task MatchesPhrase_RequiresExactOrder() { } [Fact] - public async Task MatchesFuzzy_ToleratesTypos() { + public async Task MatchesFuzzy_ToleratesTypos() + { await using var ctx = fixture.CreateDbContext(); - var results = await ctx.Articles - .Where(a => EF.Functions.MatchesFuzzy(a.Content, "nueral", 2)) + var results = await ctx + .Articles.Where(a => EF.Functions.MatchesFuzzy(a.Content, "nueral", 2)) .Select(a => a.Title) .ToListAsync(); @@ -58,11 +63,12 @@ public async Task MatchesFuzzy_ToleratesTypos() { } [Fact] - public async Task MatchesTerm_RequiresExactToken() { + public async Task MatchesTerm_RequiresExactToken() + { await using var ctx = fixture.CreateDbContext(); - var hits = await ctx.Articles - .Where(a => EF.Functions.MatchesTerm(a.Content, "gpus")) + var hits = await ctx + .Articles.Where(a => EF.Functions.MatchesTerm(a.Content, "gpus")) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests.csproj b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests.csproj index 467c082..f93b024 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests.csproj +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests.csproj @@ -1,5 +1,4 @@ - net10.0 enable @@ -24,5 +23,4 @@ - diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/FieldTypeTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/FieldTypeTests.cs index f366bd7..fa41609 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/FieldTypeTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/FieldTypeTests.cs @@ -3,18 +3,20 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class FieldTypeTests(ParadeDbFixture fixture) { +public class FieldTypeTests(ParadeDbFixture fixture) +{ [Fact] - public async Task Bm25Boolean_IndexedFastColumn_FiltersByValue() { + public async Task Bm25Boolean_IndexedFastColumn_FiltersByValue() + { await using var ctx = fixture.CreateDbContext(); - var inStockNames = await ctx.Products - .Where(p => EF.Functions.Matches(p.Name, "keyboard")) + var inStockNames = await ctx + .Products.Where(p => EF.Functions.Matches(p.Name, "keyboard")) .Where(p => p.InStock) .Select(p => p.Name) .ToListAsync(); - var outOfStockNames = await ctx.Products - .Where(p => EF.Functions.Matches(p.Name, "keyboard")) + var outOfStockNames = await ctx + .Products.Where(p => EF.Functions.Matches(p.Name, "keyboard")) .Where(p => !p.InStock) .Select(p => p.Name) .ToListAsync(); @@ -24,12 +26,13 @@ public async Task Bm25Boolean_IndexedFastColumn_FiltersByValue() { } [Fact] - public async Task Bm25DateTime_FastColumn_SupportsRangeFilter() { + public async Task Bm25DateTime_FastColumn_SupportsRangeFilter() + { await using var ctx = fixture.CreateDbContext(); var cutoff = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); - var recent = await ctx.Products - .Where(p => EF.Functions.Matches(p.Name, "laptop OR mouse OR keyboard")) + var recent = await ctx + .Products.Where(p => EF.Functions.Matches(p.Name, "laptop OR mouse OR keyboard")) .Where(p => p.ReleasedAt >= cutoff) .Select(p => p.Name) .ToListAsync(); @@ -40,11 +43,12 @@ public async Task Bm25DateTime_FastColumn_SupportsRangeFilter() { } [Fact] - public async Task Bm25Json_WithExpandDots_IndexesJsonField() { + public async Task Bm25Json_WithExpandDots_IndexesJsonField() + { await using var ctx = fixture.CreateDbContext(); - var hits = await ctx.Products - .Where(p => EF.Functions.Matches(p.Name, "laptop OR mouse OR keyboard")) + var hits = await ctx + .Products.Where(p => EF.Functions.Matches(p.Name, "laptop OR mouse OR keyboard")) .Select(p => p.Name) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/IndexConfigurationTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/IndexConfigurationTests.cs index f0929b6..3abc374 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/IndexConfigurationTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/IndexConfigurationTests.cs @@ -4,9 +4,11 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class IndexConfigurationTests(ParadeDbFixture fixture) { +public class IndexConfigurationTests(ParadeDbFixture fixture) +{ [Fact] - public async Task PgSearchExtension_IsInstalled() { + public async Task PgSearchExtension_IsInstalled() + { await using var conn = new NpgsqlConnection(fixture.ConnectionString); await conn.OpenAsync(); await using var cmd = conn.CreateCommand(); @@ -18,7 +20,8 @@ public async Task PgSearchExtension_IsInstalled() { } [Fact] - public async Task Bm25Index_IsCreatedWithStorageParameters() { + public async Task Bm25Index_IsCreatedWithStorageParameters() + { await using var conn = new NpgsqlConnection(fixture.ConnectionString); await conn.OpenAsync(); await using var cmd = conn.CreateCommand(); @@ -37,11 +40,12 @@ SELECT indexdef FROM pg_indexes } [Fact] - public async Task EnglishStemmer_MatchesWordVariants() { + public async Task EnglishStemmer_MatchesWordVariants() + { await using var ctx = fixture.CreateDbContext(); - var hits = await ctx.Articles - .Where(a => EF.Functions.Matches(a.Content, "run")) + var hits = await ctx + .Articles.Where(a => EF.Functions.Matches(a.Content, "run")) .Select(a => a.Title) .ToListAsync(); @@ -50,14 +54,15 @@ public async Task EnglishStemmer_MatchesWordVariants() { } [Fact] - public async Task RawTokenizer_KeepsExactCategoryValue() { + public async Task RawTokenizer_KeepsExactCategoryValue() + { await using var ctx = fixture.CreateDbContext(); - var exactHits = await ctx.Articles - .Where(a => EF.Functions.MatchesTerm(a.Category, "machine-learning")) + var exactHits = await ctx + .Articles.Where(a => EF.Functions.MatchesTerm(a.Category, "machine-learning")) .CountAsync(); - var tokenizedAttempt = await ctx.Articles - .Where(a => EF.Functions.MatchesTerm(a.Category, "machine")) + var tokenizedAttempt = await ctx + .Articles.Where(a => EF.Functions.MatchesTerm(a.Category, "machine")) .CountAsync(); Assert.Equal(2, exactHits); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/IntegrationDbContext.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/IntegrationDbContext.cs index 61f9c8f..8e20644 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/IntegrationDbContext.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/IntegrationDbContext.cs @@ -3,7 +3,9 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; -public sealed class IntegrationDbContext(DbContextOptions options) : DbContext(options) { +public sealed class IntegrationDbContext(DbContextOptions options) + : DbContext(options) +{ public DbSet
Articles => Set
(); public DbSet Products => Set(); public DbSet KeywordRecords => Set(); @@ -16,7 +18,8 @@ public sealed class IntegrationDbContext(DbContextOptions [Table("articles")] [Bm25Index(nameof(Id), nameof(Title), nameof(Content), nameof(Category), nameof(Rating))] -public sealed class Article { +public sealed class Article +{ [Column("id")] public int Id { get; set; } @@ -39,7 +42,8 @@ public sealed class Article { [Table("products")] [Bm25Index(nameof(Id), nameof(Name), nameof(InStock), nameof(ReleasedAt), nameof(Specs))] -public sealed class Product { +public sealed class Product +{ [Column("id")] public int Id { get; set; } @@ -62,7 +66,8 @@ public sealed class Product { [Table("keyword_records")] [Bm25Index(nameof(Id), nameof(Code))] -public sealed class KeywordRecord { +public sealed class KeywordRecord +{ [Column("id")] public int Id { get; set; } @@ -73,7 +78,8 @@ public sealed class KeywordRecord { [Table("ngram_records")] [Bm25Index(nameof(Id), nameof(Body))] -public sealed class NgramRecord { +public sealed class NgramRecord +{ [Column("id")] public int Id { get; set; } @@ -84,7 +90,8 @@ public sealed class NgramRecord { [Table("icu_records")] [Bm25Index(nameof(Id), nameof(Body))] -public sealed class IcuRecord { +public sealed class IcuRecord +{ [Column("id")] public int Id { get; set; } @@ -95,7 +102,8 @@ public sealed class IcuRecord { [Table("source_code_records")] [Bm25Index(nameof(Id), nameof(Snippet))] -public sealed class SourceCodeRecord { +public sealed class SourceCodeRecord +{ [Column("id")] public int Id { get; set; } @@ -106,7 +114,8 @@ public sealed class SourceCodeRecord { [Table("regex_records")] [Bm25Index(nameof(Id), nameof(Body))] -public sealed class RegexRecord { +public sealed class RegexRecord +{ [Column("id")] public int Id { get; set; } @@ -117,7 +126,8 @@ public sealed class RegexRecord { [Table("german_articles")] [Bm25Index(nameof(Id), nameof(Content))] -public sealed class GermanArticle { +public sealed class GermanArticle +{ [Column("id")] public int Id { get; set; } diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonBoostTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonBoostTests.cs index be35464..dbd8bfd 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonBoostTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonBoostTests.cs @@ -3,39 +3,49 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonBoostTests(ParadeDbFixture fixture) { +public class JsonBoostTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.Boost wraps an inner query into {"boost":{"query":...,"factor":N}} // and that the factor actually amplifies BM25 scores via the JSON @@@ operator path. // A regression in CloneNode (e.g., shared mutable JsonNode reuse) or JSON shape would // either fail to serialize, fail to match, or return un-amplified scores. [Fact] - public async Task Boost_WrapsInnerQuery_AmplifiesScoreVsUnwrappedQuery() { + public async Task Boost_WrapsInnerQuery_AmplifiesScoreVsUnwrappedQuery() + { await using var ctx = fixture.CreateDbContext(); const double boostFactor = 5.0; var inner = ParadeDbJsonQuery.Parse("neural"); var boosted = ParadeDbJsonQuery.Boost(inner, boostFactor); - var boostedResults = await ctx.Articles - .JsonSearch(a => a.Id, boosted) + var boostedResults = await ctx + .Articles.JsonSearch(a => a.Id, boosted) .Select(a => new { a.Title, Score = EF.Functions.Score(a.Id) }) .ToListAsync(); - var unboostedResults = await ctx.Articles - .JsonSearch(a => a.Id, inner) + var unboostedResults = await ctx + .Articles.JsonSearch(a => a.Id, inner) .Select(a => new { a.Title, Score = EF.Functions.Score(a.Id) }) .ToListAsync(); // Boost only changes scores, not the matching set. - Assert.Equal(unboostedResults.Select(x => x.Title).OrderBy(t => t), - boostedResults.Select(x => x.Title).OrderBy(t => t)); + Assert.Equal( + unboostedResults.Select(x => x.Title).OrderBy(t => t), + boostedResults.Select(x => x.Title).OrderBy(t => t) + ); Assert.Contains(boostedResults, x => x.Title == "Introduction to neural networks"); var boostedHit = boostedResults.Single(x => x.Title == "Introduction to neural networks"); - var unboostedHit = unboostedResults.Single(x => x.Title == "Introduction to neural networks"); - Assert.True(unboostedHit.Score > 0, - $"Sanity: unboosted Parse score should be positive; was {unboostedHit.Score}."); - Assert.True(boostedHit.Score > unboostedHit.Score * 2.0, - $"Boost factor {boostFactor} should substantially amplify the score; " + - $"boosted={boostedHit.Score}, unboosted={unboostedHit.Score}."); + var unboostedHit = unboostedResults.Single(x => + x.Title == "Introduction to neural networks" + ); + Assert.True( + unboostedHit.Score > 0, + $"Sanity: unboosted Parse score should be positive; was {unboostedHit.Score}." + ); + Assert.True( + boostedHit.Score > unboostedHit.Score * 2.0, + $"Boost factor {boostFactor} should substantially amplify the score; " + + $"boosted={boostedHit.Score}, unboosted={unboostedHit.Score}." + ); } } diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonConstScoreTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonConstScoreTests.cs index aa0dfe6..1d4fb7a 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonConstScoreTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonConstScoreTests.cs @@ -3,22 +3,24 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonConstScoreTests(ParadeDbFixture fixture) { +public class JsonConstScoreTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.ConstScore produces {"const_score":{"query":...,"score":N}} // and that pg_search overrides the BM25 score with N for every matching document. This // differs from Boost (which multiplies the existing score). A regression that swapped // "score" → "factor", or mis-wired CloneNode, would either fail to apply the override // or change the result set. [Fact] - public async Task ConstScore_WrapsInnerQuery_FixesScoreToConstantForAllMatches() { + public async Task ConstScore_WrapsInnerQuery_FixesScoreToConstantForAllMatches() + { await using var ctx = fixture.CreateDbContext(); const double constantScore = 3.14; var inner = ParadeDbJsonQuery.Term("category", "machine-learning"); var query = ParadeDbJsonQuery.ConstScore(inner, constantScore); - var results = await ctx.Articles - .JsonSearch(a => a.Id, query) + var results = await ctx + .Articles.JsonSearch(a => a.Id, query) .Select(a => new { a.Title, Score = EF.Functions.Score(a.Id) }) .ToListAsync(); @@ -26,9 +28,12 @@ public async Task ConstScore_WrapsInnerQuery_FixesScoreToConstantForAllMatches() Assert.Equal(2, results.Count); Assert.Contains(results, x => x.Title == "Introduction to neural networks"); Assert.Contains(results, x => x.Title == "Transformer architectures"); - foreach (var hit in results) { - Assert.True(Math.Abs(hit.Score - constantScore) < 0.001, - $"ConstScore should fix score to {constantScore}; got {hit.Score} for '{hit.Title}'."); + foreach (var hit in results) + { + Assert.True( + Math.Abs(hit.Score - constantScore) < 0.001, + $"ConstScore should fix score to {constantScore}; got {hit.Score} for '{hit.Title}'." + ); } } } diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonDateTimeRangeTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonDateTimeRangeTests.cs index aad260e..0670ecb 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonDateTimeRangeTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonDateTimeRangeTests.cs @@ -3,23 +3,31 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonDateTimeRangeTests(ParadeDbFixture fixture) { +public class JsonDateTimeRangeTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.Range with isDatetime: true serializes UTC bounds as // ISO-8601 ("yyyy-MM-ddTHH:mm:ssZ") and adds "is_datetime": true so pg_search parses // them as timestamps, not strings. A regression in CreateJsonValue's DateTime branch // (wrong format, missing 'Z', non-UTC handling) or a missing is_datetime flag would // either fail to filter or return zero matches against a Bm25DateTime-indexed column. [Fact] - public async Task DateTimeRange_FiltersByDateBoundary_IncludesOnlyProductsInRange() { + public async Task DateTimeRange_FiltersByDateBoundary_IncludesOnlyProductsInRange() + { await using var ctx = fixture.CreateDbContext(); var lower = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); var upper = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); - var query = ParadeDbJsonQuery.Range("released_at", lower, upper, - lowerInclusive: true, upperInclusive: false, isDatetime: true); + var query = ParadeDbJsonQuery.Range( + "released_at", + lower, + upper, + lowerInclusive: true, + upperInclusive: false, + isDatetime: true + ); - var hits = await ctx.Products - .JsonSearch(p => p.Id, query) + var hits = await ctx + .Products.JsonSearch(p => p.Id, query) .Select(p => p.Name) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonDisjunctionMaxTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonDisjunctionMaxTests.cs index 8229ab5..16fb34e 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonDisjunctionMaxTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonDisjunctionMaxTests.cs @@ -3,24 +3,27 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonDisjunctionMaxTests(ParadeDbFixture fixture) { +public class JsonDisjunctionMaxTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.DisjunctionMax produces {"disjunction_max":{"disjuncts":[...]}} // with OR matching semantics — a document matching ANY disjunct appears once, even when // it matches multiple. Distinct from Boolean.Should (which sums scores). A regression in // CloneNode usage across the disjuncts array, or the disjuncts being misnamed, would // either fail to deserialize or change the matching set. [Fact] - public async Task DisjunctionMax_OverlappingDisjuncts_ReturnsDistinctUnionOfMatches() { + public async Task DisjunctionMax_OverlappingDisjuncts_ReturnsDistinctUnionOfMatches() + { await using var ctx = fixture.CreateDbContext(); // "neural" matches Article 1 only; "machine-learning" category matches Articles 1 and 2. // Disjunction → union (Articles 1, 2). Article 1 is in both disjuncts but must appear once. var query = ParadeDbJsonQuery.DisjunctionMax( ParadeDbJsonQuery.Parse("neural"), - ParadeDbJsonQuery.Term("category", "machine-learning")); + ParadeDbJsonQuery.Term("category", "machine-learning") + ); - var hits = await ctx.Articles - .JsonSearch(a => a.Id, query) + var hits = await ctx + .Articles.JsonSearch(a => a.Id, query) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonExistsTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonExistsTests.cs index 6f0d12e..0ad0564 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonExistsTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonExistsTests.cs @@ -3,18 +3,20 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonExistsTests(ParadeDbFixture fixture) { +public class JsonExistsTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.Exists produces {"exists":{"field":"..."}} and pg_search // returns every row that has the named field indexed. The two-key shape is small, so // the test serves mainly as a regression guard against typos in "exists"/"field". [Fact] - public async Task Exists_OnPopulatedField_MatchesAllIndexedRows() { + public async Task Exists_OnPopulatedField_MatchesAllIndexedRows() + { await using var ctx = fixture.CreateDbContext(); var query = ParadeDbJsonQuery.Exists("category"); - var hits = await ctx.Articles - .JsonSearch(a => a.Id, query) + var hits = await ctx + .Articles.JsonSearch(a => a.Id, query) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonFuzzyTermOptionsTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonFuzzyTermOptionsTests.cs index 1adea3d..4ddae1c 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonFuzzyTermOptionsTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonFuzzyTermOptionsTests.cs @@ -3,7 +3,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonFuzzyTermOptionsTests(ParadeDbFixture fixture) { +public class JsonFuzzyTermOptionsTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.FuzzyTerm(field, value, distance, prefix, // transpositionCostOne) conditionally emits the JSON keys "prefix" and // "transposition_cost_one" on the {"fuzzy_term":{...}} node. Distinct from @@ -11,21 +12,32 @@ public class JsonFuzzyTermOptionsTests(ParadeDbFixture fixture) { // A regression renaming "transposition_cost_one" (or hard-coding it) would // silently make distance=1 reject the adjacent-swap that the test relies on. [Fact] - public async Task FuzzyTerm_WithTranspositionCostOne_MatchesAdjacentSwapAtDistance1() { + public async Task FuzzyTerm_WithTranspositionCostOne_MatchesAdjacentSwapAtDistance1() + { await using var ctx = fixture.CreateDbContext(); // "nueral" ↔ "neural" is an adjacent transposition (transp distance 1, Levenshtein 2). - var withTransposition = ParadeDbJsonQuery.FuzzyTerm("content", "nueral", 1, - prefix: false, transpositionCostOne: true); - var withoutTransposition = ParadeDbJsonQuery.FuzzyTerm("content", "nueral", 1, - prefix: false, transpositionCostOne: false); + var withTransposition = ParadeDbJsonQuery.FuzzyTerm( + "content", + "nueral", + 1, + prefix: false, + transpositionCostOne: true + ); + var withoutTransposition = ParadeDbJsonQuery.FuzzyTerm( + "content", + "nueral", + 1, + prefix: false, + transpositionCostOne: false + ); - var hitsWith = await ctx.Articles - .JsonSearch(a => a.Id, withTransposition) + var hitsWith = await ctx + .Articles.JsonSearch(a => a.Id, withTransposition) .Select(a => a.Title) .ToListAsync(); - var hitsWithout = await ctx.Articles - .JsonSearch(a => a.Id, withoutTransposition) + var hitsWithout = await ctx + .Articles.JsonSearch(a => a.Id, withoutTransposition) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonFuzzyTermTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonFuzzyTermTests.cs index 3d45642..aebef05 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonFuzzyTermTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonFuzzyTermTests.cs @@ -3,20 +3,22 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonFuzzyTermTests(ParadeDbFixture fixture) { +public class JsonFuzzyTermTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.FuzzyTerm builds {"fuzzy_term":{"field":"...","value":"...","distance":N}} // and pg_search accepts it via the @@@ jsonb path. Distinct from the CLR MatchesTermFuzzy // translation (which goes through pdb.fuzzy(...) casts). A regression in the JSON keys // ("fuzzy_term"/"field"/"value"/"distance") would either fail to deserialize or zero matches. [Fact] - public async Task FuzzyTerm_WithDistance2_MatchesTypoedToken() { + public async Task FuzzyTerm_WithDistance2_MatchesTypoedToken() + { await using var ctx = fixture.CreateDbContext(); // "nueral" → "neural" is a Levenshtein distance of 2; distance: 2 should match. var query = ParadeDbJsonQuery.FuzzyTerm("content", "nueral", 2); - var hits = await ctx.Articles - .JsonSearch(a => a.Id, query) + var hits = await ctx + .Articles.JsonSearch(a => a.Id, query) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonMatchConjunctionModeTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonMatchConjunctionModeTests.cs index 564f8ed..3587100 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonMatchConjunctionModeTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonMatchConjunctionModeTests.cs @@ -3,7 +3,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonMatchConjunctionModeTests(ParadeDbFixture fixture) { +public class JsonMatchConjunctionModeTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.Match 4-arg actually toggles AND/OR semantics // via the conditional "conjunction_mode" key. The existing JsonMatchTests // uses "nueral netwroks" — both fuzzy-terms only co-occur in Article 1 — so @@ -12,20 +13,31 @@ public class JsonMatchConjunctionModeTests(ParadeDbFixture fixture) { // where the two tokens live in disjoint articles, so flipping the flag MUST // change the hit set. [Fact] - public async Task Match_WithConjunctionModeFalse_AppliesOrSemantics() { + public async Task Match_WithConjunctionModeFalse_AppliesOrSemantics() + { await using var ctx = fixture.CreateDbContext(); // "neural" appears in Article 1's content; "pasta" in Article 4's content; // no single article's content has both — so OR and AND must differ. - var orQuery = ParadeDbJsonQuery.Match("neural pasta", "content", distance: 0, conjunctionMode: false); - var andQuery = ParadeDbJsonQuery.Match("neural pasta", "content", distance: 0, conjunctionMode: true); + var orQuery = ParadeDbJsonQuery.Match( + "neural pasta", + "content", + distance: 0, + conjunctionMode: false + ); + var andQuery = ParadeDbJsonQuery.Match( + "neural pasta", + "content", + distance: 0, + conjunctionMode: true + ); - var orHits = await ctx.Articles - .JsonSearch(a => a.Id, orQuery) + var orHits = await ctx + .Articles.JsonSearch(a => a.Id, orQuery) .Select(a => a.Title) .ToListAsync(); - var andHits = await ctx.Articles - .JsonSearch(a => a.Id, andQuery) + var andHits = await ctx + .Articles.JsonSearch(a => a.Id, andQuery) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonMatchFieldTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonMatchFieldTests.cs index bbd3902..a99e0a1 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonMatchFieldTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonMatchFieldTests.cs @@ -3,14 +3,16 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonMatchFieldTests(ParadeDbFixture fixture) { +public class JsonMatchFieldTests(ParadeDbFixture fixture) +{ // Verifies the 2-arg ParadeDbJsonQuery.Match(value, field) overload restricts // the match to the named field. The C# parameter order (value, field) is the // opposite of the resulting JSON key order ({"field":..., "value":...}); a // refactor that "aligns" param order with the JSON would silently invert // every caller. The 4-arg overload is the only currently-tested Match shape. [Fact] - public async Task Match_WithFieldAndValue_RestrictsToNamedField() { + public async Task Match_WithFieldAndValue_RestrictsToNamedField() + { await using var ctx = fixture.CreateDbContext(); // "models" appears in Article 1 and Article 2 content (stems to "model"), @@ -19,12 +21,12 @@ public async Task Match_WithFieldAndValue_RestrictsToNamedField() { var titleQuery = ParadeDbJsonQuery.Match("models", "title"); var contentQuery = ParadeDbJsonQuery.Match("models", "content"); - var titleHits = await ctx.Articles - .JsonSearch(a => a.Id, titleQuery) + var titleHits = await ctx + .Articles.JsonSearch(a => a.Id, titleQuery) .Select(a => a.Title) .ToListAsync(); - var contentHits = await ctx.Articles - .JsonSearch(a => a.Id, contentQuery) + var contentHits = await ctx + .Articles.JsonSearch(a => a.Id, contentQuery) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonMatchTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonMatchTests.cs index 433c780..cfae856 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonMatchTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonMatchTests.cs @@ -3,22 +3,24 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonMatchTests(ParadeDbFixture fixture) { +public class JsonMatchTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.Match 4-arg overload emits the full options shape: // {"match":{"field":"...","value":"...","distance":N,"conjunction_mode":true}} // — combines fuzzy matching with AND semantics in one JSON node. A regression in // any of the four keys, or the conditional emission of conjunction_mode, would // either fail to deserialize or change the match set. [Fact] - public async Task Match_WithDistanceAndConjunctionMode_MatchesAllFuzzyTerms() { + public async Task Match_WithDistanceAndConjunctionMode_MatchesAllFuzzyTerms() + { await using var ctx = fixture.CreateDbContext(); // "nueral" and "netwroks" both typo'd; distance=2 + conjunction=true means // every term must fuzzy-match. Article 1's content has both "neural" and "networks". var query = ParadeDbJsonQuery.Match("nueral netwroks", "content", 2, conjunctionMode: true); - var hits = await ctx.Articles - .JsonSearch(a => a.Id, query) + var hits = await ctx + .Articles.JsonSearch(a => a.Id, query) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonMoreLikeThisTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonMoreLikeThisTests.cs index 3073832..587692a 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonMoreLikeThisTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonMoreLikeThisTests.cs @@ -3,20 +3,24 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonMoreLikeThisTests(ParadeDbFixture fixture) { +public class JsonMoreLikeThisTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.MoreLikeThis builds {"more_like_this":{"key_value":N}} // and pg_search accepts it via the @@@ jsonb path. Distinct from the CLR MoreLikeThis // (which translates via pdb.more_like_this(documentId)) — a regression in the JSON // key names would still return zero matches without surfacing the underlying mistake. [Fact] - public async Task MoreLikeThis_FromSeedArticle_ExecutesAndReturnsSimilarArticle() { + public async Task MoreLikeThis_FromSeedArticle_ExecutesAndReturnsSimilarArticle() + { await using var ctx = fixture.CreateDbContext(); - var seed = await ctx.Articles.SingleAsync(a => a.Title == "Introduction to neural networks"); + var seed = await ctx.Articles.SingleAsync(a => + a.Title == "Introduction to neural networks" + ); var query = ParadeDbJsonQuery.MoreLikeThis(seed.Id); - var related = await ctx.Articles - .JsonSearch(a => a.Id, query) + var related = await ctx + .Articles.JsonSearch(a => a.Id, query) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonParseLenientTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonParseLenientTests.cs index 3e9e016..647bc77 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonParseLenientTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonParseLenientTests.cs @@ -4,7 +4,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonParseLenientTests(ParadeDbFixture fixture) { +public class JsonParseLenientTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.Parse's "lenient" JSON-key emission actually // takes effect — the existing JsonParseOptionsTests pins lenient: true in // both halves to isolate conjunction_mode, so the lenient branch is never @@ -12,27 +13,32 @@ public class JsonParseLenientTests(ParadeDbFixture fixture) { // that pg_search rejects in strict mode and tolerates in lenient mode — // a regression dropping the "lenient" key would let the strict case parse. [Fact] - public async Task Parse_WithLenientFalse_RejectsMalformedQueryThatLenientTrueAccepts() { + public async Task Parse_WithLenientFalse_RejectsMalformedQueryThatLenientTrueAccepts() + { await using var ctx = fixture.CreateDbContext(); // Trailing AND with no right-hand operand — syntactically invalid. - var lenientQuery = ParadeDbJsonQuery.Parse("neural AND", - lenient: true, conjunctionMode: false); - var strictQuery = ParadeDbJsonQuery.Parse("neural AND", - lenient: false, conjunctionMode: false); + var lenientQuery = ParadeDbJsonQuery.Parse( + "neural AND", + lenient: true, + conjunctionMode: false + ); + var strictQuery = ParadeDbJsonQuery.Parse( + "neural AND", + lenient: false, + conjunctionMode: false + ); // Lenient: parser ignores the trailing operator and matches "neural" → Article 1. - var lenientHits = await ctx.Articles - .JsonSearch(a => a.Id, lenientQuery) + var lenientHits = await ctx + .Articles.JsonSearch(a => a.Id, lenientQuery) .Select(a => a.Title) .ToListAsync(); Assert.Contains(lenientHits, t => t == "Introduction to neural networks"); // Strict: pg_search rejects the malformed query at execution time. await Assert.ThrowsAsync(async () => - await ctx.Articles - .JsonSearch(a => a.Id, strictQuery) - .Select(a => a.Title) - .ToListAsync()); + await ctx.Articles.JsonSearch(a => a.Id, strictQuery).Select(a => a.Title).ToListAsync() + ); } } diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonParseOptionsTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonParseOptionsTests.cs index 3a28a50..36922a4 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonParseOptionsTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonParseOptionsTests.cs @@ -3,7 +3,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonParseOptionsTests(ParadeDbFixture fixture) { +public class JsonParseOptionsTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.Parse(string, bool, bool) emits the JSON keys // "lenient" and "conjunction_mode" — distinct from the 1-arg overload that // emits only "query_string". The CLR sibling EF.Functions.Parse(...) is @@ -11,19 +12,28 @@ public class JsonParseOptionsTests(ParadeDbFixture fixture) { // regression renaming either key (e.g. "lenient" → "strict") would silently // flip the AND/OR meaning of every unquoted multi-term parse query. [Fact] - public async Task Parse_WithConjunctionMode_RequiresAllTermsInsteadOfAny() { + public async Task Parse_WithConjunctionMode_RequiresAllTermsInsteadOfAny() + { await using var ctx = fixture.CreateDbContext(); // "neural" appears only in Article 1; "quantum" only in Article 3 — no article has both. - var orQuery = ParadeDbJsonQuery.Parse("neural quantum", lenient: true, conjunctionMode: false); - var andQuery = ParadeDbJsonQuery.Parse("neural quantum", lenient: true, conjunctionMode: true); + var orQuery = ParadeDbJsonQuery.Parse( + "neural quantum", + lenient: true, + conjunctionMode: false + ); + var andQuery = ParadeDbJsonQuery.Parse( + "neural quantum", + lenient: true, + conjunctionMode: true + ); - var orHits = await ctx.Articles - .JsonSearch(a => a.Id, orQuery) + var orHits = await ctx + .Articles.JsonSearch(a => a.Id, orQuery) .Select(a => a.Title) .ToListAsync(); - var andHits = await ctx.Articles - .JsonSearch(a => a.Id, andQuery) + var andHits = await ctx + .Articles.JsonSearch(a => a.Id, andQuery) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonPhrasePrefixTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonPhrasePrefixTests.cs index 797a193..f064274 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonPhrasePrefixTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonPhrasePrefixTests.cs @@ -3,20 +3,22 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonPhrasePrefixTests(ParadeDbFixture fixture) { +public class JsonPhrasePrefixTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.PhrasePrefix builds {"phrase_prefix":{"field":"...","phrases":[...]}} // — the last phrase element is treated as a prefix (useful for autocomplete). The CLR // PhrasePrefix translates via pdb.phrase_prefix(ARRAY[...]); this JSON form goes through // the @@@ jsonb path. A regression in the phrases array or field key would either fail // to match or change the match set entirely. [Fact] - public async Task PhrasePrefix_LastTermAsPrefix_MatchesContainingDocument() { + public async Task PhrasePrefix_LastTermAsPrefix_MatchesContainingDocument() + { await using var ctx = fixture.CreateDbContext(); var query = ParadeDbJsonQuery.PhrasePrefix("content", "neural", "net"); - var hits = await ctx.Articles - .JsonSearch(a => a.Id, query) + var hits = await ctx + .Articles.JsonSearch(a => a.Id, query) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonPhraseSlopTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonPhraseSlopTests.cs index 6db0998..44e4a7b 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonPhraseSlopTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonPhraseSlopTests.cs @@ -3,21 +3,26 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonPhraseSlopTests(ParadeDbFixture fixture) { +public class JsonPhraseSlopTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.Phrase with slop produces {"phrase":{"field":"...","phrases":[...],"slop":N}} // and pg_search allows N words between phrase terms. Article 1's content has "Deep learning models" — // strict "deep models" doesn't match (1 word in between), but slop>=1 does. // A regression that dropped the "slop" key would fail the test by returning zero matches. [Fact] - public async Task Phrase_WithSlop_AllowsWordsBetweenTerms() { + public async Task Phrase_WithSlop_AllowsWordsBetweenTerms() + { await using var ctx = fixture.CreateDbContext(); - var strict = await ctx.Articles - .JsonSearch(a => a.Id, ParadeDbJsonQuery.Phrase("content", "deep", "models")) + var strict = await ctx + .Articles.JsonSearch(a => a.Id, ParadeDbJsonQuery.Phrase("content", "deep", "models")) .Select(a => a.Title) .ToListAsync(); - var withSlop = await ctx.Articles - .JsonSearch(a => a.Id, ParadeDbJsonQuery.Phrase("content", 2, "deep", "models")) + var withSlop = await ctx + .Articles.JsonSearch( + a => a.Id, + ParadeDbJsonQuery.Phrase("content", 2, "deep", "models") + ) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonQueryTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonQueryTests.cs index feaded8..df972ed 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonQueryTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonQueryTests.cs @@ -3,16 +3,23 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonQueryTests(ParadeDbFixture fixture) { +public class JsonQueryTests(ParadeDbFixture fixture) +{ [Fact] - public async Task JsonSearch_NumericRange_FiltersByRating() { + public async Task JsonSearch_NumericRange_FiltersByRating() + { await using var ctx = fixture.CreateDbContext(); - var query = ParadeDbJsonQuery.Range("rating", lowerBound: 4, upperBound: 5, - lowerInclusive: true, upperInclusive: true); + var query = ParadeDbJsonQuery.Range( + "rating", + lowerBound: 4, + upperBound: 5, + lowerInclusive: true, + upperInclusive: true + ); - var hits = await ctx.Articles - .JsonSearch(a => a.Id, query) + var hits = await ctx + .Articles.JsonSearch(a => a.Id, query) .Select(a => a.Title) .ToListAsync(); @@ -21,15 +28,19 @@ public async Task JsonSearch_NumericRange_FiltersByRating() { } [Fact] - public async Task JsonSearch_Should_OrSemantics() { + public async Task JsonSearch_Should_OrSemantics() + { await using var ctx = fixture.CreateDbContext(); - var query = ParadeDbJsonQuery.Boolean(b => b.Should( - ParadeDbJsonQuery.Term("category", "cooking"), - ParadeDbJsonQuery.Term("category", "physics"))); + var query = ParadeDbJsonQuery.Boolean(b => + b.Should( + ParadeDbJsonQuery.Term("category", "cooking"), + ParadeDbJsonQuery.Term("category", "physics") + ) + ); - var hits = await ctx.Articles - .JsonSearch(a => a.Id, query) + var hits = await ctx + .Articles.JsonSearch(a => a.Id, query) .Select(a => a.Title) .ToListAsync(); @@ -39,15 +50,16 @@ public async Task JsonSearch_Should_OrSemantics() { } [Fact] - public async Task JsonSearch_MustNot_ExcludesMatchingDocs() { + public async Task JsonSearch_MustNot_ExcludesMatchingDocs() + { await using var ctx = fixture.CreateDbContext(); - var query = ParadeDbJsonQuery.Boolean(b => b - .Must(ParadeDbJsonQuery.All()) - .MustNot(ParadeDbJsonQuery.Term("category", "cooking"))); + var query = ParadeDbJsonQuery.Boolean(b => + b.Must(ParadeDbJsonQuery.All()).MustNot(ParadeDbJsonQuery.Term("category", "cooking")) + ); - var hits = await ctx.Articles - .JsonSearch(a => a.Id, query) + var hits = await ctx + .Articles.JsonSearch(a => a.Id, query) .Select(a => a.Title) .ToListAsync(); @@ -56,14 +68,19 @@ public async Task JsonSearch_MustNot_ExcludesMatchingDocs() { } [Fact] - public async Task JsonSearch_InlineBuilder_OverloadWorks() { + public async Task JsonSearch_InlineBuilder_OverloadWorks() + { await using var ctx = fixture.CreateDbContext(); - var hits = await ctx.Articles - .JsonSearch(a => a.Id, b => b - .Must( - ParadeDbJsonQuery.Parse("neural"), - ParadeDbJsonQuery.Term("category", "machine-learning"))) + var hits = await ctx + .Articles.JsonSearch( + a => a.Id, + b => + b.Must( + ParadeDbJsonQuery.Parse("neural"), + ParadeDbJsonQuery.Term("category", "machine-learning") + ) + ) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonRangeExclusiveBoundTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonRangeExclusiveBoundTests.cs index b6ec774..f8c751e 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonRangeExclusiveBoundTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonRangeExclusiveBoundTests.cs @@ -3,28 +3,40 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonRangeExclusiveBoundTests(ParadeDbFixture fixture) { +public class JsonRangeExclusiveBoundTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.Range emits "excluded" (not "included") when // lowerInclusive is false. The existing JsonQueryTests numeric-range test // only uses both bounds inclusive, so the "excluded" JSON-key branch is // never executed. A regression that swaps "included"/"excluded" or flips // the boolean condition would silently change the boundary semantics. [Fact] - public async Task Range_WithExclusiveLowerBound_ExcludesBoundaryRating() { + public async Task Range_WithExclusiveLowerBound_ExcludesBoundaryRating() + { await using var ctx = fixture.CreateDbContext(); // Article 2 has rating=4 (the boundary value); Articles 1+4 have rating=5. - var inclusive = ParadeDbJsonQuery.Range("rating", lowerBound: 4, upperBound: 5, - lowerInclusive: true, upperInclusive: true); - var exclusive = ParadeDbJsonQuery.Range("rating", lowerBound: 4, upperBound: 5, - lowerInclusive: false, upperInclusive: true); + var inclusive = ParadeDbJsonQuery.Range( + "rating", + lowerBound: 4, + upperBound: 5, + lowerInclusive: true, + upperInclusive: true + ); + var exclusive = ParadeDbJsonQuery.Range( + "rating", + lowerBound: 4, + upperBound: 5, + lowerInclusive: false, + upperInclusive: true + ); - var inclusiveHits = await ctx.Articles - .JsonSearch(a => a.Id, inclusive) + var inclusiveHits = await ctx + .Articles.JsonSearch(a => a.Id, inclusive) .Select(a => a.Title) .ToListAsync(); - var exclusiveHits = await ctx.Articles - .JsonSearch(a => a.Id, exclusive) + var exclusiveHits = await ctx + .Articles.JsonSearch(a => a.Id, exclusive) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonRangeOpenEndedTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonRangeOpenEndedTests.cs index e4c458d..f9d6be5 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonRangeOpenEndedTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonRangeOpenEndedTests.cs @@ -3,23 +3,29 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonRangeOpenEndedTests(ParadeDbFixture fixture) { +public class JsonRangeOpenEndedTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.Range omits the "upper_bound" key entirely // when upperBound is null — the `if (upperBound != null)` branch. The // existing JsonQueryTests/JsonDateTimeRangeTests always pass both bounds, // so a regression that always emits "upper_bound" (or crashes on null) // would only be caught here. [Fact] - public async Task Range_WithNullUpperBound_MatchesAllValuesAtOrAboveLowerBound() { + public async Task Range_WithNullUpperBound_MatchesAllValuesAtOrAboveLowerBound() + { await using var ctx = fixture.CreateDbContext(); // Ratings in the seed: Article 1=5, 2=4, 3=3, 4=5. lower=4 inclusive, // no upper → expect Articles 1, 2, 4 (rating >= 4); Article 3 excluded. - var query = ParadeDbJsonQuery.Range("rating", - lowerBound: 4, upperBound: null!, lowerInclusive: true); + var query = ParadeDbJsonQuery.Range( + "rating", + lowerBound: 4, + upperBound: null!, + lowerInclusive: true + ); - var hits = await ctx.Articles - .JsonSearch(a => a.Id, query) + var hits = await ctx + .Articles.JsonSearch(a => a.Id, query) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonRegexTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonRegexTests.cs index ec9fde1..3fe7527 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonRegexTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonRegexTests.cs @@ -3,19 +3,21 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonRegexTests(ParadeDbFixture fixture) { +public class JsonRegexTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.Regex builds {"regex":{"field":"...","pattern":"..."}} and // pg_search accepts it via the @@@ jsonb path. Distinct from the CLR Regex translation // (which uses pdb.regex(...)). A regression in the "regex"/"field"/"pattern" key names // would either fail to deserialize or return zero matches. [Fact] - public async Task Regex_Pattern_MatchesIndexedTokensMatchingExpression() { + public async Task Regex_Pattern_MatchesIndexedTokensMatchingExpression() + { await using var ctx = fixture.CreateDbContext(); var query = ParadeDbJsonQuery.Regex("content", "neur.*"); - var hits = await ctx.Articles - .JsonSearch(a => a.Id, query) + var hits = await ctx + .Articles.JsonSearch(a => a.Id, query) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonTermSetTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonTermSetTests.cs index abc5f19..f3992e0 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonTermSetTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/JsonTermSetTests.cs @@ -3,19 +3,21 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class JsonTermSetTests(ParadeDbFixture fixture) { +public class JsonTermSetTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbJsonQuery.TermSet produces {"term_set":{"field":"...","terms":[...]}} // — matches any of the listed exact terms on the Raw-tokenized category column. // Distinct from CLR MatchesTermSet (which translates to `=== ARRAY[...]`); this // exercises the JSON @@@ jsonb path and the params object[] → JsonArray conversion. [Fact] - public async Task TermSet_MultipleCategories_MatchesAnyListedTerm() { + public async Task TermSet_MultipleCategories_MatchesAnyListedTerm() + { await using var ctx = fixture.CreateDbContext(); var query = ParadeDbJsonQuery.TermSet("category", "machine-learning", "cooking"); - var hits = await ctx.Articles - .JsonSearch(a => a.Id, query) + var hits = await ctx + .Articles.JsonSearch(a => a.Id, query) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesAllBoostedTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesAllBoostedTests.cs index 5f36668..46c91f1 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesAllBoostedTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesAllBoostedTests.cs @@ -3,33 +3,41 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class MatchesAllBoostedTests(ParadeDbFixture fixture) { +public class MatchesAllBoostedTests(ParadeDbFixture fixture) +{ // Verifies MatchesAllBoosted translates to: column &&& 'query'::pdb.boost(factor) // with AND semantics — distinct from MatchesBoosted (||| / OR). A regression in // the method-call translator that drops the boost cast, or swaps the operator, // would either return matches with unboosted scores or change the result set. [Fact] - public async Task MatchesAllBoosted_AndOperator_AmplifiesScoreVsUnboostedMatchesAll() { + public async Task MatchesAllBoosted_AndOperator_AmplifiesScoreVsUnboostedMatchesAll() + { await using var ctx = fixture.CreateDbContext(); const double boostFactor = 5.0; - var boosted = await ctx.Articles - .Where(a => EF.Functions.MatchesAllBoosted(a.Content, "neural networks", boostFactor)) + var boosted = await ctx + .Articles.Where(a => + EF.Functions.MatchesAllBoosted(a.Content, "neural networks", boostFactor) + ) .Select(a => new { a.Title, Score = EF.Functions.Score(a.Id) }) .ToListAsync(); - var unboosted = await ctx.Articles - .Where(a => EF.Functions.MatchesAll(a.Content, "neural networks")) + var unboosted = await ctx + .Articles.Where(a => EF.Functions.MatchesAll(a.Content, "neural networks")) .Select(a => new { a.Title, Score = EF.Functions.Score(a.Id) }) .ToListAsync(); // Same AND semantics → same matching rows; only the score should differ. - Assert.Equal(unboosted.Select(x => x.Title).OrderBy(t => t), - boosted.Select(x => x.Title).OrderBy(t => t)); + Assert.Equal( + unboosted.Select(x => x.Title).OrderBy(t => t), + boosted.Select(x => x.Title).OrderBy(t => t) + ); Assert.Contains(boosted, x => x.Title == "Introduction to neural networks"); var boostedHit = boosted.Single(x => x.Title == "Introduction to neural networks"); var unboostedHit = unboosted.Single(x => x.Title == "Introduction to neural networks"); - Assert.True(boostedHit.Score > unboostedHit.Score * 2.0, - $"Boost factor {boostFactor} should amplify score substantially; " + - $"boosted={boostedHit.Score}, unboosted={unboostedHit.Score}."); + Assert.True( + boostedHit.Score > unboostedHit.Score * 2.0, + $"Boost factor {boostFactor} should amplify score substantially; " + + $"boosted={boostedHit.Score}, unboosted={unboostedHit.Score}." + ); } } diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesAllFuzzyBoostedTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesAllFuzzyBoostedTests.cs index 2d30771..0cff96b 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesAllFuzzyBoostedTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesAllFuzzyBoostedTests.cs @@ -3,35 +3,50 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class MatchesAllFuzzyBoostedTests(ParadeDbFixture fixture) { +public class MatchesAllFuzzyBoostedTests(ParadeDbFixture fixture) +{ // Verifies MatchesAllFuzzyBoosted translates to: column &&& 'query'::pdb.fuzzy(d)::pdb.boost(f). // Chains TWO cast operators on AND (&&&) — the most complex translation path in the // function set. A regression that drops either cast (fuzzy or boost), inverts cast // order, or swaps &&& for ||| would either change the result set (typos no longer // tolerated / OR semantics applied) or leave the score un-amplified. [Fact] - public async Task MatchesAllFuzzyBoosted_TypoTolerantAndOperator_AmplifiesScoreVsPlainFuzzyAll() { + public async Task MatchesAllFuzzyBoosted_TypoTolerantAndOperator_AmplifiesScoreVsPlainFuzzyAll() + { await using var ctx = fixture.CreateDbContext(); const int distance = 2; const double boostFactor = 5.0; - var boosted = await ctx.Articles - .Where(a => EF.Functions.MatchesAllFuzzyBoosted(a.Content, "nueral netwroks", distance, boostFactor)) + var boosted = await ctx + .Articles.Where(a => + EF.Functions.MatchesAllFuzzyBoosted( + a.Content, + "nueral netwroks", + distance, + boostFactor + ) + ) .Select(a => new { a.Title, Score = EF.Functions.Score(a.Id) }) .ToListAsync(); - var unboosted = await ctx.Articles - .Where(a => EF.Functions.MatchesAllFuzzy(a.Content, "nueral netwroks", distance)) + var unboosted = await ctx + .Articles.Where(a => + EF.Functions.MatchesAllFuzzy(a.Content, "nueral netwroks", distance) + ) .Select(a => new { a.Title, Score = EF.Functions.Score(a.Id) }) .ToListAsync(); // Same AND+fuzzy semantics → identical matches; only the score should differ. - Assert.Equal(unboosted.Select(x => x.Title).OrderBy(t => t), - boosted.Select(x => x.Title).OrderBy(t => t)); + Assert.Equal( + unboosted.Select(x => x.Title).OrderBy(t => t), + boosted.Select(x => x.Title).OrderBy(t => t) + ); Assert.Contains(boosted, x => x.Title == "Introduction to neural networks"); var boostedHit = boosted.Single(x => x.Title == "Introduction to neural networks"); var unboostedHit = unboosted.Single(x => x.Title == "Introduction to neural networks"); - Assert.True(boostedHit.Score > unboostedHit.Score * 2.0, - $"Boost factor {boostFactor} should amplify the fuzzy AND score; " + - $"boosted={boostedHit.Score}, unboosted={unboostedHit.Score}."); + Assert.True( + boostedHit.Score > unboostedHit.Score * 2.0, + $"Boost factor {boostFactor} should amplify the fuzzy AND score; " + + $"boosted={boostedHit.Score}, unboosted={unboostedHit.Score}." + ); } } diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesAllFuzzyOptionsTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesAllFuzzyOptionsTests.cs index c944b40..e61e25d 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesAllFuzzyOptionsTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesAllFuzzyOptionsTests.cs @@ -3,7 +3,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class MatchesAllFuzzyOptionsTests(ParadeDbFixture fixture) { +public class MatchesAllFuzzyOptionsTests(ParadeDbFixture fixture) +{ // Verifies the 5-arg MatchesAllFuzzy overload emits conjunction_mode => true on the // shared pdb.match(...) translation path (see BuildFuzzyMatchFunc in the translator). // The OR-mode sibling — MatchesFuzzy 5-arg — goes through the same helper without that @@ -11,21 +12,36 @@ public class MatchesAllFuzzyOptionsTests(ParadeDbFixture fixture) { // OR branch (or forgets the conjunction_mode arg) would let docs match on a single // fuzzy term, making AND-fuzzy silently behave like OR-fuzzy. [Fact] - public async Task MatchesAllFuzzy_WithFullOptions_RequiresEveryTermToFuzzyMatch() { + public async Task MatchesAllFuzzy_WithFullOptions_RequiresEveryTermToFuzzyMatch() + { await using var ctx = fixture.CreateDbContext(); // "nueral" ↔ "neural" is an adjacent-character swap (transposition distance 1). // "xyzzzzz" has no edit-distance-1 neighbor anywhere in the seeded content. // OR-fuzzy must still match Article 1 on the "nueral" hit alone. - var orHits = await ctx.Articles - .Where(a => EF.Functions.MatchesFuzzy(a.Content, "nueral xyzzzzz", 1, - prefix: false, transpositionCostOne: true)) + var orHits = await ctx + .Articles.Where(a => + EF.Functions.MatchesFuzzy( + a.Content, + "nueral xyzzzzz", + 1, + prefix: false, + transpositionCostOne: true + ) + ) .Select(a => a.Title) .ToListAsync(); // AND-fuzzy must require every term — "xyzzzzz" fuzzy-matches nothing → no hits. - var andHits = await ctx.Articles - .Where(a => EF.Functions.MatchesAllFuzzy(a.Content, "nueral xyzzzzz", 1, - prefix: false, transpositionCostOne: true)) + var andHits = await ctx + .Articles.Where(a => + EF.Functions.MatchesAllFuzzy( + a.Content, + "nueral xyzzzzz", + 1, + prefix: false, + transpositionCostOne: true + ) + ) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesFuzzyBoostedTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesFuzzyBoostedTests.cs index 083d6ed..444be56 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesFuzzyBoostedTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesFuzzyBoostedTests.cs @@ -3,34 +3,42 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class MatchesFuzzyBoostedTests(ParadeDbFixture fixture) { +public class MatchesFuzzyBoostedTests(ParadeDbFixture fixture) +{ // Verifies MatchesFuzzyBoosted translates to: column ||| 'query'::pdb.fuzzy(d)::pdb.boost(f). // OR variant of the already-tested MatchesAllFuzzyBoosted — same chained-cast path, // different boolean operator (||| vs &&&). A regression that swapped operators or // dropped a cast would either change the match set or leave the score un-amplified. [Fact] - public async Task MatchesFuzzyBoosted_OrOperator_AmplifiesScoreVsUnboostedFuzzy() { + public async Task MatchesFuzzyBoosted_OrOperator_AmplifiesScoreVsUnboostedFuzzy() + { await using var ctx = fixture.CreateDbContext(); const int distance = 2; const double boostFactor = 5.0; - var boosted = await ctx.Articles - .Where(a => EF.Functions.MatchesFuzzyBoosted(a.Content, "nueral", distance, boostFactor)) + var boosted = await ctx + .Articles.Where(a => + EF.Functions.MatchesFuzzyBoosted(a.Content, "nueral", distance, boostFactor) + ) .Select(a => new { a.Title, Score = EF.Functions.Score(a.Id) }) .ToListAsync(); - var unboosted = await ctx.Articles - .Where(a => EF.Functions.MatchesFuzzy(a.Content, "nueral", distance)) + var unboosted = await ctx + .Articles.Where(a => EF.Functions.MatchesFuzzy(a.Content, "nueral", distance)) .Select(a => new { a.Title, Score = EF.Functions.Score(a.Id) }) .ToListAsync(); // Same OR+fuzzy semantics → identical matches; only the score should differ. - Assert.Equal(unboosted.Select(x => x.Title).OrderBy(t => t), - boosted.Select(x => x.Title).OrderBy(t => t)); + Assert.Equal( + unboosted.Select(x => x.Title).OrderBy(t => t), + boosted.Select(x => x.Title).OrderBy(t => t) + ); Assert.Contains(boosted, x => x.Title == "Introduction to neural networks"); var boostedHit = boosted.Single(x => x.Title == "Introduction to neural networks"); var unboostedHit = unboosted.Single(x => x.Title == "Introduction to neural networks"); - Assert.True(boostedHit.Score > unboostedHit.Score * 2.0, - $"Boost factor {boostFactor} should amplify the fuzzy OR score; " + - $"boosted={boostedHit.Score}, unboosted={unboostedHit.Score}."); + Assert.True( + boostedHit.Score > unboostedHit.Score * 2.0, + $"Boost factor {boostFactor} should amplify the fuzzy OR score; " + + $"boosted={boostedHit.Score}, unboosted={unboostedHit.Score}." + ); } } diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesFuzzyOptionsTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesFuzzyOptionsTests.cs index 8051d3e..9ba472a 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesFuzzyOptionsTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesFuzzyOptionsTests.cs @@ -3,7 +3,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class MatchesFuzzyOptionsTests(ParadeDbFixture fixture) { +public class MatchesFuzzyOptionsTests(ParadeDbFixture fixture) +{ // Verifies the 5-arg MatchesFuzzy overload translates the transpositionCostOne flag // into pdb.fuzzy(distance, prefix, transposition_cost_one) so an adjacent-character // swap counts as 1 edit (default: 2). "nueral" vs "neural" has Levenshtein distance 2 @@ -11,17 +12,32 @@ public class MatchesFuzzyOptionsTests(ParadeDbFixture fixture) { // that makes the match succeed. A regression that drops the extra args (or swaps them) // would flip both assertions. [Fact] - public async Task MatchesFuzzy_WithTranspositionCostOne_MatchesAdjacentSwapAtDistance1() { + public async Task MatchesFuzzy_WithTranspositionCostOne_MatchesAdjacentSwapAtDistance1() + { await using var ctx = fixture.CreateDbContext(); - var withTransposition = await ctx.Articles - .Where(a => EF.Functions.MatchesFuzzy(a.Content, "nueral", 1, - prefix: false, transpositionCostOne: true)) + var withTransposition = await ctx + .Articles.Where(a => + EF.Functions.MatchesFuzzy( + a.Content, + "nueral", + 1, + prefix: false, + transpositionCostOne: true + ) + ) .Select(a => a.Title) .ToListAsync(); - var withoutTransposition = await ctx.Articles - .Where(a => EF.Functions.MatchesFuzzy(a.Content, "nueral", 1, - prefix: false, transpositionCostOne: false)) + var withoutTransposition = await ctx + .Articles.Where(a => + EF.Functions.MatchesFuzzy( + a.Content, + "nueral", + 1, + prefix: false, + transpositionCostOne: false + ) + ) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesFuzzyPrefixTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesFuzzyPrefixTests.cs index 5bd7d7d..4aa053c 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesFuzzyPrefixTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesFuzzyPrefixTests.cs @@ -3,7 +3,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class MatchesFuzzyPrefixTests(ParadeDbFixture fixture) { +public class MatchesFuzzyPrefixTests(ParadeDbFixture fixture) +{ // Verifies the 5-arg MatchesFuzzy overload's "prefix" flag — exempts the // initial substring from edit distance, equivalent to prefix matching. // MatchesFuzzyOptionsTests and MatchesAllFuzzyOptionsTests both only @@ -12,7 +13,8 @@ public class MatchesFuzzyPrefixTests(ParadeDbFixture fixture) { // untested. A regression that swaps the prefix and transpositionCostOne // args (both bool, same position-class) would flip this test. [Fact] - public async Task MatchesFuzzy_WithPrefixTrue_MatchesTokensStartingWithQuery() { + public async Task MatchesFuzzy_WithPrefixTrue_MatchesTokensStartingWithQuery() + { await using var ctx = fixture.CreateDbContext(); // "neurla" is Levenshtein distance 2 from "neural". With distance: 1 @@ -20,14 +22,28 @@ public async Task MatchesFuzzy_WithPrefixTrue_MatchesTokensStartingWithQuery() { // With distance: 1 and prefix: true → the prefix flag exempts the // initial substring from edit distance, so distance: 1 is enough // → matches Article 1. - var prefixOff = await ctx.Articles - .Where(a => EF.Functions.MatchesFuzzy(a.Content, "neurla", 1, - prefix: false, transpositionCostOne: false)) + var prefixOff = await ctx + .Articles.Where(a => + EF.Functions.MatchesFuzzy( + a.Content, + "neurla", + 1, + prefix: false, + transpositionCostOne: false + ) + ) .Select(a => a.Title) .ToListAsync(); - var prefixOn = await ctx.Articles - .Where(a => EF.Functions.MatchesFuzzy(a.Content, "neurla", 1, - prefix: true, transpositionCostOne: false)) + var prefixOn = await ctx + .Articles.Where(a => + EF.Functions.MatchesFuzzy( + a.Content, + "neurla", + 1, + prefix: true, + transpositionCostOne: false + ) + ) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesTermFuzzyOptionsTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesTermFuzzyOptionsTests.cs index 170a8a3..8984dc4 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesTermFuzzyOptionsTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesTermFuzzyOptionsTests.cs @@ -3,7 +3,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class MatchesTermFuzzyOptionsTests(ParadeDbFixture fixture) { +public class MatchesTermFuzzyOptionsTests(ParadeDbFixture fixture) +{ // Verifies the 5-arg MatchesTermFuzzy overload translates to // pdb.fuzzy_term(value, distance, transposition_cost_one, prefix) — a POSITIONAL // function call (BuildFuzzyTermFunc), distinct from the named-arg pdb.match(...) @@ -11,18 +12,33 @@ public class MatchesTermFuzzyOptionsTests(ParadeDbFixture fixture) { // the positional order (e.g. emits prefix before transposition_cost_one) would // flip the meaning of the two booleans and silently invert both assertions. [Fact] - public async Task MatchesTermFuzzy_WithTranspositionCostOne_MatchesAdjacentSwapAtDistance1() { + public async Task MatchesTermFuzzy_WithTranspositionCostOne_MatchesAdjacentSwapAtDistance1() + { await using var ctx = fixture.CreateDbContext(); // "nueral" ↔ "neural" is an adjacent swap (transposition distance 1, Levenshtein 2). - var withTransposition = await ctx.Articles - .Where(a => EF.Functions.MatchesTermFuzzy(a.Content, "nueral", 1, - prefix: false, transpositionCostOne: true)) + var withTransposition = await ctx + .Articles.Where(a => + EF.Functions.MatchesTermFuzzy( + a.Content, + "nueral", + 1, + prefix: false, + transpositionCostOne: true + ) + ) .Select(a => a.Title) .ToListAsync(); - var withoutTransposition = await ctx.Articles - .Where(a => EF.Functions.MatchesTermFuzzy(a.Content, "nueral", 1, - prefix: false, transpositionCostOne: false)) + var withoutTransposition = await ctx + .Articles.Where(a => + EF.Functions.MatchesTermFuzzy( + a.Content, + "nueral", + 1, + prefix: false, + transpositionCostOne: false + ) + ) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesTermFuzzyPrefixTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesTermFuzzyPrefixTests.cs index cd334ea..996deb7 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesTermFuzzyPrefixTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MatchesTermFuzzyPrefixTests.cs @@ -3,7 +3,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class MatchesTermFuzzyPrefixTests(ParadeDbFixture fixture) { +public class MatchesTermFuzzyPrefixTests(ParadeDbFixture fixture) +{ // Verifies the 5-arg MatchesTermFuzzy overload's "prefix" flag — routes // through BuildFuzzyTermFunc which emits pdb.fuzzy_term(value, distance, // transposition_cost_one, prefix) as a POSITIONAL call. The existing @@ -11,20 +12,35 @@ public class MatchesTermFuzzyPrefixTests(ParadeDbFixture fixture) { // prefix at false), so a regression that swaps positional args 3 and 4 // would slip through it but be caught here. [Fact] - public async Task MatchesTermFuzzy_WithPrefixTrue_ExtendsMatchPastEditDistance() { + public async Task MatchesTermFuzzy_WithPrefixTrue_ExtendsMatchPastEditDistance() + { await using var ctx = fixture.CreateDbContext(); // "neurla" → "neural" is Levenshtein distance 2. At distance: 1 with // prefix: false, no match (distance too small). With prefix: true the // initial substring is exempt from edit distance, allowing the match. - var prefixOff = await ctx.Articles - .Where(a => EF.Functions.MatchesTermFuzzy(a.Content, "neurla", 1, - prefix: false, transpositionCostOne: false)) + var prefixOff = await ctx + .Articles.Where(a => + EF.Functions.MatchesTermFuzzy( + a.Content, + "neurla", + 1, + prefix: false, + transpositionCostOne: false + ) + ) .Select(a => a.Title) .ToListAsync(); - var prefixOn = await ctx.Articles - .Where(a => EF.Functions.MatchesTermFuzzy(a.Content, "neurla", 1, - prefix: true, transpositionCostOne: false)) + var prefixOn = await ctx + .Articles.Where(a => + EF.Functions.MatchesTermFuzzy( + a.Content, + "neurla", + 1, + prefix: true, + transpositionCostOne: false + ) + ) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MoreLikeThisFieldsTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MoreLikeThisFieldsTests.cs index 55421df..d764d69 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MoreLikeThisFieldsTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/MoreLikeThisFieldsTests.cs @@ -3,18 +3,22 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class MoreLikeThisFieldsTests(ParadeDbFixture fixture) { +public class MoreLikeThisFieldsTests(ParadeDbFixture fixture) +{ // Verifies MoreLikeThis(keyField, documentId, params string[] fields) translates to // pdb.more_like_this(documentId, ARRAY['field1', ...]) — restricting the similarity // computation to the named fields. The 2-arg form is already covered; this exercises // the array-of-strings parameter, distinct from GH-15/GH-17 translation paths. [Fact] - public async Task MoreLikeThis_WithFieldsRestriction_ExecutesAndReturnsSimilarArticle() { + public async Task MoreLikeThis_WithFieldsRestriction_ExecutesAndReturnsSimilarArticle() + { await using var ctx = fixture.CreateDbContext(); - var seed = await ctx.Articles.SingleAsync(a => a.Title == "Introduction to neural networks"); + var seed = await ctx.Articles.SingleAsync(a => + a.Title == "Introduction to neural networks" + ); - var related = await ctx.Articles - .Where(a => EF.Functions.MoreLikeThis(a.Id, seed.Id, "content")) + var related = await ctx + .Articles.Where(a => EF.Functions.MoreLikeThis(a.Id, seed.Id, "content")) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/OrderByScoreAscendingTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/OrderByScoreAscendingTests.cs index 48b05dc..5305a8d 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/OrderByScoreAscendingTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/OrderByScoreAscendingTests.cs @@ -3,29 +3,33 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class OrderByScoreAscendingTests(ParadeDbFixture fixture) { +public class OrderByScoreAscendingTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbSearchExtensions.OrderByScore composes an ascending Score() // ORDER BY equivalent to .OrderBy(a => EF.Functions.Score(a.Id)) by hand. Pairs // with the already-covered OrderByScoreDescending — both branches of the shared // reflection-built ApplyScoreOrdering helper need a regression guard. [Fact] - public async Task OrderByScore_RanksMatchingArticles_SameOrderAsExplicitScoreSelector() { + public async Task OrderByScore_RanksMatchingArticles_SameOrderAsExplicitScoreSelector() + { await using var ctx = fixture.CreateDbContext(); - var viaExtension = await ctx.Articles - .Where(a => EF.Functions.Matches(a.Content, "models machine learning")) + var viaExtension = await ctx + .Articles.Where(a => EF.Functions.Matches(a.Content, "models machine learning")) .OrderByScore(a => a.Id) .Select(a => a.Title) .ToListAsync(); - var viaExplicit = await ctx.Articles - .Where(a => EF.Functions.Matches(a.Content, "models machine learning")) + var viaExplicit = await ctx + .Articles.Where(a => EF.Functions.Matches(a.Content, "models machine learning")) .OrderBy(a => EF.Functions.Score(a.Id)) .Select(a => a.Title) .ToListAsync(); - Assert.True(viaExtension.Count >= 2, - $"Need at least 2 matches for ordering to be observable; got {viaExtension.Count}."); + Assert.True( + viaExtension.Count >= 2, + $"Need at least 2 matches for ordering to be observable; got {viaExtension.Count}." + ); Assert.Equal(viaExplicit, viaExtension); } } diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/OrderByScoreTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/OrderByScoreTests.cs index af90ba5..8b14427 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/OrderByScoreTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/OrderByScoreTests.cs @@ -3,30 +3,34 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class OrderByScoreTests(ParadeDbFixture fixture) { +public class OrderByScoreTests(ParadeDbFixture fixture) +{ // Verifies ParadeDbSearchExtensions.OrderByScoreDescending composes a Score() ORDER BY // equivalent to writing .OrderByDescending(a => EF.Functions.Score(a.Id)) by hand. // A regression in the reflection-built expression (wrong Queryable overload, missing // Convert for a value-type key, broken StripConvert) would either throw at translation // or produce a different ordering than the explicit form. [Fact] - public async Task OrderByScoreDescending_RanksMatchingArticles_SameOrderAsExplicitScoreSelector() { + public async Task OrderByScoreDescending_RanksMatchingArticles_SameOrderAsExplicitScoreSelector() + { await using var ctx = fixture.CreateDbContext(); - var viaExtension = await ctx.Articles - .Where(a => EF.Functions.Matches(a.Content, "models machine learning")) + var viaExtension = await ctx + .Articles.Where(a => EF.Functions.Matches(a.Content, "models machine learning")) .OrderByScoreDescending(a => a.Id) .Select(a => a.Title) .ToListAsync(); - var viaExplicit = await ctx.Articles - .Where(a => EF.Functions.Matches(a.Content, "models machine learning")) + var viaExplicit = await ctx + .Articles.Where(a => EF.Functions.Matches(a.Content, "models machine learning")) .OrderByDescending(a => EF.Functions.Score(a.Id)) .Select(a => a.Title) .ToListAsync(); - Assert.True(viaExtension.Count >= 2, - $"Need at least 2 matches for ordering to be observable; got {viaExtension.Count}."); + Assert.True( + viaExtension.Count >= 2, + $"Need at least 2 matches for ordering to be observable; got {viaExtension.Count}." + ); Assert.Equal(viaExplicit, viaExtension); } } diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/ParadeDbFixture.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/ParadeDbFixture.cs index c116319..7faaba4 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/ParadeDbFixture.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/ParadeDbFixture.cs @@ -3,7 +3,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; -public sealed class ParadeDbFixture : IAsyncLifetime { +public sealed class ParadeDbFixture : IAsyncLifetime +{ private readonly PostgreSqlContainer _container = new PostgreSqlBuilder() .WithImage("paradedb/paradedb:latest") .WithDatabase("paradedb_test") @@ -13,7 +14,8 @@ public sealed class ParadeDbFixture : IAsyncLifetime { public string ConnectionString { get; private set; } = default!; - public async Task InitializeAsync() { + public async Task InitializeAsync() + { await _container.StartAsync(); ConnectionString = _container.GetConnectionString(); @@ -22,85 +24,108 @@ public async Task InitializeAsync() { await SeedAsync(ctx); } - public async Task DisposeAsync() { + public async Task DisposeAsync() + { await _container.DisposeAsync(); } - public IntegrationDbContext CreateDbContext() => new( - new DbContextOptionsBuilder() - .UseNpgsql(ConnectionString, n => n.UseParadeDb()) - .Options); + public IntegrationDbContext CreateDbContext() => + new( + new DbContextOptionsBuilder() + .UseNpgsql(ConnectionString, n => n.UseParadeDb()) + .Options + ); - private static async Task SeedAsync(IntegrationDbContext ctx) { + private static async Task SeedAsync(IntegrationDbContext ctx) + { ctx.Articles.AddRange( - new Article { + new Article + { Title = "Introduction to neural networks", - Content = "Deep learning models running on GPUs revolutionize machine learning. Neural networks are layered.", + Content = + "Deep learning models running on GPUs revolutionize machine learning. Neural networks are layered.", Category = "machine-learning", Rating = 5, }, - new Article { + new Article + { Title = "Transformer architectures", - Content = "The attention mechanism powers modern language models. Transformers run efficiently on TPUs.", + Content = + "The attention mechanism powers modern language models. Transformers run efficiently on TPUs.", Category = "machine-learning", Rating = 4, }, - new Article { + new Article + { Title = "Quantum computing fundamentals", - Content = "Qubits and entanglement enable parallel computation beyond classical bits.", + Content = + "Qubits and entanglement enable parallel computation beyond classical bits.", Category = "physics", Rating = 3, }, - new Article { + new Article + { Title = "Cooking pasta perfectly", - Content = "Salt the water generously and cook the pasta until al dente. Taste it as you go.", + Content = + "Salt the water generously and cook the pasta until al dente. Taste it as you go.", Category = "cooking", Rating = 5, - }); + } + ); ctx.Products.AddRange( - new Product { + new Product + { Name = "Ultra book laptop", InStock = true, ReleasedAt = new DateTime(2024, 6, 1, 0, 0, 0, DateTimeKind.Utc), Specs = """{"weight": 1200, "color": "silver"}""", }, - new Product { + new Product + { Name = "Mechanical keyboard", InStock = false, ReleasedAt = new DateTime(2023, 1, 15, 0, 0, 0, DateTimeKind.Utc), Specs = """{"weight": 900, "color": "black"}""", }, - new Product { + new Product + { Name = "Wireless mouse", InStock = true, ReleasedAt = new DateTime(2024, 11, 20, 0, 0, 0, DateTimeKind.Utc), Specs = """{"weight": 80, "color": "white"}""", - }); + } + ); ctx.KeywordRecords.AddRange( new KeywordRecord { Code = "ABC-123" }, - new KeywordRecord { Code = "XYZ-789" }); + new KeywordRecord { Code = "XYZ-789" } + ); ctx.NgramRecords.AddRange( new NgramRecord { Body = "supercalifragilistic" }, - new NgramRecord { Body = "ordinary text" }); + new NgramRecord { Body = "ordinary text" } + ); ctx.IcuRecords.AddRange( new IcuRecord { Body = "Café résumé naïve" }, - new IcuRecord { Body = "Plain ASCII text" }); + new IcuRecord { Body = "Plain ASCII text" } + ); ctx.SourceCodeRecords.AddRange( new SourceCodeRecord { Snippet = "GetUserById" }, - new SourceCodeRecord { Snippet = "SaveChangesAsync" }); + new SourceCodeRecord { Snippet = "SaveChangesAsync" } + ); ctx.RegexRecords.AddRange( new RegexRecord { Body = "alpha beta gamma" }, - new RegexRecord { Body = "delta epsilon" }); + new RegexRecord { Body = "delta epsilon" } + ); ctx.GermanArticles.AddRange( new GermanArticle { Content = "Die Häuser sind groß und schön." }, - new GermanArticle { Content = "Der Mann läuft schnell." }); + new GermanArticle { Content = "Der Mann läuft schnell." } + ); await ctx.SaveChangesAsync(); } diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/PhrasePrefixMaxExpansionsTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/PhrasePrefixMaxExpansionsTests.cs index db14bd4..b13db3f 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/PhrasePrefixMaxExpansionsTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/PhrasePrefixMaxExpansionsTests.cs @@ -3,16 +3,18 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class PhrasePrefixMaxExpansionsTests(ParadeDbFixture fixture) { +public class PhrasePrefixMaxExpansionsTests(ParadeDbFixture fixture) +{ // Verifies the 3-arg PhrasePrefix overload (with maxExpansions) translates to // pdb.phrase_prefix(ARRAY[...], max_expansion => N) and runs. Guards against drift // back to the plural "max_expansions" — pg_search's named arg is singular. [Fact] - public async Task PhrasePrefix_WithMaxExpansions_MatchesContainingDocument() { + public async Task PhrasePrefix_WithMaxExpansions_MatchesContainingDocument() + { await using var ctx = fixture.CreateDbContext(); - var hits = await ctx.Articles - .Where(a => EF.Functions.PhrasePrefix(a.Content, 5, "neural", "net")) + var hits = await ctx + .Articles.Where(a => EF.Functions.PhrasePrefix(a.Content, 5, "neural", "net")) .Select(a => a.Title) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/ScoringAndSnippetTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/ScoringAndSnippetTests.cs index ba1baf1..9ae95c4 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/ScoringAndSnippetTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/ScoringAndSnippetTests.cs @@ -3,30 +3,37 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class ScoringAndSnippetTests(ParadeDbFixture fixture) { +public class ScoringAndSnippetTests(ParadeDbFixture fixture) +{ [Fact] - public async Task Score_OrdersByRelevance() { + public async Task Score_OrdersByRelevance() + { await using var ctx = fixture.CreateDbContext(); - var ranked = await ctx.Articles - .Where(a => EF.Functions.Matches(a.Content, "neural networks")) + var ranked = await ctx + .Articles.Where(a => EF.Functions.Matches(a.Content, "neural networks")) .Select(a => new { a.Title, Score = EF.Functions.Score(a.Id) }) .OrderByDescending(x => x.Score) .ToListAsync(); Assert.NotEmpty(ranked); Assert.True(ranked[0].Score > 0); - if (ranked.Count > 1) { - Assert.True(ranked[0].Score >= ranked[1].Score, "Results should be ordered by descending score."); + if (ranked.Count > 1) + { + Assert.True( + ranked[0].Score >= ranked[1].Score, + "Results should be ordered by descending score." + ); } } [Fact] - public async Task Snippet_HighlightsMatchedTerms() { + public async Task Snippet_HighlightsMatchedTerms() + { await using var ctx = fixture.CreateDbContext(); - var snippets = await ctx.Articles - .Where(a => EF.Functions.Matches(a.Content, "neural networks")) + var snippets = await ctx + .Articles.Where(a => EF.Functions.Matches(a.Content, "neural networks")) .Select(a => EF.Functions.Snippet(a.Content, "", "", 100)) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/SearchApiTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/SearchApiTests.cs index c8873ca..5472d73 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/SearchApiTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/SearchApiTests.cs @@ -3,28 +3,33 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class SearchApiTests(ParadeDbFixture fixture) { +public class SearchApiTests(ParadeDbFixture fixture) +{ [Fact] - public async Task MatchesBoosted_RaisesScoreOfBoostedTerm() { + public async Task MatchesBoosted_RaisesScoreOfBoostedTerm() + { await using var ctx = fixture.CreateDbContext(); - var withBoost = await ctx.Articles - .Where(a => EF.Functions.MatchesBoosted(a.Content, "neural", 5.0)) + var withBoost = await ctx + .Articles.Where(a => EF.Functions.MatchesBoosted(a.Content, "neural", 5.0)) .Select(a => new { a.Title, Score = EF.Functions.Score(a.Id) }) .OrderByDescending(a => a.Score) .ToListAsync(); Assert.NotEmpty(withBoost); - Assert.True(withBoost[0].Score > 1.0, - $"Boosted score should be amplified above default 1.0; was {withBoost[0].Score}."); + Assert.True( + withBoost[0].Score > 1.0, + $"Boosted score should be amplified above default 1.0; was {withBoost[0].Score}." + ); } [Fact] - public async Task MatchesTermSet_MatchesAnyToken() { + public async Task MatchesTermSet_MatchesAnyToken() + { await using var ctx = fixture.CreateDbContext(); - var hits = await ctx.Articles - .Where(a => EF.Functions.MatchesTermSet(a.Content, "gpus", "tpus")) + var hits = await ctx + .Articles.Where(a => EF.Functions.MatchesTermSet(a.Content, "gpus", "tpus")) .Select(a => a.Title) .ToListAsync(); @@ -33,14 +38,15 @@ public async Task MatchesTermSet_MatchesAnyToken() { } [Fact] - public async Task MatchesPhraseWithSlop_AllowsWordsBetweenTerms() { + public async Task MatchesPhraseWithSlop_AllowsWordsBetweenTerms() + { await using var ctx = fixture.CreateDbContext(); - var exact = await ctx.Articles - .Where(a => EF.Functions.MatchesPhrase(a.Content, "deep models")) + var exact = await ctx + .Articles.Where(a => EF.Functions.MatchesPhrase(a.Content, "deep models")) .CountAsync(); - var withSlop = await ctx.Articles - .Where(a => EF.Functions.MatchesPhrase(a.Content, "deep models", 2)) + var withSlop = await ctx + .Articles.Where(a => EF.Functions.MatchesPhrase(a.Content, "deep models", 2)) .CountAsync(); Assert.Equal(0, exact); @@ -48,11 +54,12 @@ public async Task MatchesPhraseWithSlop_AllowsWordsBetweenTerms() { } [Fact] - public async Task MatchesAllFuzzy_AndOperatorToleratesTypos() { + public async Task MatchesAllFuzzy_AndOperatorToleratesTypos() + { await using var ctx = fixture.CreateDbContext(); - var hits = await ctx.Articles - .Where(a => EF.Functions.MatchesAllFuzzy(a.Content, "nueral netwroks", 2)) + var hits = await ctx + .Articles.Where(a => EF.Functions.MatchesAllFuzzy(a.Content, "nueral netwroks", 2)) .Select(a => a.Title) .ToListAsync(); @@ -60,11 +67,12 @@ public async Task MatchesAllFuzzy_AndOperatorToleratesTypos() { } [Fact] - public async Task MatchesTermFuzzy_SingleTermToleratesTypos() { + public async Task MatchesTermFuzzy_SingleTermToleratesTypos() + { await using var ctx = fixture.CreateDbContext(); - var hits = await ctx.Articles - .Where(a => EF.Functions.MatchesTermFuzzy(a.Content, "neurla", 2)) + var hits = await ctx + .Articles.Where(a => EF.Functions.MatchesTermFuzzy(a.Content, "neurla", 2)) .Select(a => a.Title) .ToListAsync(); @@ -72,11 +80,12 @@ public async Task MatchesTermFuzzy_SingleTermToleratesTypos() { } [Fact] - public async Task Snippets_ReturnsHighlightedExcerptArray() { + public async Task Snippets_ReturnsHighlightedExcerptArray() + { await using var ctx = fixture.CreateDbContext(); - var snippets = await ctx.Articles - .Where(a => EF.Functions.Matches(a.Content, "neural")) + var snippets = await ctx + .Articles.Where(a => EF.Functions.Matches(a.Content, "neural")) .Select(a => EF.Functions.Snippets(a.Content, 80, 3, 0)) .ToListAsync(); @@ -85,11 +94,12 @@ public async Task Snippets_ReturnsHighlightedExcerptArray() { } [Fact] - public async Task Regex_FindsTokensMatchingPattern() { + public async Task Regex_FindsTokensMatchingPattern() + { await using var ctx = fixture.CreateDbContext(); - var hits = await ctx.Articles - .Where(a => EF.Functions.Regex(a.Content, "neur.*")) + var hits = await ctx + .Articles.Where(a => EF.Functions.Regex(a.Content, "neur.*")) .Select(a => a.Title) .ToListAsync(); @@ -97,11 +107,12 @@ public async Task Regex_FindsTokensMatchingPattern() { } [Fact] - public async Task PhrasePrefix_MatchesPartialLastWord() { + public async Task PhrasePrefix_MatchesPartialLastWord() + { await using var ctx = fixture.CreateDbContext(); - var hits = await ctx.Articles - .Where(a => EF.Functions.PhrasePrefix(a.Content, "neural", "net")) + var hits = await ctx + .Articles.Where(a => EF.Functions.PhrasePrefix(a.Content, "neural", "net")) .Select(a => a.Title) .ToListAsync(); @@ -109,12 +120,15 @@ public async Task PhrasePrefix_MatchesPartialLastWord() { } [Fact] - public async Task Parse_LenientMode_IgnoresMalformedSyntax() { + public async Task Parse_LenientMode_IgnoresMalformedSyntax() + { await using var ctx = fixture.CreateDbContext(); // Trailing operator would normally throw; lenient + conjunctionMode lets it parse. - var hits = await ctx.Articles - .Where(a => EF.Functions.Parse(a.Id, "neural networks", lenient: true, conjunctionMode: true)) + var hits = await ctx + .Articles.Where(a => + EF.Functions.Parse(a.Id, "neural networks", lenient: true, conjunctionMode: true) + ) .Select(a => a.Title) .ToListAsync(); @@ -122,12 +136,16 @@ public async Task Parse_LenientMode_IgnoresMalformedSyntax() { } [Fact] - public async Task ComposedLinq_SearchPlusWhereAndProjection_Works() { + public async Task ComposedLinq_SearchPlusWhereAndProjection_Works() + { await using var ctx = fixture.CreateDbContext(); - var top = await ctx.Articles - .Where(a => EF.Functions.Matches(a.Content, "neural networks") && a.Rating >= 4) - .Select(a => new { + var top = await ctx + .Articles.Where(a => + EF.Functions.Matches(a.Content, "neural networks") && a.Rating >= 4 + ) + .Select(a => new + { a.Title, Snippet = EF.Functions.Snippet(a.Content, "", "", 60), Score = EF.Functions.Score(a.Id), diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/SnippetDefaultTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/SnippetDefaultTests.cs index fe4707b..4fea727 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/SnippetDefaultTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/SnippetDefaultTests.cs @@ -3,17 +3,19 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class SnippetDefaultTests(ParadeDbFixture fixture) { +public class SnippetDefaultTests(ParadeDbFixture fixture) +{ // Verifies the no-arg Snippet overload translates to bare pdb.snippet(column) — distinct // from the parameterised pdb.snippet(column, start_tag => ..., end_tag => ..., max_num_chars => ...) // form that's already tested. Default ParadeDB tags are .... A regression that // routed through the named-arg path would either fail or change the highlight markers. [Fact] - public async Task Snippet_NoArgs_HighlightsWithDefaultBoldTags() { + public async Task Snippet_NoArgs_HighlightsWithDefaultBoldTags() + { await using var ctx = fixture.CreateDbContext(); - var snippets = await ctx.Articles - .Where(a => EF.Functions.Matches(a.Content, "neural networks")) + var snippets = await ctx + .Articles.Where(a => EF.Functions.Matches(a.Content, "neural networks")) .Select(a => EF.Functions.Snippet(a.Content)) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/TokenizerTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/TokenizerTests.cs index 1194f85..fba590a 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/TokenizerTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests/TokenizerTests.cs @@ -3,13 +3,15 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.IntegrationTests; [Collection(nameof(ParadeDbCollection))] -public class TokenizerTests(ParadeDbFixture fixture) { +public class TokenizerTests(ParadeDbFixture fixture) +{ [Fact] - public async Task KeywordTokenizer_TreatsCodeAsSingleToken() { + public async Task KeywordTokenizer_TreatsCodeAsSingleToken() + { await using var ctx = fixture.CreateDbContext(); - var hits = await ctx.KeywordRecords - .Where(k => EF.Functions.MatchesTerm(k.Code, "ABC-123")) + var hits = await ctx + .KeywordRecords.Where(k => EF.Functions.MatchesTerm(k.Code, "ABC-123")) .Select(k => k.Code) .ToListAsync(); @@ -18,11 +20,12 @@ public async Task KeywordTokenizer_TreatsCodeAsSingleToken() { } [Fact] - public async Task NgramTokenizer_MatchesSubstring() { + public async Task NgramTokenizer_MatchesSubstring() + { await using var ctx = fixture.CreateDbContext(); - var hits = await ctx.NgramRecords - .Where(n => EF.Functions.MatchesTerm(n.Body, "frag")) + var hits = await ctx + .NgramRecords.Where(n => EF.Functions.MatchesTerm(n.Body, "frag")) .Select(n => n.Body) .ToListAsync(); @@ -30,11 +33,12 @@ public async Task NgramTokenizer_MatchesSubstring() { } [Fact] - public async Task IcuTokenizer_HandlesUnicodeText() { + public async Task IcuTokenizer_HandlesUnicodeText() + { await using var ctx = fixture.CreateDbContext(); - var hits = await ctx.IcuRecords - .Where(i => EF.Functions.Matches(i.Body, "café")) + var hits = await ctx + .IcuRecords.Where(i => EF.Functions.Matches(i.Body, "café")) .Select(i => i.Body) .ToListAsync(); @@ -42,11 +46,12 @@ public async Task IcuTokenizer_HandlesUnicodeText() { } [Fact] - public async Task SourceCodeTokenizer_SplitsCamelCase() { + public async Task SourceCodeTokenizer_SplitsCamelCase() + { await using var ctx = fixture.CreateDbContext(); - var hits = await ctx.SourceCodeRecords - .Where(s => EF.Functions.Matches(s.Snippet, "user")) + var hits = await ctx + .SourceCodeRecords.Where(s => EF.Functions.Matches(s.Snippet, "user")) .Select(s => s.Snippet) .ToListAsync(); @@ -54,11 +59,12 @@ public async Task SourceCodeTokenizer_SplitsCamelCase() { } [Fact] - public async Task RegexTokenizer_TokenizesByPattern() { + public async Task RegexTokenizer_TokenizesByPattern() + { await using var ctx = fixture.CreateDbContext(); - var hits = await ctx.RegexRecords - .Where(r => EF.Functions.Matches(r.Body, "alpha")) + var hits = await ctx + .RegexRecords.Where(r => EF.Functions.Matches(r.Body, "alpha")) .Select(r => r.Body) .ToListAsync(); @@ -66,12 +72,13 @@ public async Task RegexTokenizer_TokenizesByPattern() { } [Fact] - public async Task GermanStemmer_MatchesGermanWordVariants() { + public async Task GermanStemmer_MatchesGermanWordVariants() + { await using var ctx = fixture.CreateDbContext(); // 'Häuser' stems to a German root; 'Haus' should match via the same stem. - var hits = await ctx.GermanArticles - .Where(g => EF.Functions.Matches(g.Content, "Haus")) + var hits = await ctx + .GermanArticles.Where(g => EF.Functions.Matches(g.Content, "Haus")) .Select(g => g.Content) .ToListAsync(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.Tests/Bm25EnumExtensionsTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.Tests/Bm25EnumExtensionsTests.cs index fa04547..8ae70d6 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.Tests/Bm25EnumExtensionsTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.Tests/Bm25EnumExtensionsTests.cs @@ -6,7 +6,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.Tests; /// Tests for — every enum value must map to its pg_search /// string. Unspecified throws because the indexer expects an explicit mapping. /// -public class Bm25EnumExtensionsTests { +public class Bm25EnumExtensionsTests +{ // ── Bm25Tokenizer ───────────────────────────────────────────────── [Theory] @@ -23,17 +24,23 @@ public class Bm25EnumExtensionsTests { [InlineData(Bm25Tokenizer.JapaneseLindera, "japanese_lindera")] [InlineData(Bm25Tokenizer.KoreanLindera, "korean_lindera")] [InlineData(Bm25Tokenizer.Jieba, "jieba")] - public void Tokenizer_maps_to_expected_pg_search_string(Bm25Tokenizer tokenizer, string expected) { + public void Tokenizer_maps_to_expected_pg_search_string( + Bm25Tokenizer tokenizer, + string expected + ) + { Assert.Equal(expected, tokenizer.ToParadeDbString()); } [Fact] - public void Tokenizer_Unspecified_throws() { + public void Tokenizer_Unspecified_throws() + { Assert.Throws(() => Bm25Tokenizer.Unspecified.ToParadeDbString()); } [Fact] - public void Tokenizer_out_of_range_throws() { + public void Tokenizer_out_of_range_throws() + { var bogus = (Bm25Tokenizer)999; Assert.Throws(() => bogus.ToParadeDbString()); } @@ -61,17 +68,20 @@ public void Tokenizer_out_of_range_throws() { [InlineData(Bm25Language.Swedish, "Swedish")] [InlineData(Bm25Language.Tamil, "Tamil")] [InlineData(Bm25Language.Turkish, "Turkish")] - public void Language_maps_to_expected_pg_search_string(Bm25Language language, string expected) { + public void Language_maps_to_expected_pg_search_string(Bm25Language language, string expected) + { Assert.Equal(expected, language.ToParadeDbString()); } [Fact] - public void Language_Unspecified_throws() { + public void Language_Unspecified_throws() + { Assert.Throws(() => Bm25Language.Unspecified.ToParadeDbString()); } [Fact] - public void Language_out_of_range_throws() { + public void Language_out_of_range_throws() + { var bogus = (Bm25Language)999; Assert.Throws(() => bogus.ToParadeDbString()); } @@ -82,17 +92,20 @@ public void Language_out_of_range_throws() { [InlineData(Bm25Record.Basic, "basic")] [InlineData(Bm25Record.Freq, "freq")] [InlineData(Bm25Record.Position, "position")] - public void Record_maps_to_expected_pg_search_string(Bm25Record record, string expected) { + public void Record_maps_to_expected_pg_search_string(Bm25Record record, string expected) + { Assert.Equal(expected, record.ToParadeDbString()); } [Fact] - public void Record_Unspecified_throws() { + public void Record_Unspecified_throws() + { Assert.Throws(() => Bm25Record.Unspecified.ToParadeDbString()); } [Fact] - public void Record_out_of_range_throws() { + public void Record_out_of_range_throws() + { var bogus = (Bm25Record)999; Assert.Throws(() => bogus.ToParadeDbString()); } diff --git a/Equibles.ParadeDB.EntityFrameworkCore.Tests/Bm25IndexConfigurationTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.Tests/Bm25IndexConfigurationTests.cs index 94c8379..adace31 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.Tests/Bm25IndexConfigurationTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.Tests/Bm25IndexConfigurationTests.cs @@ -2,14 +2,18 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.Tests; -public class Bm25IndexConfigurationTests { - private static string GetCreateScript() where TEntity : class { +public class Bm25IndexConfigurationTests +{ + private static string GetCreateScript() + where TEntity : class + { using var ctx = new ConfigTestContext(); return ctx.Database.GenerateCreateScript(); } [Fact] - public void EntityWithoutPerColumnAttributes_EmitsOnlyKeyField() { + public void EntityWithoutPerColumnAttributes_EmitsOnlyKeyField() + { var sql = GetCreateScript(); Assert.Contains("key_field='Id'", sql); Assert.DoesNotContain("text_fields=", sql); @@ -20,93 +24,114 @@ public void EntityWithoutPerColumnAttributes_EmitsOnlyKeyField() { } [Fact] - public void Bm25Text_WithEnglishStemmer_EmitsStemmerInTokenizer() { + public void Bm25Text_WithEnglishStemmer_EmitsStemmerInTokenizer() + { var sql = GetCreateScript(); - Assert.Contains("""text_fields='{"Content":{"tokenizer":{"type":"default","stemmer":"English"}}}'""", sql); + Assert.Contains( + """text_fields='{"Content":{"tokenizer":{"type":"default","stemmer":"English"}}}'""", + sql + ); } [Fact] - public void Bm25Text_WithRawTokenizer_EmitsRawType() { + public void Bm25Text_WithRawTokenizer_EmitsRawType() + { var sql = GetCreateScript(); Assert.Contains("""text_fields='{"Slug":{"tokenizer":{"type":"raw"}}}'""", sql); } [Fact] - public void Bm25Text_WithStopwordsLanguage_EmitsStopwordsKey() { + public void Bm25Text_WithStopwordsLanguage_EmitsStopwordsKey() + { var sql = GetCreateScript(); Assert.Contains("\"stopwords_language\":\"French\"", sql); } [Fact] - public void Bm25Text_WithNgramTokenizer_EmitsMinMaxGramAndPrefixOnly() { + public void Bm25Text_WithNgramTokenizer_EmitsMinMaxGramAndPrefixOnly() + { var sql = GetCreateScript(); - Assert.Contains("""text_fields='{"Content":{"tokenizer":{"type":"ngram","min_gram":2,"max_gram":4,"prefix_only":false}}}'""", sql); + Assert.Contains( + """text_fields='{"Content":{"tokenizer":{"type":"ngram","min_gram":2,"max_gram":4,"prefix_only":false}}}'""", + sql + ); } [Fact] - public void Bm25Text_WithNgramAndPrefixOnly_EmitsPrefixOnly() { + public void Bm25Text_WithNgramAndPrefixOnly_EmitsPrefixOnly() + { var sql = GetCreateScript(); Assert.Contains("\"prefix_only\":true", sql); } [Fact] - public void Bm25Text_WithRegexTokenizer_EmitsPattern() { + public void Bm25Text_WithRegexTokenizer_EmitsPattern() + { var sql = GetCreateScript(); Assert.Contains("\"type\":\"regex\"", sql); Assert.Contains("\"pattern\":", sql); } [Fact] - public void Bm25Text_WithFastTrue_EmitsFastKey() { + public void Bm25Text_WithFastTrue_EmitsFastKey() + { var sql = GetCreateScript(); Assert.Contains("\"fast\":true", sql); } [Fact] - public void Bm25Text_WithRecordPosition_EmitsRecordKey() { + public void Bm25Text_WithRecordPosition_EmitsRecordKey() + { var sql = GetCreateScript(); Assert.Contains("\"record\":\"position\"", sql); } [Fact] - public void Bm25Text_WithIndexedFalse_EmitsIndexedFalse() { + public void Bm25Text_WithIndexedFalse_EmitsIndexedFalse() + { var sql = GetCreateScript(); Assert.Contains("\"indexed\":false", sql); } [Fact] - public void Bm25Text_WithFieldnormsFalse_EmitsFieldnormsFalse() { + public void Bm25Text_WithFieldnormsFalse_EmitsFieldnormsFalse() + { var sql = GetCreateScript(); Assert.Contains("\"fieldnorms\":false", sql); } [Fact] - public void Bm25Numeric_WithFastTrue_EmitsNumericFields() { + public void Bm25Numeric_WithFastTrue_EmitsNumericFields() + { var sql = GetCreateScript(); Assert.Contains("""numeric_fields='{"Rating":{"fast":true}}'""", sql); } [Fact] - public void Bm25Boolean_WithFastTrue_EmitsBooleanFields() { + public void Bm25Boolean_WithFastTrue_EmitsBooleanFields() + { var sql = GetCreateScript(); Assert.Contains("""boolean_fields='{"InStock":{"fast":true}}'""", sql); } [Fact] - public void Bm25DateTime_WithFastTrue_EmitsDatetimeFields() { + public void Bm25DateTime_WithFastTrue_EmitsDatetimeFields() + { var sql = GetCreateScript(); Assert.Contains("""datetime_fields='{"PublishedAt":{"fast":true}}'""", sql); } [Fact] - public void Bm25Json_WithExpandDots_EmitsExpandDots() { + public void Bm25Json_WithExpandDots_EmitsExpandDots() + { var sql = GetCreateScript(); Assert.Contains("json_fields=", sql); Assert.Contains("\"expand_dots\":true", sql); } [Fact] - public void MixedFieldTypes_EmitSeparateStorageParameters() { + public void MixedFieldTypes_EmitSeparateStorageParameters() + { var sql = GetCreateScript(); Assert.Contains("text_fields=", sql); Assert.Contains("numeric_fields=", sql); @@ -114,249 +139,328 @@ public void MixedFieldTypes_EmitSeparateStorageParameters() { } [Fact] - public void NgramParamWithoutNgramTokenizer_Throws() { - var ex = Assert.Throws(() => GetCreateScript()); - Assert.Contains("MinGram/MaxGram/PrefixOnly require Tokenizer = Bm25Tokenizer.Ngram", ex.Message); + public void NgramParamWithoutNgramTokenizer_Throws() + { + var ex = Assert.Throws(() => + GetCreateScript() + ); + Assert.Contains( + "MinGram/MaxGram/PrefixOnly require Tokenizer = Bm25Tokenizer.Ngram", + ex.Message + ); } [Fact] - public void NgramTokenizerWithoutMinMax_Throws() { - var ex = Assert.Throws(() => GetCreateScript()); + public void NgramTokenizerWithoutMinMax_Throws() + { + var ex = Assert.Throws(() => + GetCreateScript() + ); Assert.Contains("Bm25Tokenizer.Ngram requires both MinGram and MaxGram", ex.Message); } [Fact] - public void RegexTokenizerWithoutPattern_Throws() { - var ex = Assert.Throws(() => GetCreateScript()); + public void RegexTokenizerWithoutPattern_Throws() + { + var ex = Assert.Throws(() => + GetCreateScript() + ); Assert.Contains("Bm25Tokenizer.Regex requires a RegexPattern", ex.Message); } [Fact] - public void RegexParamWithoutRegexTokenizer_Throws() { - var ex = Assert.Throws(() => GetCreateScript()); + public void RegexParamWithoutRegexTokenizer_Throws() + { + var ex = Assert.Throws(() => + GetCreateScript() + ); Assert.Contains("RegexPattern requires Tokenizer = Bm25Tokenizer.Regex", ex.Message); } [Fact] - public void Bm25Numeric_WithIndexedFalse_EmitsIndexedFalse() { + public void Bm25Numeric_WithIndexedFalse_EmitsIndexedFalse() + { var sql = GetCreateScript(); Assert.Contains("numeric_fields=", sql); Assert.Contains("\"indexed\":false", sql); } [Fact] - public void Bm25Boolean_WithIndexedFalse_EmitsIndexedFalse() { + public void Bm25Boolean_WithIndexedFalse_EmitsIndexedFalse() + { var sql = GetCreateScript(); Assert.Contains("boolean_fields=", sql); Assert.Contains("\"indexed\":false", sql); } [Fact] - public void Bm25DateTime_WithIndexedFalse_EmitsIndexedFalse() { + public void Bm25DateTime_WithIndexedFalse_EmitsIndexedFalse() + { var sql = GetCreateScript(); Assert.Contains("datetime_fields=", sql); Assert.Contains("\"indexed\":false", sql); } [Fact] - public void OrphanFieldAttributeWithoutBm25Index_Throws() { - var ex = Assert.Throws(() => GetCreateScript()); + public void OrphanFieldAttributeWithoutBm25Index_Throws() + { + var ex = Assert.Throws(() => + GetCreateScript() + ); Assert.Contains("OrphanText", ex.Message); Assert.Contains("no [Bm25Index]", ex.Message); } [Fact] - public void FieldAttributeOnPropertyNotInIndexColumns_Throws() { - var ex = Assert.Throws(() => GetCreateScript()); + public void FieldAttributeOnPropertyNotInIndexColumns_Throws() + { + var ex = Assert.Throws(() => + GetCreateScript() + ); Assert.Contains("Untracked", ex.Message); Assert.Contains("not listed in the [Bm25Index] columns", ex.Message); } } -internal sealed class ConfigTestContext : DbContext where TEntity : class { - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { +internal sealed class ConfigTestContext : DbContext + where TEntity : class +{ + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { optionsBuilder.UseNpgsql("Host=localhost;Database=test", npgsql => npgsql.UseParadeDb()); } - protected override void OnModelCreating(ModelBuilder modelBuilder) { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { modelBuilder.Entity(); } } [Bm25Index(nameof(Id), nameof(Content))] -internal class PlainEntity { +internal class PlainEntity +{ public int Id { get; set; } public string Content { get; set; } = null!; } [Bm25Index(nameof(Id), nameof(Content))] -internal class EnglishStemmerEntity { +internal class EnglishStemmerEntity +{ public int Id { get; set; } + [Bm25Text(Stemmer = Bm25Language.English)] public string Content { get; set; } = null!; } [Bm25Index(nameof(Id), nameof(Slug))] -internal class RawTokenizerEntity { +internal class RawTokenizerEntity +{ public int Id { get; set; } + [Bm25Text(Tokenizer = Bm25Tokenizer.Raw)] public string Slug { get; set; } = null!; } [Bm25Index(nameof(Id), nameof(Content))] -internal class StopwordsEntity { +internal class StopwordsEntity +{ public int Id { get; set; } + [Bm25Text(StopwordsLanguage = Bm25Language.French)] public string Content { get; set; } = null!; } [Bm25Index(nameof(Id), nameof(Content))] -internal class NgramEntity { +internal class NgramEntity +{ public int Id { get; set; } + [Bm25Text(Tokenizer = Bm25Tokenizer.Ngram, MinGram = 2, MaxGram = 4)] public string Content { get; set; } = null!; } [Bm25Index(nameof(Id), nameof(Content))] -internal class NgramPrefixEntity { +internal class NgramPrefixEntity +{ public int Id { get; set; } + [Bm25Text(Tokenizer = Bm25Tokenizer.Ngram, MinGram = 2, MaxGram = 4, PrefixOnly = true)] public string Content { get; set; } = null!; } [Bm25Index(nameof(Id), nameof(Content))] -internal class RegexEntity { +internal class RegexEntity +{ public int Id { get; set; } + [Bm25Text(Tokenizer = Bm25Tokenizer.Regex, RegexPattern = "neuro.*")] public string Content { get; set; } = null!; } [Bm25Index(nameof(Id), nameof(Title))] -internal class FastTextEntity { +internal class FastTextEntity +{ public int Id { get; set; } + [Bm25Text(Fast = true)] public string Title { get; set; } = null!; } [Bm25Index(nameof(Id), nameof(Content))] -internal class RecordPositionEntity { +internal class RecordPositionEntity +{ public int Id { get; set; } + [Bm25Text(Record = Bm25Record.Position)] public string Content { get; set; } = null!; } [Bm25Index(nameof(Id), nameof(Content))] -internal class NotIndexedTextEntity { +internal class NotIndexedTextEntity +{ public int Id { get; set; } + [Bm25Text(Indexed = false)] public string Content { get; set; } = null!; } [Bm25Index(nameof(Id), nameof(Content))] -internal class NoFieldnormsEntity { +internal class NoFieldnormsEntity +{ public int Id { get; set; } + [Bm25Text(Fieldnorms = false)] public string Content { get; set; } = null!; } [Bm25Index(nameof(Id), nameof(Rating))] -internal class NumericEntity { +internal class NumericEntity +{ public int Id { get; set; } + [Bm25Numeric(Fast = true)] public int Rating { get; set; } } [Bm25Index(nameof(Id), nameof(InStock))] -internal class BooleanEntity { +internal class BooleanEntity +{ public int Id { get; set; } + [Bm25Boolean(Fast = true)] public bool InStock { get; set; } } [Bm25Index(nameof(Id), nameof(PublishedAt))] -internal class DateTimeEntity { +internal class DateTimeEntity +{ public int Id { get; set; } + [Bm25DateTime(Fast = true)] public DateTime PublishedAt { get; set; } } [Bm25Index(nameof(Id), nameof(Metadata))] -internal class JsonEntity { +internal class JsonEntity +{ public int Id { get; set; } + [Bm25Json(ExpandDots = true)] public string Metadata { get; set; } = null!; } [Bm25Index(nameof(Id), nameof(Title), nameof(Rating), nameof(InStock))] -internal class MixedEntity { +internal class MixedEntity +{ public int Id { get; set; } + [Bm25Text(Fast = true)] public string Title { get; set; } = null!; + [Bm25Numeric(Fast = true)] public int Rating { get; set; } + [Bm25Boolean(Fast = true)] public bool InStock { get; set; } } [Bm25Index(nameof(Id), nameof(Content))] -internal class InvalidNgramParamEntity { +internal class InvalidNgramParamEntity +{ public int Id { get; set; } + [Bm25Text(MinGram = 2)] public string Content { get; set; } = null!; } [Bm25Index(nameof(Id), nameof(Content))] -internal class NgramMissingMinMaxEntity { +internal class NgramMissingMinMaxEntity +{ public int Id { get; set; } + [Bm25Text(Tokenizer = Bm25Tokenizer.Ngram)] public string Content { get; set; } = null!; } [Bm25Index(nameof(Id), nameof(Content))] -internal class RegexMissingPatternEntity { +internal class RegexMissingPatternEntity +{ public int Id { get; set; } + [Bm25Text(Tokenizer = Bm25Tokenizer.Regex)] public string Content { get; set; } = null!; } [Bm25Index(nameof(Id), nameof(Content))] -internal class RegexParamOnNonRegexTokenizerEntity { +internal class RegexParamOnNonRegexTokenizerEntity +{ public int Id { get; set; } + [Bm25Text(Tokenizer = Bm25Tokenizer.Default, RegexPattern = "neuro.*")] public string Content { get; set; } = null!; } [Bm25Index(nameof(Id), nameof(Rating))] -internal class NotIndexedNumericEntity { +internal class NotIndexedNumericEntity +{ public int Id { get; set; } + [Bm25Numeric(Indexed = false)] public int Rating { get; set; } } [Bm25Index(nameof(Id), nameof(InStock))] -internal class NotIndexedBooleanEntity { +internal class NotIndexedBooleanEntity +{ public int Id { get; set; } + [Bm25Boolean(Indexed = false)] public bool InStock { get; set; } } [Bm25Index(nameof(Id), nameof(PublishedAt))] -internal class NotIndexedDateTimeEntity { +internal class NotIndexedDateTimeEntity +{ public int Id { get; set; } + [Bm25DateTime(Indexed = false)] public DateTime PublishedAt { get; set; } } -internal class OrphanNoIndexEntity { +internal class OrphanNoIndexEntity +{ public int Id { get; set; } + [Bm25Text] public string OrphanText { get; set; } = null!; } [Bm25Index(nameof(Id), nameof(Indexed))] -internal class FieldAttrNotInIndexEntity { +internal class FieldAttrNotInIndexEntity +{ public int Id { get; set; } public string Indexed { get; set; } = null!; + [Bm25Text] public string Untracked { get; set; } = null!; } diff --git a/Equibles.ParadeDB.EntityFrameworkCore.Tests/Equibles.ParadeDB.EntityFrameworkCore.Tests.csproj b/Equibles.ParadeDB.EntityFrameworkCore.Tests/Equibles.ParadeDB.EntityFrameworkCore.Tests.csproj index cd0eea6..ff3569b 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.Tests/Equibles.ParadeDB.EntityFrameworkCore.Tests.csproj +++ b/Equibles.ParadeDB.EntityFrameworkCore.Tests/Equibles.ParadeDB.EntityFrameworkCore.Tests.csproj @@ -1,5 +1,4 @@ - net8.0;net9.0;net10.0 enable @@ -34,5 +33,4 @@ - diff --git a/Equibles.ParadeDB.EntityFrameworkCore.Tests/ExpressionTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.Tests/ExpressionTests.cs index fef4992..95675b0 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.Tests/ExpressionTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.Tests/ExpressionTests.cs @@ -4,60 +4,113 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.Tests; -public class ExpressionTests { +public class ExpressionTests +{ private static SqlExpression Stub(string token) => new StubSqlExpression(token); - private sealed class StubSqlExpression : SqlExpression { + private sealed class StubSqlExpression : SqlExpression + { private readonly string _token; - public StubSqlExpression(string token) : base(typeof(string), new FakeTypeMapping()) { + + public StubSqlExpression(string token) + : base(typeof(string), new FakeTypeMapping()) + { _token = token; } + protected override System.Linq.Expressions.Expression VisitChildren( - System.Linq.Expressions.ExpressionVisitor visitor) => this; - protected override void Print(ExpressionPrinter expressionPrinter) => expressionPrinter.Append(_token); - public override bool Equals(object? obj) => obj is StubSqlExpression other && other._token == _token; + System.Linq.Expressions.ExpressionVisitor visitor + ) => this; + + protected override void Print(ExpressionPrinter expressionPrinter) => + expressionPrinter.Append(_token); + + public override bool Equals(object? obj) => + obj is StubSqlExpression other && other._token == _token; + public override int GetHashCode() => _token.GetHashCode(); + #if NET9_0_OR_GREATER - public override System.Linq.Expressions.Expression Quote() => throw new NotSupportedException(); + public override System.Linq.Expressions.Expression Quote() => + throw new NotSupportedException(); #endif } - private sealed class FakeTypeMapping : RelationalTypeMapping { - public FakeTypeMapping() : base("text", typeof(string)) { } - protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) => this; + private sealed class FakeTypeMapping : RelationalTypeMapping + { + public FakeTypeMapping() + : base("text", typeof(string)) { } + + protected override RelationalTypeMapping Clone( + RelationalTypeMappingParameters parameters + ) => this; } [Fact] - public void ModifiedQueryExpression_Equals_ReturnsTrueForSameInnerAndSuffix() { + public void ModifiedQueryExpression_Equals_ReturnsTrueForSameInnerAndSuffix() + { var inner = Stub("hello"); - var a = new ParadeDbModifiedQueryExpression(inner, "::pdb.fuzzy(2)", typeof(string), inner.TypeMapping); - var b = new ParadeDbModifiedQueryExpression(inner, "::pdb.fuzzy(2)", typeof(string), inner.TypeMapping); + var a = new ParadeDbModifiedQueryExpression( + inner, + "::pdb.fuzzy(2)", + typeof(string), + inner.TypeMapping + ); + var b = new ParadeDbModifiedQueryExpression( + inner, + "::pdb.fuzzy(2)", + typeof(string), + inner.TypeMapping + ); Assert.True(a.Equals(b)); Assert.Equal(a.GetHashCode(), b.GetHashCode()); } [Fact] - public void ModifiedQueryExpression_Equals_ReturnsFalseForDifferentSuffix() { + public void ModifiedQueryExpression_Equals_ReturnsFalseForDifferentSuffix() + { var inner = Stub("hello"); - var a = new ParadeDbModifiedQueryExpression(inner, "::pdb.fuzzy(2)", typeof(string), inner.TypeMapping); - var b = new ParadeDbModifiedQueryExpression(inner, "::pdb.boost(2)", typeof(string), inner.TypeMapping); + var a = new ParadeDbModifiedQueryExpression( + inner, + "::pdb.fuzzy(2)", + typeof(string), + inner.TypeMapping + ); + var b = new ParadeDbModifiedQueryExpression( + inner, + "::pdb.boost(2)", + typeof(string), + inner.TypeMapping + ); Assert.False(a.Equals(b)); } [Fact] - public void ModifiedQueryExpression_Equals_ReturnsFalseForUnrelatedType() { + public void ModifiedQueryExpression_Equals_ReturnsFalseForUnrelatedType() + { var inner = Stub("hello"); - var a = new ParadeDbModifiedQueryExpression(inner, "::pdb.fuzzy(2)", typeof(string), inner.TypeMapping); + var a = new ParadeDbModifiedQueryExpression( + inner, + "::pdb.fuzzy(2)", + typeof(string), + inner.TypeMapping + ); Assert.False(a.Equals("not an expression")); } [Fact] - public void ModifiedQueryExpression_Print_EmitsInnerThenSuffix() { + public void ModifiedQueryExpression_Print_EmitsInnerThenSuffix() + { var inner = Stub("hello"); - var expr = new ParadeDbModifiedQueryExpression(inner, "::pdb.fuzzy(2)", typeof(string), inner.TypeMapping); + var expr = new ParadeDbModifiedQueryExpression( + inner, + "::pdb.fuzzy(2)", + typeof(string), + inner.TypeMapping + ); var printer = new ExpressionPrinter(); printer.Visit(expr); @@ -66,46 +119,87 @@ public void ModifiedQueryExpression_Print_EmitsInnerThenSuffix() { } [Fact] - public void NamedArgFunctionExpression_Equals_ReturnsTrueForSameStructure() { + public void NamedArgFunctionExpression_Equals_ReturnsTrueForSameStructure() + { var arg = Stub("c"); var named = Stub(""); - var a = new ParadeDbNamedArgFunctionExpression("pdb.snippet", - [arg], [("start_tag", named)], typeof(string), arg.TypeMapping); - var b = new ParadeDbNamedArgFunctionExpression("pdb.snippet", - [arg], [("start_tag", named)], typeof(string), arg.TypeMapping); + var a = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [arg], + [("start_tag", named)], + typeof(string), + arg.TypeMapping + ); + var b = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [arg], + [("start_tag", named)], + typeof(string), + arg.TypeMapping + ); Assert.True(a.Equals(b)); Assert.Equal(a.GetHashCode(), b.GetHashCode()); } [Fact] - public void NamedArgFunctionExpression_Equals_ReturnsFalseForDifferentFunctionName() { + public void NamedArgFunctionExpression_Equals_ReturnsFalseForDifferentFunctionName() + { var arg = Stub("c"); - var a = new ParadeDbNamedArgFunctionExpression("pdb.snippet", [arg], [], typeof(string), arg.TypeMapping); - var b = new ParadeDbNamedArgFunctionExpression("pdb.snippets", [arg], [], typeof(string), arg.TypeMapping); + var a = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [arg], + [], + typeof(string), + arg.TypeMapping + ); + var b = new ParadeDbNamedArgFunctionExpression( + "pdb.snippets", + [arg], + [], + typeof(string), + arg.TypeMapping + ); Assert.False(a.Equals(b)); } [Fact] - public void NamedArgFunctionExpression_Equals_ReturnsFalseForDifferentNamedArgName() { + public void NamedArgFunctionExpression_Equals_ReturnsFalseForDifferentNamedArgName() + { var arg = Stub("c"); var v1 = Stub(""); - var a = new ParadeDbNamedArgFunctionExpression("pdb.snippet", - [arg], [("start_tag", v1)], typeof(string), arg.TypeMapping); - var b = new ParadeDbNamedArgFunctionExpression("pdb.snippet", - [arg], [("end_tag", v1)], typeof(string), arg.TypeMapping); + var a = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [arg], + [("start_tag", v1)], + typeof(string), + arg.TypeMapping + ); + var b = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [arg], + [("end_tag", v1)], + typeof(string), + arg.TypeMapping + ); Assert.False(a.Equals(b)); } [Fact] - public void NamedArgFunctionExpression_Print_EmitsPositionalAndNamedArgs() { + public void NamedArgFunctionExpression_Print_EmitsPositionalAndNamedArgs() + { var positional = Stub("c"); var named = Stub(""); - var expr = new ParadeDbNamedArgFunctionExpression("pdb.snippet", - [positional], [("start_tag", named)], typeof(string), positional.TypeMapping); + var expr = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [positional], + [("start_tag", named)], + typeof(string), + positional.TypeMapping + ); var printer = new ExpressionPrinter(); printer.Visit(expr); @@ -118,17 +212,30 @@ public void NamedArgFunctionExpression_Print_EmitsPositionalAndNamedArgs() { #if NET9_0_OR_GREATER [Fact] - public void ModifiedQueryExpression_Quote_Throws() { + public void ModifiedQueryExpression_Quote_Throws() + { var inner = Stub("hello"); - var expr = new ParadeDbModifiedQueryExpression(inner, "::pdb.fuzzy(2)", typeof(string), inner.TypeMapping); + var expr = new ParadeDbModifiedQueryExpression( + inner, + "::pdb.fuzzy(2)", + typeof(string), + inner.TypeMapping + ); Assert.Throws(() => expr.Quote()); } [Fact] - public void NamedArgFunctionExpression_Quote_Throws() { + public void NamedArgFunctionExpression_Quote_Throws() + { var arg = Stub("c"); - var expr = new ParadeDbNamedArgFunctionExpression("pdb.snippet", [arg], [], typeof(string), arg.TypeMapping); + var expr = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [arg], + [], + typeof(string), + arg.TypeMapping + ); Assert.Throws(() => expr.Quote()); } @@ -141,29 +248,43 @@ public void NamedArgFunctionExpression_Quote_Throws() { /// expression-tree visitation. Lets us assert that VisitChildren returns a NEW /// expression when a child changes (the "something changed" branch). /// - private sealed class ReplaceVisitor : System.Linq.Expressions.ExpressionVisitor { + private sealed class ReplaceVisitor : System.Linq.Expressions.ExpressionVisitor + { public System.Linq.Expressions.Expression Target { get; } public System.Linq.Expressions.Expression Replacement { get; } - public ReplaceVisitor(System.Linq.Expressions.Expression target, - System.Linq.Expressions.Expression replacement) { + public ReplaceVisitor( + System.Linq.Expressions.Expression target, + System.Linq.Expressions.Expression replacement + ) + { Target = target; Replacement = replacement; } - public override System.Linq.Expressions.Expression Visit(System.Linq.Expressions.Expression? node) { - if (node is not null && ReferenceEquals(node, Target)) return Replacement; + public override System.Linq.Expressions.Expression Visit( + System.Linq.Expressions.Expression? node + ) + { + if (node is not null && ReferenceEquals(node, Target)) + return Replacement; return base.Visit(node)!; } } [Fact] - public void NamedArgFunctionExpression_VisitChildren_returns_new_when_positional_changes() { + public void NamedArgFunctionExpression_VisitChildren_returns_new_when_positional_changes() + { var original = Stub("c"); var replacement = Stub("c2"); var named = Stub(""); - var expr = new ParadeDbNamedArgFunctionExpression("pdb.snippet", - [original], [("start_tag", named)], typeof(string), original.TypeMapping); + var expr = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [original], + [("start_tag", named)], + typeof(string), + original.TypeMapping + ); var visitor = new ReplaceVisitor(original, replacement); var result = visitor.Visit(expr); @@ -175,12 +296,18 @@ public void NamedArgFunctionExpression_VisitChildren_returns_new_when_positional } [Fact] - public void NamedArgFunctionExpression_VisitChildren_returns_new_when_named_changes() { + public void NamedArgFunctionExpression_VisitChildren_returns_new_when_named_changes() + { var positional = Stub("c"); var original = Stub(""); var replacement = Stub(""); - var expr = new ParadeDbNamedArgFunctionExpression("pdb.snippet", - [positional], [("start_tag", original)], typeof(string), positional.TypeMapping); + var expr = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [positional], + [("start_tag", original)], + typeof(string), + positional.TypeMapping + ); var visitor = new ReplaceVisitor(original, replacement); var result = visitor.Visit(expr); @@ -192,11 +319,17 @@ public void NamedArgFunctionExpression_VisitChildren_returns_new_when_named_chan } [Fact] - public void NamedArgFunctionExpression_VisitChildren_returns_same_when_no_changes() { + public void NamedArgFunctionExpression_VisitChildren_returns_same_when_no_changes() + { var arg = Stub("c"); var named = Stub(""); - var expr = new ParadeDbNamedArgFunctionExpression("pdb.snippet", - [arg], [("start_tag", named)], typeof(string), arg.TypeMapping); + var expr = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [arg], + [("start_tag", named)], + typeof(string), + arg.TypeMapping + ); var visitor = new ReplaceVisitor(Stub("nothing-matches-this"), Stub("unused")); var result = visitor.Visit(expr); @@ -205,9 +338,15 @@ public void NamedArgFunctionExpression_VisitChildren_returns_same_when_no_change } [Fact] - public void ModifiedQueryExpression_Print_DelegatesToInner() { + public void ModifiedQueryExpression_Print_DelegatesToInner() + { var inner = Stub("hello"); - var expr = new ParadeDbModifiedQueryExpression(inner, "::pdb.fuzzy(2)", typeof(string), inner.TypeMapping); + var expr = new ParadeDbModifiedQueryExpression( + inner, + "::pdb.fuzzy(2)", + typeof(string), + inner.TypeMapping + ); var printer = new ExpressionPrinter(); printer.Visit(expr); @@ -218,10 +357,16 @@ public void ModifiedQueryExpression_Print_DelegatesToInner() { } [Fact] - public void ModifiedQueryExpression_VisitChildren_returns_new_when_inner_changes() { + public void ModifiedQueryExpression_VisitChildren_returns_new_when_inner_changes() + { var original = Stub("hello"); var replacement = Stub("world"); - var expr = new ParadeDbModifiedQueryExpression(original, "::pdb.boost(2)", typeof(string), original.TypeMapping); + var expr = new ParadeDbModifiedQueryExpression( + original, + "::pdb.boost(2)", + typeof(string), + original.TypeMapping + ); var visitor = new ReplaceVisitor(original, replacement); var result = visitor.Visit(expr); @@ -233,9 +378,15 @@ public void ModifiedQueryExpression_VisitChildren_returns_new_when_inner_changes } [Fact] - public void ModifiedQueryExpression_VisitChildren_returns_same_when_inner_unchanged() { + public void ModifiedQueryExpression_VisitChildren_returns_same_when_inner_unchanged() + { var inner = Stub("hello"); - var expr = new ParadeDbModifiedQueryExpression(inner, "::pdb.boost(2)", typeof(string), inner.TypeMapping); + var expr = new ParadeDbModifiedQueryExpression( + inner, + "::pdb.boost(2)", + typeof(string), + inner.TypeMapping + ); var visitor = new ReplaceVisitor(Stub("nothing-matches-this"), Stub("unused")); var result = visitor.Visit(expr); @@ -246,64 +397,125 @@ public void ModifiedQueryExpression_VisitChildren_returns_same_when_inner_unchan // ── NamedArgFunctionExpression.Equals — remaining branch arms ────── [Fact] - public void NamedArgFunctionExpression_Equals_ReturnsFalseForUnrelatedType() { + public void NamedArgFunctionExpression_Equals_ReturnsFalseForUnrelatedType() + { var arg = Stub("c"); - var a = new ParadeDbNamedArgFunctionExpression("pdb.snippet", [arg], [], typeof(string), arg.TypeMapping); + var a = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [arg], + [], + typeof(string), + arg.TypeMapping + ); Assert.False(a.Equals("not an expression")); } [Fact] - public void NamedArgFunctionExpression_Equals_ReturnsFalseForDifferentPositionalCount() { + public void NamedArgFunctionExpression_Equals_ReturnsFalseForDifferentPositionalCount() + { var arg1 = Stub("c"); var arg2 = Stub("d"); - var a = new ParadeDbNamedArgFunctionExpression("pdb.snippet", [arg1], [], typeof(string), arg1.TypeMapping); - var b = new ParadeDbNamedArgFunctionExpression("pdb.snippet", [arg1, arg2], [], typeof(string), arg1.TypeMapping); + var a = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [arg1], + [], + typeof(string), + arg1.TypeMapping + ); + var b = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [arg1, arg2], + [], + typeof(string), + arg1.TypeMapping + ); Assert.False(a.Equals(b)); } [Fact] - public void NamedArgFunctionExpression_Equals_ReturnsFalseForDifferentNamedCount() { + public void NamedArgFunctionExpression_Equals_ReturnsFalseForDifferentNamedCount() + { var arg = Stub("c"); var v = Stub(""); - var a = new ParadeDbNamedArgFunctionExpression("pdb.snippet", - [arg], [("start_tag", v)], typeof(string), arg.TypeMapping); - var b = new ParadeDbNamedArgFunctionExpression("pdb.snippet", - [arg], [("start_tag", v), ("end_tag", v)], typeof(string), arg.TypeMapping); + var a = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [arg], + [("start_tag", v)], + typeof(string), + arg.TypeMapping + ); + var b = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [arg], + [("start_tag", v), ("end_tag", v)], + typeof(string), + arg.TypeMapping + ); Assert.False(a.Equals(b)); } [Fact] - public void NamedArgFunctionExpression_Equals_ReturnsFalseForDifferentPositionalValue() { + public void NamedArgFunctionExpression_Equals_ReturnsFalseForDifferentPositionalValue() + { var arg1 = Stub("c"); var arg2 = Stub("d"); - var a = new ParadeDbNamedArgFunctionExpression("pdb.snippet", [arg1], [], typeof(string), arg1.TypeMapping); - var b = new ParadeDbNamedArgFunctionExpression("pdb.snippet", [arg2], [], typeof(string), arg1.TypeMapping); + var a = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [arg1], + [], + typeof(string), + arg1.TypeMapping + ); + var b = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [arg2], + [], + typeof(string), + arg1.TypeMapping + ); Assert.False(a.Equals(b)); } [Fact] - public void NamedArgFunctionExpression_Equals_ReturnsFalseForDifferentNamedValue() { + public void NamedArgFunctionExpression_Equals_ReturnsFalseForDifferentNamedValue() + { var arg = Stub("c"); var v1 = Stub(""); var v2 = Stub(""); - var a = new ParadeDbNamedArgFunctionExpression("pdb.snippet", - [arg], [("start_tag", v1)], typeof(string), arg.TypeMapping); - var b = new ParadeDbNamedArgFunctionExpression("pdb.snippet", - [arg], [("start_tag", v2)], typeof(string), arg.TypeMapping); + var a = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [arg], + [("start_tag", v1)], + typeof(string), + arg.TypeMapping + ); + var b = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [arg], + [("start_tag", v2)], + typeof(string), + arg.TypeMapping + ); Assert.False(a.Equals(b)); } [Fact] - public void NamedArgFunctionExpression_Print_emits_separator_between_multiple_positional_args() { + public void NamedArgFunctionExpression_Print_emits_separator_between_multiple_positional_args() + { var p1 = Stub("c"); var p2 = Stub("d"); - var expr = new ParadeDbNamedArgFunctionExpression("pdb.fn", - [p1, p2], [], typeof(string), p1.TypeMapping); + var expr = new ParadeDbNamedArgFunctionExpression( + "pdb.fn", + [p1, p2], + [], + typeof(string), + p1.TypeMapping + ); var printer = new ExpressionPrinter(); printer.Visit(expr); @@ -314,11 +526,17 @@ public void NamedArgFunctionExpression_Print_emits_separator_between_multiple_po } [Fact] - public void NamedArgFunctionExpression_Print_handles_named_args_without_positional() { + public void NamedArgFunctionExpression_Print_handles_named_args_without_positional() + { var v1 = Stub(""); var v2 = Stub(""); - var expr = new ParadeDbNamedArgFunctionExpression("pdb.fn", - [], [("start_tag", v1), ("end_tag", v2)], typeof(string), v1.TypeMapping); + var expr = new ParadeDbNamedArgFunctionExpression( + "pdb.fn", + [], + [("start_tag", v1), ("end_tag", v2)], + typeof(string), + v1.TypeMapping + ); var printer = new ExpressionPrinter(); printer.Visit(expr); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.Tests/InternalsCoverageTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.Tests/InternalsCoverageTests.cs index da2e551..00b22ba 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.Tests/InternalsCoverageTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.Tests/InternalsCoverageTests.cs @@ -15,8 +15,10 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.Tests; /// - rebuild path /// (children change during processing). /// -public class InternalsCoverageTests { - private static IServiceProvider Services() { +public class InternalsCoverageTests +{ + private static IServiceProvider Services() + { var ctx = new TestDbContext(); return ((IInfrastructure)ctx).Instance; } @@ -24,11 +26,15 @@ private static IServiceProvider Services() { // ── Translator fall-through ─────────────────────────────────────── [Fact] - public void Translate_returns_null_for_unknown_method() { + public void Translate_returns_null_for_unknown_method() + { var services = Services(); var plugins = services.GetService>()!; - var translator = plugins.OfType().Single() - .Translators.OfType().Single(); + var translator = plugins + .OfType() + .Single() + .Translators.OfType() + .Single(); var unknown = typeof(string).GetMethod(nameof(string.StartsWith), [typeof(string)])!; var sql = services.GetService()!; @@ -45,12 +51,15 @@ public void Translate_returns_null_for_unknown_method() { // ── Nullability processor: child-pass-through path ──────────────── - private static ParadeDbSqlNullabilityProcessor CreateProcessor() { + private static ParadeDbSqlNullabilityProcessor CreateProcessor() + { var services = Services(); var factory = (ParadeDbParameterBasedSqlProcessorFactory) services.GetService()!; - var depsField = typeof(ParadeDbParameterBasedSqlProcessorFactory) - .GetField("_dependencies", BindingFlags.NonPublic | BindingFlags.Instance)!; + var depsField = typeof(ParadeDbParameterBasedSqlProcessorFactory).GetField( + "_dependencies", + BindingFlags.NonPublic | BindingFlags.Instance + )!; var deps = (RelationalParameterBasedSqlProcessorDependencies)depsField.GetValue(factory)!; #if NET8_0 @@ -58,31 +67,46 @@ private static ParadeDbSqlNullabilityProcessor CreateProcessor() { #elif NET9_0 // EF Core 9 ctor: (bool useRelationalNulls, IReadOnlySet parametersToConstantize) var parameters = new RelationalParameterBasedSqlProcessorParameters( - false, new HashSet()); + false, + new HashSet() + ); return new ParadeDbSqlNullabilityProcessor(deps, parameters); #else // EF Core 10 ctor: (bool useRelationalNulls, ParameterTranslationMode) var parameters = new RelationalParameterBasedSqlProcessorParameters( - false, ParameterTranslationMode.Constant); + false, + ParameterTranslationMode.Constant + ); return new ParadeDbSqlNullabilityProcessor(deps, parameters); #endif } - private static SqlExpression InvokeVisitCustom(ParadeDbSqlNullabilityProcessor processor, - SqlExpression expr, bool allowOptimizedExpansion = false) { + private static SqlExpression InvokeVisitCustom( + ParadeDbSqlNullabilityProcessor processor, + SqlExpression expr, + bool allowOptimizedExpansion = false + ) + { var method = typeof(ParadeDbSqlNullabilityProcessor).GetMethod( "VisitCustomSqlExpression", - BindingFlags.NonPublic | BindingFlags.Instance)!; + BindingFlags.NonPublic | BindingFlags.Instance + )!; var args = new object?[] { expr, allowOptimizedExpansion, null }; return (SqlExpression)method.Invoke(processor, args)!; } [Fact] - public void NullabilityProcessor_ModifiedQueryExpression_keeps_instance_when_inner_unchanged() { + public void NullabilityProcessor_ModifiedQueryExpression_keeps_instance_when_inner_unchanged() + { var processor = CreateProcessor(); var sql = Services().GetService()!; var inner = sql.Constant("hello"); - var expr = new ParadeDbModifiedQueryExpression(inner, "::pdb.boost(2)", inner.Type, inner.TypeMapping); + var expr = new ParadeDbModifiedQueryExpression( + inner, + "::pdb.boost(2)", + inner.Type, + inner.TypeMapping + ); var result = InvokeVisitCustom(processor, expr); @@ -91,13 +115,19 @@ public void NullabilityProcessor_ModifiedQueryExpression_keeps_instance_when_inn } [Fact] - public void NullabilityProcessor_NamedArgFunctionExpression_keeps_instance_when_unchanged() { + public void NullabilityProcessor_NamedArgFunctionExpression_keeps_instance_when_unchanged() + { var processor = CreateProcessor(); var sql = Services().GetService()!; var positional = sql.Constant("col"); var namedValue = sql.Constant(""); - var expr = new ParadeDbNamedArgFunctionExpression("pdb.snippet", - [positional], [("start_tag", namedValue)], typeof(string), positional.TypeMapping); + var expr = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [positional], + [("start_tag", namedValue)], + typeof(string), + positional.TypeMapping + ); var result = InvokeVisitCustom(processor, expr); @@ -110,13 +140,19 @@ public void NullabilityProcessor_NamedArgFunctionExpression_keeps_instance_when_ /// "positional changed → rebuild" branch on . /// [Fact] - public void NullabilityProcessor_NamedArgFunctionExpression_rebuilds_when_positional_simplified() { + public void NullabilityProcessor_NamedArgFunctionExpression_rebuilds_when_positional_simplified() + { var processor = CreateProcessor(); var sql = Services().GetService()!; var positional = sql.IsNotNull(sql.Constant("hi")); var namedValue = sql.Constant(""); - var expr = new ParadeDbNamedArgFunctionExpression("pdb.snippet", - [positional], [("start_tag", namedValue)], typeof(string), namedValue.TypeMapping); + var expr = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [positional], + [("start_tag", namedValue)], + typeof(string), + namedValue.TypeMapping + ); var result = InvokeVisitCustom(processor, expr); @@ -126,13 +162,19 @@ public void NullabilityProcessor_NamedArgFunctionExpression_rebuilds_when_positi } [Fact] - public void NullabilityProcessor_NamedArgFunctionExpression_rebuilds_when_named_simplified() { + public void NullabilityProcessor_NamedArgFunctionExpression_rebuilds_when_named_simplified() + { var processor = CreateProcessor(); var sql = Services().GetService()!; var positional = sql.Constant("col"); var namedValue = sql.IsNotNull(sql.Constant("hi")); - var expr = new ParadeDbNamedArgFunctionExpression("pdb.snippet", - [positional], [("flag", namedValue)], typeof(string), positional.TypeMapping); + var expr = new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", + [positional], + [("flag", namedValue)], + typeof(string), + positional.TypeMapping + ); var result = InvokeVisitCustom(processor, expr); @@ -146,11 +188,17 @@ public void NullabilityProcessor_NamedArgFunctionExpression_rebuilds_when_named_ /// around the new instance — otherwise downstream visitors keep seeing the stale child. /// [Fact] - public void NullabilityProcessor_ModifiedQueryExpression_rebuilds_when_inner_simplified() { + public void NullabilityProcessor_ModifiedQueryExpression_rebuilds_when_inner_simplified() + { var processor = CreateProcessor(); var sql = Services().GetService()!; var inner = sql.IsNotNull(sql.Constant("hi")); - var expr = new ParadeDbModifiedQueryExpression(inner, "::pdb.boost(2)", inner.Type, inner.TypeMapping); + var expr = new ParadeDbModifiedQueryExpression( + inner, + "::pdb.boost(2)", + inner.Type, + inner.TypeMapping + ); var result = InvokeVisitCustom(processor, expr); @@ -159,4 +207,3 @@ public void NullabilityProcessor_ModifiedQueryExpression_rebuilds_when_inner_sim Assert.Equal("::pdb.boost(2)", rebuilt.ModifierSuffix); } } - diff --git a/Equibles.ParadeDB.EntityFrameworkCore.Tests/ParadeDbDbContextOptionsExtensionTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.Tests/ParadeDbDbContextOptionsExtensionTests.cs index 122e8f4..fdfd215 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.Tests/ParadeDbDbContextOptionsExtensionTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.Tests/ParadeDbDbContextOptionsExtensionTests.cs @@ -8,8 +8,10 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.Tests; /// invoked by EF Core when describing the active provider/plugins; the LINQ tests don't /// touch all of them so we exercise them explicitly here. /// -public class ParadeDbDbContextOptionsExtensionTests { - private static DbContextOptionsExtensionInfo Info() { +public class ParadeDbDbContextOptionsExtensionTests +{ + private static DbContextOptionsExtensionInfo Info() + { using var ctx = new TestDbContext(); var ext = ctx.GetService() .FindExtension()!; @@ -17,29 +19,34 @@ private static DbContextOptionsExtensionInfo Info() { } [Fact] - public void IsDatabaseProvider_is_false() { + public void IsDatabaseProvider_is_false() + { Assert.False(Info().IsDatabaseProvider); } [Fact] - public void LogFragment_mentions_paradedb() { + public void LogFragment_mentions_paradedb() + { Assert.Contains("ParadeDB", Info().LogFragment, StringComparison.OrdinalIgnoreCase); } [Fact] - public void GetServiceProviderHashCode_is_stable() { + public void GetServiceProviderHashCode_is_stable() + { var info = Info(); Assert.Equal(info.GetServiceProviderHashCode(), info.GetServiceProviderHashCode()); } [Fact] - public void ShouldUseSameServiceProvider_is_true_for_same_extension_info_type() { + public void ShouldUseSameServiceProvider_is_true_for_same_extension_info_type() + { var info = Info(); Assert.True(info.ShouldUseSameServiceProvider(Info())); } [Fact] - public void PopulateDebugInfo_writes_paradedb_marker() { + public void PopulateDebugInfo_writes_paradedb_marker() + { var debug = new Dictionary(); Info().PopulateDebugInfo(debug); Assert.Equal("1", debug["ParadeDB:BM25"]); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.Tests/ParadeDbFunctionsTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.Tests/ParadeDbFunctionsTests.cs index cc4f6c8..8e7aeaf 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.Tests/ParadeDbFunctionsTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.Tests/ParadeDbFunctionsTests.cs @@ -8,146 +8,183 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.Tests; /// that the translator never touches (LINQ provider replaces the call before /// the body runs). /// -public class ParadeDbFunctionsTests { +public class ParadeDbFunctionsTests +{ private readonly DbFunctions _ef = EF.Functions; [Fact] - public void Matches_throws_when_called_directly() { + public void Matches_throws_when_called_directly() + { Assert.Throws(() => _ef.Matches("col", "q")); } [Fact] - public void MatchesAll_throws_when_called_directly() { + public void MatchesAll_throws_when_called_directly() + { Assert.Throws(() => _ef.MatchesAll("col", "q")); } [Fact] - public void MatchesPhrase_throws_when_called_directly() { + public void MatchesPhrase_throws_when_called_directly() + { Assert.Throws(() => _ef.MatchesPhrase("col", "q")); } [Fact] - public void MatchesPhrase_with_slop_throws_when_called_directly() { + public void MatchesPhrase_with_slop_throws_when_called_directly() + { Assert.Throws(() => _ef.MatchesPhrase("col", "q", 2)); } [Fact] - public void MatchesTerm_throws_when_called_directly() { + public void MatchesTerm_throws_when_called_directly() + { Assert.Throws(() => _ef.MatchesTerm("col", "q")); } [Fact] - public void MatchesTermSet_throws_when_called_directly() { + public void MatchesTermSet_throws_when_called_directly() + { Assert.Throws(() => _ef.MatchesTermSet("col", "a", "b")); } [Fact] - public void MatchesFuzzy_throws_when_called_directly() { + public void MatchesFuzzy_throws_when_called_directly() + { Assert.Throws(() => _ef.MatchesFuzzy("col", "q", 2)); } [Fact] - public void MatchesFuzzy_full_throws_when_called_directly() { - Assert.Throws(() => _ef.MatchesFuzzy("col", "q", 2, true, false)); + public void MatchesFuzzy_full_throws_when_called_directly() + { + Assert.Throws(() => + _ef.MatchesFuzzy("col", "q", 2, true, false) + ); } [Fact] - public void MatchesAllFuzzy_throws_when_called_directly() { + public void MatchesAllFuzzy_throws_when_called_directly() + { Assert.Throws(() => _ef.MatchesAllFuzzy("col", "q", 2)); } [Fact] - public void MatchesAllFuzzy_full_throws_when_called_directly() { - Assert.Throws(() => _ef.MatchesAllFuzzy("col", "q", 1, false, true)); + public void MatchesAllFuzzy_full_throws_when_called_directly() + { + Assert.Throws(() => + _ef.MatchesAllFuzzy("col", "q", 1, false, true) + ); } [Fact] - public void MatchesTermFuzzy_throws_when_called_directly() { + public void MatchesTermFuzzy_throws_when_called_directly() + { Assert.Throws(() => _ef.MatchesTermFuzzy("col", "q", 1)); } [Fact] - public void MatchesTermFuzzy_full_throws_when_called_directly() { - Assert.Throws(() => _ef.MatchesTermFuzzy("col", "q", 2, true, true)); + public void MatchesTermFuzzy_full_throws_when_called_directly() + { + Assert.Throws(() => + _ef.MatchesTermFuzzy("col", "q", 2, true, true) + ); } [Fact] - public void MatchesBoosted_throws_when_called_directly() { + public void MatchesBoosted_throws_when_called_directly() + { Assert.Throws(() => _ef.MatchesBoosted("col", "q", 2.0)); } [Fact] - public void MatchesAllBoosted_throws_when_called_directly() { + public void MatchesAllBoosted_throws_when_called_directly() + { Assert.Throws(() => _ef.MatchesAllBoosted("col", "q", 1.5)); } [Fact] - public void MatchesFuzzyBoosted_throws_when_called_directly() { + public void MatchesFuzzyBoosted_throws_when_called_directly() + { Assert.Throws(() => _ef.MatchesFuzzyBoosted("col", "q", 2, 2.0)); } [Fact] - public void MatchesAllFuzzyBoosted_throws_when_called_directly() { - Assert.Throws(() => _ef.MatchesAllFuzzyBoosted("col", "q", 1, 3.0)); + public void MatchesAllFuzzyBoosted_throws_when_called_directly() + { + Assert.Throws(() => + _ef.MatchesAllFuzzyBoosted("col", "q", 1, 3.0) + ); } [Fact] - public void Score_throws_when_called_directly() { + public void Score_throws_when_called_directly() + { Assert.Throws(() => _ef.Score(new object())); } [Fact] - public void Snippet_throws_when_called_directly() { + public void Snippet_throws_when_called_directly() + { Assert.Throws(() => _ef.Snippet("col")); } [Fact] - public void Snippet_params_throws_when_called_directly() { + public void Snippet_params_throws_when_called_directly() + { Assert.Throws(() => _ef.Snippet("col", "", "", 100)); } [Fact] - public void Snippets_throws_when_called_directly() { + public void Snippets_throws_when_called_directly() + { Assert.Throws(() => _ef.Snippets("col", 15, 5, 0)); } [Fact] - public void Parse_throws_when_called_directly() { + public void Parse_throws_when_called_directly() + { Assert.Throws(() => _ef.Parse(new object(), "q")); } [Fact] - public void Parse_with_options_throws_when_called_directly() { + public void Parse_with_options_throws_when_called_directly() + { Assert.Throws(() => _ef.Parse(new object(), "q", true, true)); } [Fact] - public void Regex_throws_when_called_directly() { + public void Regex_throws_when_called_directly() + { Assert.Throws(() => _ef.Regex("col", "neuro.*")); } [Fact] - public void PhrasePrefix_throws_when_called_directly() { + public void PhrasePrefix_throws_when_called_directly() + { Assert.Throws(() => _ef.PhrasePrefix("col", "a", "b")); } [Fact] - public void PhrasePrefix_max_expansions_throws_when_called_directly() { + public void PhrasePrefix_max_expansions_throws_when_called_directly() + { Assert.Throws(() => _ef.PhrasePrefix("col", 10, "a", "b")); } [Fact] - public void MoreLikeThis_throws_when_called_directly() { + public void MoreLikeThis_throws_when_called_directly() + { Assert.Throws(() => _ef.MoreLikeThis(new object(), 1)); } [Fact] - public void MoreLikeThis_with_fields_throws_when_called_directly() { + public void MoreLikeThis_with_fields_throws_when_called_directly() + { Assert.Throws(() => _ef.MoreLikeThis(new object(), 1, "f")); } [Fact] - public void JsonSearch_throws_when_called_directly() { + public void JsonSearch_throws_when_called_directly() + { Assert.Throws(() => _ef.JsonSearch(new object(), "{}")); } } diff --git a/Equibles.ParadeDB.EntityFrameworkCore.Tests/ParadeDbJsonQueryTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.Tests/ParadeDbJsonQueryTests.cs index 58f2735..691fb67 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.Tests/ParadeDbJsonQueryTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.Tests/ParadeDbJsonQueryTests.cs @@ -5,20 +5,26 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.Tests; /// /// Pure CLR tests validating JSON output from factory methods. /// -public class ParadeDbJsonQueryTests { +public class ParadeDbJsonQueryTests +{ private static JsonElement Parse(string json) => JsonDocument.Parse(json).RootElement; // ── Parse ──────────────────────────────────────────────────────── [Fact] - public void Parse_creates_correct_json() { + public void Parse_creates_correct_json() + { var json = ParadeDbJsonQuery.Parse("revenue growth").ToJson(); var doc = Parse(json); - Assert.Equal("revenue growth", doc.GetProperty("parse").GetProperty("query_string").GetString()); + Assert.Equal( + "revenue growth", + doc.GetProperty("parse").GetProperty("query_string").GetString() + ); } [Fact] - public void Parse_with_options_creates_correct_json() { + public void Parse_with_options_creates_correct_json() + { var json = ParadeDbJsonQuery.Parse("revenue growth", true, true).ToJson(); var doc = Parse(json); var parse = doc.GetProperty("parse"); @@ -30,7 +36,8 @@ public void Parse_with_options_creates_correct_json() { // ── Term ───────────────────────────────────────────────────────── [Fact] - public void Term_with_string_value_creates_correct_json() { + public void Term_with_string_value_creates_correct_json() + { var json = ParadeDbJsonQuery.Term("DocumentId", "abc-123").ToJson(); var doc = Parse(json); var term = doc.GetProperty("term"); @@ -39,7 +46,8 @@ public void Term_with_string_value_creates_correct_json() { } [Fact] - public void Term_with_int_value_creates_correct_json() { + public void Term_with_int_value_creates_correct_json() + { var json = ParadeDbJsonQuery.Term("DocumentType", 10).ToJson(); var doc = Parse(json); var term = doc.GetProperty("term"); @@ -48,7 +56,8 @@ public void Term_with_int_value_creates_correct_json() { } [Fact] - public void Term_with_guid_value_creates_correct_json() { + public void Term_with_guid_value_creates_correct_json() + { var guid = Guid.Parse("1d56ce60-1234-5678-9abc-def012345678"); var json = ParadeDbJsonQuery.Term("DocumentId", guid).ToJson(); var doc = Parse(json); @@ -58,7 +67,8 @@ public void Term_with_guid_value_creates_correct_json() { // ── Term Set ───────────────────────────────────────────────────── [Fact] - public void TermSet_creates_correct_json() { + public void TermSet_creates_correct_json() + { var json = ParadeDbJsonQuery.TermSet("Tags", "gpu", "tpu").ToJson(); var doc = Parse(json); var termSet = doc.GetProperty("term_set"); @@ -72,7 +82,8 @@ public void TermSet_creates_correct_json() { // ── Match ──────────────────────────────────────────────────────── [Fact] - public void Match_with_field_and_options_creates_correct_json() { + public void Match_with_field_and_options_creates_correct_json() + { var json = ParadeDbJsonQuery.Match("shoes", "Content", 2, true).ToJson(); var doc = Parse(json); var match = doc.GetProperty("match"); @@ -85,7 +96,8 @@ public void Match_with_field_and_options_creates_correct_json() { // ── Fuzzy Term ─────────────────────────────────────────────────── [Fact] - public void FuzzyTerm_creates_correct_json() { + public void FuzzyTerm_creates_correct_json() + { var json = ParadeDbJsonQuery.FuzzyTerm("Content", "machin", 2).ToJson(); var doc = Parse(json); var ft = doc.GetProperty("fuzzy_term"); @@ -95,7 +107,8 @@ public void FuzzyTerm_creates_correct_json() { } [Fact] - public void FuzzyTerm_with_options_creates_correct_json() { + public void FuzzyTerm_with_options_creates_correct_json() + { var json = ParadeDbJsonQuery.FuzzyTerm("Content", "machin", 2, true, true).ToJson(); var doc = Parse(json); var ft = doc.GetProperty("fuzzy_term"); @@ -106,7 +119,8 @@ public void FuzzyTerm_with_options_creates_correct_json() { // ── Phrase ──────────────────────────────────────────────────────── [Fact] - public void Phrase_creates_correct_json() { + public void Phrase_creates_correct_json() + { var json = ParadeDbJsonQuery.Phrase("Content", "neural", "networks").ToJson(); var doc = Parse(json); var phrase = doc.GetProperty("phrase"); @@ -117,7 +131,8 @@ public void Phrase_creates_correct_json() { } [Fact] - public void Phrase_with_slop_creates_correct_json() { + public void Phrase_with_slop_creates_correct_json() + { var json = ParadeDbJsonQuery.Phrase("Content", 2, "neural", "networks").ToJson(); var doc = Parse(json); var phrase = doc.GetProperty("phrase"); @@ -127,7 +142,8 @@ public void Phrase_with_slop_creates_correct_json() { // ── Phrase Prefix ──────────────────────────────────────────────── [Fact] - public void PhrasePrefix_creates_correct_json() { + public void PhrasePrefix_creates_correct_json() + { var json = ParadeDbJsonQuery.PhrasePrefix("Content", "running", "sh").ToJson(); var doc = Parse(json); var pp = doc.GetProperty("phrase_prefix"); @@ -138,7 +154,8 @@ public void PhrasePrefix_creates_correct_json() { // ── Regex ──────────────────────────────────────────────────────── [Fact] - public void Regex_creates_correct_json() { + public void Regex_creates_correct_json() + { var json = ParadeDbJsonQuery.Regex("Content", "neuro.*").ToJson(); var doc = Parse(json); var regex = doc.GetProperty("regex"); @@ -149,8 +166,11 @@ public void Regex_creates_correct_json() { // ── Range ──────────────────────────────────────────────────────── [Fact] - public void Range_with_both_bounds_creates_correct_json() { - var json = ParadeDbJsonQuery.Range("Price", 10, 100, lowerInclusive: true, upperInclusive: false).ToJson(); + public void Range_with_both_bounds_creates_correct_json() + { + var json = ParadeDbJsonQuery + .Range("Price", 10, 100, lowerInclusive: true, upperInclusive: false) + .ToJson(); var doc = Parse(json); var range = doc.GetProperty("range"); Assert.Equal("Price", range.GetProperty("field").GetString()); @@ -159,7 +179,8 @@ public void Range_with_both_bounds_creates_correct_json() { } [Fact] - public void Range_with_lower_bound_only_creates_correct_json() { + public void Range_with_lower_bound_only_creates_correct_json() + { var json = ParadeDbJsonQuery.Range("Price", 10, null).ToJson(); var doc = Parse(json); var range = doc.GetProperty("range"); @@ -168,15 +189,19 @@ public void Range_with_lower_bound_only_creates_correct_json() { } [Fact] - public void Range_with_lower_exclusive_uses_excluded_key() { - var json = ParadeDbJsonQuery.Range("Price", 10, 100, lowerInclusive: false, upperInclusive: true).ToJson(); + public void Range_with_lower_exclusive_uses_excluded_key() + { + var json = ParadeDbJsonQuery + .Range("Price", 10, 100, lowerInclusive: false, upperInclusive: true) + .ToJson(); var range = Parse(json).GetProperty("range"); Assert.Equal(10, range.GetProperty("lower_bound").GetProperty("excluded").GetInt32()); Assert.Equal(100, range.GetProperty("upper_bound").GetProperty("included").GetInt32()); } [Fact] - public void Range_with_upper_bound_only_creates_correct_json() { + public void Range_with_upper_bound_only_creates_correct_json() + { var json = ParadeDbJsonQuery.Range("Price", null, 100).ToJson(); var range = Parse(json).GetProperty("range"); Assert.Equal(JsonValueKind.Null, range.GetProperty("lower_bound").ValueKind); @@ -184,19 +209,24 @@ public void Range_with_upper_bound_only_creates_correct_json() { } [Fact] - public void Range_with_is_datetime_creates_correct_json() { + public void Range_with_is_datetime_creates_correct_json() + { var dt = new DateTime(2025, 1, 15, 0, 0, 0, DateTimeKind.Utc); var json = ParadeDbJsonQuery.Range("ReportingDate", dt, null, isDatetime: true).ToJson(); var doc = Parse(json); var range = doc.GetProperty("range"); Assert.True(range.GetProperty("is_datetime").GetBoolean()); - Assert.Equal("2025-01-15T00:00:00Z", range.GetProperty("lower_bound").GetProperty("included").GetString()); + Assert.Equal( + "2025-01-15T00:00:00Z", + range.GetProperty("lower_bound").GetProperty("included").GetString() + ); } // ── Boost ──────────────────────────────────────────────────────── [Fact] - public void Boost_wraps_inner_query_correctly() { + public void Boost_wraps_inner_query_correctly() + { var inner = ParadeDbJsonQuery.Parse("shoes"); var json = ParadeDbJsonQuery.Boost(inner, 2.5).ToJson(); var doc = Parse(json); @@ -208,7 +238,8 @@ public void Boost_wraps_inner_query_correctly() { // ── Const Score ────────────────────────────────────────────────── [Fact] - public void ConstScore_wraps_inner_query_correctly() { + public void ConstScore_wraps_inner_query_correctly() + { var inner = ParadeDbJsonQuery.Parse("shoes"); var json = ParadeDbJsonQuery.ConstScore(inner, 1.0).ToJson(); var doc = Parse(json); @@ -220,7 +251,8 @@ public void ConstScore_wraps_inner_query_correctly() { // ── Exists ─────────────────────────────────────────────────────── [Fact] - public void Exists_creates_correct_json() { + public void Exists_creates_correct_json() + { var json = ParadeDbJsonQuery.Exists("Content").ToJson(); var doc = Parse(json); Assert.Equal("Content", doc.GetProperty("exists").GetProperty("field").GetString()); @@ -229,7 +261,8 @@ public void Exists_creates_correct_json() { // ── All ────────────────────────────────────────────────────────── [Fact] - public void All_creates_correct_json() { + public void All_creates_correct_json() + { var json = ParadeDbJsonQuery.All().ToJson(); var doc = Parse(json); Assert.Equal(JsonValueKind.Null, doc.GetProperty("all").ValueKind); @@ -238,10 +271,14 @@ public void All_creates_correct_json() { // ── Disjunction Max ────────────────────────────────────────────── [Fact] - public void DisjunctionMax_creates_correct_json() { - var json = ParadeDbJsonQuery.DisjunctionMax( - ParadeDbJsonQuery.Parse("shoes"), - ParadeDbJsonQuery.Match("boots", "Content")).ToJson(); + public void DisjunctionMax_creates_correct_json() + { + var json = ParadeDbJsonQuery + .DisjunctionMax( + ParadeDbJsonQuery.Parse("shoes"), + ParadeDbJsonQuery.Match("boots", "Content") + ) + .ToJson(); var doc = Parse(json); var dm = doc.GetProperty("disjunction_max"); Assert.Equal(2, dm.GetProperty("disjuncts").GetArrayLength()); @@ -250,7 +287,8 @@ public void DisjunctionMax_creates_correct_json() { // ── More Like This ─────────────────────────────────────────────── [Fact] - public void MoreLikeThis_creates_correct_json() { + public void MoreLikeThis_creates_correct_json() + { var json = ParadeDbJsonQuery.MoreLikeThis(42).ToJson(); var doc = Parse(json); Assert.Equal(42, doc.GetProperty("more_like_this").GetProperty("key_value").GetInt32()); @@ -259,9 +297,11 @@ public void MoreLikeThis_creates_correct_json() { // ── Boolean ────────────────────────────────────────────────────── [Fact] - public void Boolean_must_creates_correct_json() { - var json = ParadeDbJsonQuery.Boolean(b => b - .Must(ParadeDbJsonQuery.Parse("shoes"))).ToJson(); + public void Boolean_must_creates_correct_json() + { + var json = ParadeDbJsonQuery + .Boolean(b => b.Must(ParadeDbJsonQuery.Parse("shoes"))) + .ToJson(); var doc = Parse(json); var boolean = doc.GetProperty("boolean"); Assert.Equal(1, boolean.GetProperty("must").GetArrayLength()); @@ -270,27 +310,37 @@ public void Boolean_must_creates_correct_json() { } [Fact] - public void Boolean_should_creates_correct_json() { - var json = ParadeDbJsonQuery.Boolean(b => b - .Should(ParadeDbJsonQuery.Parse("shoes"), ParadeDbJsonQuery.Parse("boots"))).ToJson(); + public void Boolean_should_creates_correct_json() + { + var json = ParadeDbJsonQuery + .Boolean(b => + b.Should(ParadeDbJsonQuery.Parse("shoes"), ParadeDbJsonQuery.Parse("boots")) + ) + .ToJson(); var doc = Parse(json); Assert.Equal(2, doc.GetProperty("boolean").GetProperty("should").GetArrayLength()); } [Fact] - public void Boolean_must_not_creates_correct_json() { - var json = ParadeDbJsonQuery.Boolean(b => b - .MustNot(ParadeDbJsonQuery.Term("Status", "archived"))).ToJson(); + public void Boolean_must_not_creates_correct_json() + { + var json = ParadeDbJsonQuery + .Boolean(b => b.MustNot(ParadeDbJsonQuery.Term("Status", "archived"))) + .ToJson(); var doc = Parse(json); Assert.Equal(1, doc.GetProperty("boolean").GetProperty("must_not").GetArrayLength()); } [Fact] - public void Boolean_combined_creates_correct_json() { - var json = ParadeDbJsonQuery.Boolean(b => b - .Must(ParadeDbJsonQuery.Parse("revenue growth")) - .Should(ParadeDbJsonQuery.Term("DocumentType", 10)) - .MustNot(ParadeDbJsonQuery.Term("Status", "archived"))).ToJson(); + public void Boolean_combined_creates_correct_json() + { + var json = ParadeDbJsonQuery + .Boolean(b => + b.Must(ParadeDbJsonQuery.Parse("revenue growth")) + .Should(ParadeDbJsonQuery.Term("DocumentType", 10)) + .MustNot(ParadeDbJsonQuery.Term("Status", "archived")) + ) + .ToJson(); var doc = Parse(json); var boolean = doc.GetProperty("boolean"); Assert.Equal(1, boolean.GetProperty("must").GetArrayLength()); @@ -299,14 +349,21 @@ public void Boolean_combined_creates_correct_json() { } [Fact] - public void Nested_boolean_creates_correct_json() { - var json = ParadeDbJsonQuery.Boolean(b => b - .Must( - ParadeDbJsonQuery.Parse("revenue growth"), - ParadeDbJsonQuery.Boolean(inner => inner - .Should( - ParadeDbJsonQuery.Term("DocumentType", 10), - ParadeDbJsonQuery.Term("DocumentType", 20))))).ToJson(); + public void Nested_boolean_creates_correct_json() + { + var json = ParadeDbJsonQuery + .Boolean(b => + b.Must( + ParadeDbJsonQuery.Parse("revenue growth"), + ParadeDbJsonQuery.Boolean(inner => + inner.Should( + ParadeDbJsonQuery.Term("DocumentType", 10), + ParadeDbJsonQuery.Term("DocumentType", 20) + ) + ) + ) + ) + .ToJson(); var doc = Parse(json); var must = doc.GetProperty("boolean").GetProperty("must"); Assert.Equal(2, must.GetArrayLength()); @@ -317,7 +374,8 @@ public void Nested_boolean_creates_correct_json() { // ── ToString / ToJson parity ───────────────────────────────────── [Fact] - public void ToString_returns_same_string_as_ToJson() { + public void ToString_returns_same_string_as_ToJson() + { var query = ParadeDbJsonQuery.Parse("shoes"); Assert.Equal(query.ToJson(), query.ToString()); } @@ -325,7 +383,8 @@ public void ToString_returns_same_string_as_ToJson() { // ── Match (field, value) — overload without options ────────────── [Fact] - public void Match_with_field_only_creates_correct_json() { + public void Match_with_field_only_creates_correct_json() + { var json = ParadeDbJsonQuery.Match("shoes", "Content").ToJson(); var doc = Parse(json); var match = doc.GetProperty("match"); @@ -338,7 +397,8 @@ public void Match_with_field_only_creates_correct_json() { // ── CreateJsonValue — remaining primitive switch arms ──────────── [Fact] - public void Term_with_long_value_serializes_as_number() { + public void Term_with_long_value_serializes_as_number() + { var json = ParadeDbJsonQuery.Term("Big", 9_000_000_000L).ToJson(); var value = Parse(json).GetProperty("term").GetProperty("value"); Assert.Equal(JsonValueKind.Number, value.ValueKind); @@ -346,7 +406,8 @@ public void Term_with_long_value_serializes_as_number() { } [Fact] - public void Term_with_double_value_serializes_as_number() { + public void Term_with_double_value_serializes_as_number() + { var json = ParadeDbJsonQuery.Term("Score", 2.5d).ToJson(); var value = Parse(json).GetProperty("term").GetProperty("value"); Assert.Equal(JsonValueKind.Number, value.ValueKind); @@ -354,7 +415,8 @@ public void Term_with_double_value_serializes_as_number() { } [Fact] - public void Term_with_float_value_serializes_as_number() { + public void Term_with_float_value_serializes_as_number() + { var json = ParadeDbJsonQuery.Term("Score", 1.25f).ToJson(); var value = Parse(json).GetProperty("term").GetProperty("value"); Assert.Equal(JsonValueKind.Number, value.ValueKind); @@ -362,14 +424,16 @@ public void Term_with_float_value_serializes_as_number() { } [Fact] - public void Term_with_bool_value_serializes_as_boolean() { + public void Term_with_bool_value_serializes_as_boolean() + { var json = ParadeDbJsonQuery.Term("InStock", true).ToJson(); var value = Parse(json).GetProperty("term").GetProperty("value"); Assert.Equal(JsonValueKind.True, value.ValueKind); } [Fact] - public void Term_with_enum_value_serializes_as_underlying_int() { + public void Term_with_enum_value_serializes_as_underlying_int() + { var json = ParadeDbJsonQuery.Term("Record", Bm25Record.Position).ToJson(); var value = Parse(json).GetProperty("term").GetProperty("value"); Assert.Equal(JsonValueKind.Number, value.ValueKind); @@ -377,7 +441,8 @@ public void Term_with_enum_value_serializes_as_underlying_int() { } [Fact] - public void Term_with_unsupported_type_falls_back_to_ToString() { + public void Term_with_unsupported_type_falls_back_to_ToString() + { // TimeSpan has a culture-invariant ToString — keeps the test stable across locales. var span = new TimeSpan(1, 2, 3); var json = ParadeDbJsonQuery.Term("Duration", span).ToJson(); @@ -387,7 +452,8 @@ public void Term_with_unsupported_type_falls_back_to_ToString() { } [Fact] - public void Term_with_non_utc_datetime_uses_round_trip_format() { + public void Term_with_non_utc_datetime_uses_round_trip_format() + { var local = new DateTime(2025, 1, 15, 12, 30, 0, DateTimeKind.Unspecified); var json = ParadeDbJsonQuery.Term("PublishedAt", local).ToJson(); var value = Parse(json).GetProperty("term").GetProperty("value"); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.Tests/QueryTranslationTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.Tests/QueryTranslationTests.cs index d652573..521b618 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.Tests/QueryTranslationTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.Tests/QueryTranslationTests.cs @@ -2,7 +2,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.Tests; -public class QueryTranslationTests : IDisposable { +public class QueryTranslationTests : IDisposable +{ private readonly TestDbContext _db = new(); public void Dispose() => _db.Dispose(); @@ -12,13 +13,15 @@ public class QueryTranslationTests : IDisposable { // ── Basic Search ────────────────────────────────────────────────── [Fact] - public void Matches_generates_or_operator() { + public void Matches_generates_or_operator() + { var sql = Sql(_db.Articles.Where(a => EF.Functions.Matches(a.Content, "shoes"))); Assert.Contains("|||", sql); } [Fact] - public void MatchesAll_generates_and_operator() { + public void MatchesAll_generates_and_operator() + { var sql = Sql(_db.Articles.Where(a => EF.Functions.MatchesAll(a.Content, "shoes"))); Assert.Contains("&&&", sql); } @@ -26,14 +29,20 @@ public void MatchesAll_generates_and_operator() { // ── Phrase Search ───────────────────────────────────────────────── [Fact] - public void MatchesPhrase_generates_phrase_operator() { - var sql = Sql(_db.Articles.Where(a => EF.Functions.MatchesPhrase(a.Content, "neural networks"))); + public void MatchesPhrase_generates_phrase_operator() + { + var sql = Sql( + _db.Articles.Where(a => EF.Functions.MatchesPhrase(a.Content, "neural networks")) + ); Assert.Contains("###", sql); } [Fact] - public void MatchesPhrase_with_slop_generates_slop_modifier() { - var sql = Sql(_db.Articles.Where(a => EF.Functions.MatchesPhrase(a.Content, "neural networks", 2))); + public void MatchesPhrase_with_slop_generates_slop_modifier() + { + var sql = Sql( + _db.Articles.Where(a => EF.Functions.MatchesPhrase(a.Content, "neural networks", 2)) + ); Assert.Contains("###", sql); Assert.Contains("::pdb.slop(2)", sql); } @@ -41,14 +50,18 @@ public void MatchesPhrase_with_slop_generates_slop_modifier() { // ── Term Search ─────────────────────────────────────────────────── [Fact] - public void MatchesTerm_generates_term_operator() { + public void MatchesTerm_generates_term_operator() + { var sql = Sql(_db.Articles.Where(a => EF.Functions.MatchesTerm(a.Content, "gpu"))); Assert.Contains("===", sql); } [Fact] - public void MatchesTermSet_generates_term_operator_with_array() { - var sql = Sql(_db.Articles.Where(a => EF.Functions.MatchesTermSet(a.Content, "gpu", "tpu"))); + public void MatchesTermSet_generates_term_operator_with_array() + { + var sql = Sql( + _db.Articles.Where(a => EF.Functions.MatchesTermSet(a.Content, "gpu", "tpu")) + ); Assert.Contains("===", sql); Assert.Contains("ARRAY", sql); } @@ -56,15 +69,19 @@ public void MatchesTermSet_generates_term_operator_with_array() { // ── Fuzzy Search ────────────────────────────────────────────────── [Fact] - public void MatchesFuzzy_generates_fuzzy_modifier() { + public void MatchesFuzzy_generates_fuzzy_modifier() + { var sql = Sql(_db.Articles.Where(a => EF.Functions.MatchesFuzzy(a.Content, "machin", 2))); Assert.Contains("|||", sql); Assert.Contains("::pdb.fuzzy(2)", sql); } [Fact] - public void MatchesFuzzy_full_generates_pdb_match_with_options() { - var sql = Sql(_db.Articles.Where(a => EF.Functions.MatchesFuzzy(a.Content, "machin", 2, true, false))); + public void MatchesFuzzy_full_generates_pdb_match_with_options() + { + var sql = Sql( + _db.Articles.Where(a => EF.Functions.MatchesFuzzy(a.Content, "machin", 2, true, false)) + ); Assert.Contains("@@@", sql); Assert.Contains("pdb.match(", sql); Assert.Contains("distance =>", sql); @@ -74,30 +91,46 @@ public void MatchesFuzzy_full_generates_pdb_match_with_options() { } [Fact] - public void MatchesAllFuzzy_generates_and_with_fuzzy() { - var sql = Sql(_db.Articles.Where(a => EF.Functions.MatchesAllFuzzy(a.Content, "machin", 2))); + public void MatchesAllFuzzy_generates_and_with_fuzzy() + { + var sql = Sql( + _db.Articles.Where(a => EF.Functions.MatchesAllFuzzy(a.Content, "machin", 2)) + ); Assert.Contains("&&&", sql); Assert.Contains("::pdb.fuzzy(2)", sql); } [Fact] - public void MatchesAllFuzzy_full_generates_pdb_match_with_conjunction() { - var sql = Sql(_db.Articles.Where(a => EF.Functions.MatchesAllFuzzy(a.Content, "machin", 1, false, true))); + public void MatchesAllFuzzy_full_generates_pdb_match_with_conjunction() + { + var sql = Sql( + _db.Articles.Where(a => + EF.Functions.MatchesAllFuzzy(a.Content, "machin", 1, false, true) + ) + ); Assert.Contains("@@@", sql); Assert.Contains("pdb.match(", sql); Assert.Contains("conjunction_mode =>", sql); } [Fact] - public void MatchesTermFuzzy_generates_term_with_fuzzy() { - var sql = Sql(_db.Articles.Where(a => EF.Functions.MatchesTermFuzzy(a.Content, "machin", 1))); + public void MatchesTermFuzzy_generates_term_with_fuzzy() + { + var sql = Sql( + _db.Articles.Where(a => EF.Functions.MatchesTermFuzzy(a.Content, "machin", 1)) + ); Assert.Contains("===", sql); Assert.Contains("::pdb.fuzzy(1)", sql); } [Fact] - public void MatchesTermFuzzy_full_generates_pdb_fuzzy_term_function() { - var sql = Sql(_db.Articles.Where(a => EF.Functions.MatchesTermFuzzy(a.Content, "machin", 2, true, true))); + public void MatchesTermFuzzy_full_generates_pdb_fuzzy_term_function() + { + var sql = Sql( + _db.Articles.Where(a => + EF.Functions.MatchesTermFuzzy(a.Content, "machin", 2, true, true) + ) + ); Assert.Contains("@@@", sql); Assert.Contains("pdb.fuzzy_term(", sql); } @@ -105,15 +138,21 @@ public void MatchesTermFuzzy_full_generates_pdb_fuzzy_term_function() { // ── Boost ───────────────────────────────────────────────────────── [Fact] - public void MatchesBoosted_generates_boost_modifier() { - var sql = Sql(_db.Articles.Where(a => EF.Functions.MatchesBoosted(a.Content, "shoes", 2.0))); + public void MatchesBoosted_generates_boost_modifier() + { + var sql = Sql( + _db.Articles.Where(a => EF.Functions.MatchesBoosted(a.Content, "shoes", 2.0)) + ); Assert.Contains("|||", sql); Assert.Contains("::pdb.boost(2)", sql); } [Fact] - public void MatchesAllBoosted_generates_and_with_boost() { - var sql = Sql(_db.Articles.Where(a => EF.Functions.MatchesAllBoosted(a.Content, "shoes", 1.5))); + public void MatchesAllBoosted_generates_and_with_boost() + { + var sql = Sql( + _db.Articles.Where(a => EF.Functions.MatchesAllBoosted(a.Content, "shoes", 1.5)) + ); Assert.Contains("&&&", sql); Assert.Contains("::pdb.boost(1.5)", sql); } @@ -121,15 +160,21 @@ public void MatchesAllBoosted_generates_and_with_boost() { // ── Fuzzy + Boost Combined ──────────────────────────────────────── [Fact] - public void MatchesFuzzyBoosted_generates_fuzzy_and_boost() { - var sql = Sql(_db.Articles.Where(a => EF.Functions.MatchesFuzzyBoosted(a.Content, "shoes", 2, 2.0))); + public void MatchesFuzzyBoosted_generates_fuzzy_and_boost() + { + var sql = Sql( + _db.Articles.Where(a => EF.Functions.MatchesFuzzyBoosted(a.Content, "shoes", 2, 2.0)) + ); Assert.Contains("|||", sql); Assert.Contains("::pdb.fuzzy(2)::pdb.boost(2)", sql); } [Fact] - public void MatchesAllFuzzyBoosted_generates_and_fuzzy_and_boost() { - var sql = Sql(_db.Articles.Where(a => EF.Functions.MatchesAllFuzzyBoosted(a.Content, "shoes", 1, 3.0))); + public void MatchesAllFuzzyBoosted_generates_and_fuzzy_and_boost() + { + var sql = Sql( + _db.Articles.Where(a => EF.Functions.MatchesAllFuzzyBoosted(a.Content, "shoes", 1, 3.0)) + ); Assert.Contains("&&&", sql); Assert.Contains("::pdb.fuzzy(1)::pdb.boost(3)", sql); } @@ -137,28 +182,34 @@ public void MatchesAllFuzzyBoosted_generates_and_fuzzy_and_boost() { // ── BM25 Scoring ────────────────────────────────────────────────── [Fact] - public void Score_generates_pdb_score_function() { - var sql = Sql(_db.Articles - .Where(a => EF.Functions.Matches(a.Content, "test")) - .Select(a => new { Score = EF.Functions.Score(a.Id) })); + public void Score_generates_pdb_score_function() + { + var sql = Sql( + _db.Articles.Where(a => EF.Functions.Matches(a.Content, "test")) + .Select(a => new { Score = EF.Functions.Score(a.Id) }) + ); Assert.Contains("pdb.score(", sql); } // ── Snippets ────────────────────────────────────────────────────── [Fact] - public void Snippet_generates_pdb_snippet_function() { - var sql = Sql(_db.Articles - .Where(a => EF.Functions.Matches(a.Content, "test")) - .Select(a => new { Snip = EF.Functions.Snippet(a.Content) })); + public void Snippet_generates_pdb_snippet_function() + { + var sql = Sql( + _db.Articles.Where(a => EF.Functions.Matches(a.Content, "test")) + .Select(a => new { Snip = EF.Functions.Snippet(a.Content) }) + ); Assert.Contains("pdb.snippet(", sql); } [Fact] - public void Snippet_with_params_generates_named_args() { - var sql = Sql(_db.Articles - .Where(a => EF.Functions.Matches(a.Content, "test")) - .Select(a => new { Snip = EF.Functions.Snippet(a.Content, "", "", 100) })); + public void Snippet_with_params_generates_named_args() + { + var sql = Sql( + _db.Articles.Where(a => EF.Functions.Matches(a.Content, "test")) + .Select(a => new { Snip = EF.Functions.Snippet(a.Content, "", "", 100) }) + ); Assert.Contains("pdb.snippet(", sql); Assert.Contains("start_tag =>", sql); Assert.Contains("end_tag =>", sql); @@ -166,10 +217,12 @@ public void Snippet_with_params_generates_named_args() { } [Fact] - public void Snippets_generates_named_args_with_quoted_limit_offset() { - var sql = Sql(_db.Articles - .Where(a => EF.Functions.Matches(a.Content, "test")) - .Select(a => new { Snips = EF.Functions.Snippets(a.Content, 15, 5, 0) })); + public void Snippets_generates_named_args_with_quoted_limit_offset() + { + var sql = Sql( + _db.Articles.Where(a => EF.Functions.Matches(a.Content, "test")) + .Select(a => new { Snips = EF.Functions.Snippets(a.Content, 15, 5, 0) }) + ); Assert.Contains("pdb.snippets(", sql); Assert.Contains("max_num_chars =>", sql); Assert.Contains("\"limit\" =>", sql); @@ -179,14 +232,16 @@ public void Snippets_generates_named_args_with_quoted_limit_offset() { // ── Parse Query ─────────────────────────────────────────────────── [Fact] - public void Parse_generates_parse_with_at_operator() { + public void Parse_generates_parse_with_at_operator() + { var sql = Sql(_db.Articles.Where(a => EF.Functions.Parse(a.Id, "title:shoes"))); Assert.Contains("@@@", sql); Assert.Contains("pdb.parse(", sql); } [Fact] - public void Parse_with_options_generates_named_args() { + public void Parse_with_options_generates_named_args() + { var sql = Sql(_db.Articles.Where(a => EF.Functions.Parse(a.Id, "shoes", true, true))); Assert.Contains("@@@", sql); Assert.Contains("pdb.parse(", sql); @@ -197,7 +252,8 @@ public void Parse_with_options_generates_named_args() { // ── Regex Search ────────────────────────────────────────────────── [Fact] - public void Regex_generates_regex_with_at_operator() { + public void Regex_generates_regex_with_at_operator() + { var sql = Sql(_db.Articles.Where(a => EF.Functions.Regex(a.Content, "neuro.*"))); Assert.Contains("@@@", sql); Assert.Contains("pdb.regex(", sql); @@ -206,15 +262,21 @@ public void Regex_generates_regex_with_at_operator() { // ── Phrase Prefix ───────────────────────────────────────────────── [Fact] - public void PhrasePrefix_generates_phrase_prefix_with_array() { - var sql = Sql(_db.Articles.Where(a => EF.Functions.PhrasePrefix(a.Content, "running", "sh"))); + public void PhrasePrefix_generates_phrase_prefix_with_array() + { + var sql = Sql( + _db.Articles.Where(a => EF.Functions.PhrasePrefix(a.Content, "running", "sh")) + ); Assert.Contains("@@@", sql); Assert.Contains("pdb.phrase_prefix(", sql); } [Fact] - public void PhrasePrefix_with_max_expansions_generates_named_arg() { - var sql = Sql(_db.Articles.Where(a => EF.Functions.PhrasePrefix(a.Content, 10, "running", "sh"))); + public void PhrasePrefix_with_max_expansions_generates_named_arg() + { + var sql = Sql( + _db.Articles.Where(a => EF.Functions.PhrasePrefix(a.Content, 10, "running", "sh")) + ); Assert.Contains("@@@", sql); Assert.Contains("pdb.phrase_prefix(", sql); Assert.Contains("max_expansion =>", sql); @@ -223,14 +285,16 @@ public void PhrasePrefix_with_max_expansions_generates_named_arg() { // ── More Like This ──────────────────────────────────────────────── [Fact] - public void MoreLikeThis_generates_more_like_this_function() { + public void MoreLikeThis_generates_more_like_this_function() + { var sql = Sql(_db.Articles.Where(a => EF.Functions.MoreLikeThis(a.Id, 3))); Assert.Contains("@@@", sql); Assert.Contains("pdb.more_like_this(", sql); } [Fact] - public void MoreLikeThis_with_fields_generates_array_arg() { + public void MoreLikeThis_with_fields_generates_array_arg() + { var sql = Sql(_db.Articles.Where(a => EF.Functions.MoreLikeThis(a.Id, 3, "description"))); Assert.Contains("@@@", sql); Assert.Contains("pdb.more_like_this(", sql); @@ -239,7 +303,8 @@ public void MoreLikeThis_with_fields_generates_array_arg() { // ── JSON Query Search ────────────────────────────────────────────── [Fact] - public void JsonSearch_generates_at_operator_with_pdb_query_cast() { + public void JsonSearch_generates_at_operator_with_pdb_query_cast() + { var json = ParadeDbJsonQuery.Parse("revenue growth").ToJson(); var sql = Sql(_db.Chunks.Where(c => EF.Functions.JsonSearch(c.Id, json))); Assert.Contains("@@@", sql); @@ -247,22 +312,27 @@ public void JsonSearch_generates_at_operator_with_pdb_query_cast() { } [Fact] - public void JsonSearch_boolean_query_generates_json_with_cast() { - var query = ParadeDbJsonQuery.Boolean(b => b - .Must( + public void JsonSearch_boolean_query_generates_json_with_cast() + { + var query = ParadeDbJsonQuery.Boolean(b => + b.Must( ParadeDbJsonQuery.Parse("revenue growth"), - ParadeDbJsonQuery.Term("DocumentType", 10))); + ParadeDbJsonQuery.Term("DocumentType", 10) + ) + ); var sql = Sql(_db.Chunks.Where(c => EF.Functions.JsonSearch(c.Id, query.ToJson()))); Assert.Contains("@@@", sql); Assert.Contains("::jsonb", sql); } [Fact] - public void JsonSearch_composes_with_order_by_score() { + public void JsonSearch_composes_with_order_by_score() + { var json = ParadeDbJsonQuery.Parse("test").ToJson(); - var sql = Sql(_db.Chunks - .Where(c => EF.Functions.JsonSearch(c.Id, json)) - .OrderByDescending(c => EF.Functions.Score(c.Id))); + var sql = Sql( + _db.Chunks.Where(c => EF.Functions.JsonSearch(c.Id, json)) + .OrderByDescending(c => EF.Functions.Score(c.Id)) + ); Assert.Contains("@@@", sql); Assert.Contains("::jsonb", sql); Assert.Contains("pdb.score(", sql); @@ -270,27 +340,29 @@ public void JsonSearch_composes_with_order_by_score() { } [Fact] - public void JsonSearch_composes_with_take() { + public void JsonSearch_composes_with_take() + { var json = ParadeDbJsonQuery.Parse("test").ToJson(); - var sql = Sql(_db.Chunks - .Where(c => EF.Functions.JsonSearch(c.Id, json)) - .Take(5)); + var sql = Sql(_db.Chunks.Where(c => EF.Functions.JsonSearch(c.Id, json)).Take(5)); Assert.Contains("@@@", sql); Assert.Contains("LIMIT", sql); } [Fact] - public void JsonSearch_composes_with_standard_linq_where() { + public void JsonSearch_composes_with_standard_linq_where() + { var json = ParadeDbJsonQuery.Parse("test").ToJson(); - var sql = Sql(_db.Chunks - .Where(c => EF.Functions.JsonSearch(c.Id, json) && c.DocumentType > 5)); + var sql = Sql( + _db.Chunks.Where(c => EF.Functions.JsonSearch(c.Id, json) && c.DocumentType > 5) + ); Assert.Contains("@@@", sql); Assert.Contains("::jsonb", sql); Assert.Contains(">", sql); } [Fact] - public void JsonSearch_extension_generates_same_as_ef_functions() { + public void JsonSearch_extension_generates_same_as_ef_functions() + { var query = ParadeDbJsonQuery.Parse("test"); var sqlExt = Sql(_db.Chunks.JsonSearch(c => c.Id, query)); var sqlDirect = Sql(_db.Chunks.Where(c => EF.Functions.JsonSearch(c.Id, query.ToJson()))); @@ -300,11 +372,18 @@ public void JsonSearch_extension_generates_same_as_ef_functions() { } [Fact] - public void JsonSearch_inline_boolean_generates_correct_sql() { - var sql = Sql(_db.Chunks.JsonSearch(c => c.Id, b => b - .Must( - ParadeDbJsonQuery.Parse("revenue growth"), - ParadeDbJsonQuery.Term("DocumentType", 10)))); + public void JsonSearch_inline_boolean_generates_correct_sql() + { + var sql = Sql( + _db.Chunks.JsonSearch( + c => c.Id, + b => + b.Must( + ParadeDbJsonQuery.Parse("revenue growth"), + ParadeDbJsonQuery.Term("DocumentType", 10) + ) + ) + ); Assert.Contains("@@@", sql); Assert.Contains("::jsonb", sql); } @@ -312,18 +391,20 @@ public void JsonSearch_inline_boolean_generates_correct_sql() { // ── Combining with LINQ ─────────────────────────────────────────── [Fact] - public void Search_composes_with_standard_linq_where() { - var sql = Sql(_db.Articles - .Where(a => EF.Functions.Matches(a.Content, "test") && a.Id > 5)); + public void Search_composes_with_standard_linq_where() + { + var sql = Sql(_db.Articles.Where(a => EF.Functions.Matches(a.Content, "test") && a.Id > 5)); Assert.Contains("|||", sql); Assert.Contains(">", sql); } [Fact] - public void Search_composes_with_order_by_score() { - var sql = Sql(_db.Articles - .Where(a => EF.Functions.Matches(a.Content, "test")) - .OrderByDescending(a => EF.Functions.Score(a.Id))); + public void Search_composes_with_order_by_score() + { + var sql = Sql( + _db.Articles.Where(a => EF.Functions.Matches(a.Content, "test")) + .OrderByDescending(a => EF.Functions.Score(a.Id)) + ); Assert.Contains("|||", sql); Assert.Contains("pdb.score(", sql); Assert.Contains("ORDER BY", sql); @@ -332,20 +413,24 @@ public void Search_composes_with_order_by_score() { // ── Score Ordering Extensions ───────────────────────────────────── [Fact] - public void OrderByScoreDescending_extension_emits_pdb_score_with_desc() { - var sql = Sql(_db.Chunks - .JsonSearch(c => c.Id, ParadeDbJsonQuery.Parse("test")) - .OrderByScoreDescending(c => c.Id)); + public void OrderByScoreDescending_extension_emits_pdb_score_with_desc() + { + var sql = Sql( + _db.Chunks.JsonSearch(c => c.Id, ParadeDbJsonQuery.Parse("test")) + .OrderByScoreDescending(c => c.Id) + ); Assert.Contains("pdb.score(", sql); Assert.Contains("ORDER BY", sql); Assert.Contains("DESC", sql); } [Fact] - public void OrderByScore_extension_emits_pdb_score_without_desc() { - var sql = Sql(_db.Chunks - .JsonSearch(c => c.Id, ParadeDbJsonQuery.Parse("test")) - .OrderByScore(c => c.Id)); + public void OrderByScore_extension_emits_pdb_score_without_desc() + { + var sql = Sql( + _db.Chunks.JsonSearch(c => c.Id, ParadeDbJsonQuery.Parse("test")) + .OrderByScore(c => c.Id) + ); Assert.Contains("pdb.score(", sql); Assert.Contains("ORDER BY", sql); Assert.DoesNotContain("DESC", sql); @@ -359,7 +444,8 @@ public void OrderByScore_extension_emits_pdb_score_without_desc() { /// plugin, including ours — ours should return null so Npgsql's translator handles it. /// [Fact] - public void NonParadeDbMethod_is_translated_by_other_plugins() { + public void NonParadeDbMethod_is_translated_by_other_plugins() + { var sql = Sql(_db.Articles.Where(a => a.Title.StartsWith("foo"))); Assert.DoesNotContain("@@@", sql); Assert.DoesNotContain("|||", sql); @@ -377,13 +463,18 @@ public void NonParadeDbMethod_is_translated_by_other_plugins() { /// instances — exercising the "args changed → build new expression" branch. /// [Fact] - public void Snippet_with_captured_parameters_still_emits_named_args() { + public void Snippet_with_captured_parameters_still_emits_named_args() + { var startTag = ""; var endTag = ""; var maxChars = 100; - var sql = Sql(_db.Articles - .Where(a => EF.Functions.Matches(a.Content, "test")) - .Select(a => new { Snip = EF.Functions.Snippet(a.Content, startTag, endTag, maxChars) })); + var sql = Sql( + _db.Articles.Where(a => EF.Functions.Matches(a.Content, "test")) + .Select(a => new + { + Snip = EF.Functions.Snippet(a.Content, startTag, endTag, maxChars), + }) + ); Assert.Contains("pdb.snippet(", sql); Assert.Contains("start_tag =>", sql); Assert.Contains("end_tag =>", sql); @@ -391,13 +482,18 @@ public void Snippet_with_captured_parameters_still_emits_named_args() { } [Fact] - public void Snippets_with_captured_parameters_still_emits_named_args() { + public void Snippets_with_captured_parameters_still_emits_named_args() + { var maxChars = 15; var limit = 5; var offset = 0; - var sql = Sql(_db.Articles - .Where(a => EF.Functions.Matches(a.Content, "test")) - .Select(a => new { Snips = EF.Functions.Snippets(a.Content, maxChars, limit, offset) })); + var sql = Sql( + _db.Articles.Where(a => EF.Functions.Matches(a.Content, "test")) + .Select(a => new + { + Snips = EF.Functions.Snippets(a.Content, maxChars, limit, offset), + }) + ); Assert.Contains("pdb.snippets(", sql); Assert.Contains("max_num_chars =>", sql); Assert.Contains("\"limit\" =>", sql); @@ -412,18 +508,22 @@ public void Snippets_with_captured_parameters_still_emits_named_args() { /// suffix (e.g. ::pdb.fuzzy(2)). Captured int → SqlParameterExpression → throws. /// [Fact] - public void MatchesFuzzy_with_captured_distance_throws_at_translation() { + public void MatchesFuzzy_with_captured_distance_throws_at_translation() + { var distance = 2; - var ex = Assert.Throws( - () => Sql(_db.Articles.Where(a => EF.Functions.MatchesFuzzy(a.Content, "x", distance)))); + var ex = Assert.Throws(() => + Sql(_db.Articles.Where(a => EF.Functions.MatchesFuzzy(a.Content, "x", distance))) + ); Assert.Contains("compile-time constants", ex.Message); } [Fact] - public void MatchesBoosted_with_captured_boost_throws_at_translation() { + public void MatchesBoosted_with_captured_boost_throws_at_translation() + { var boost = 2.0; - var ex = Assert.Throws( - () => Sql(_db.Articles.Where(a => EF.Functions.MatchesBoosted(a.Content, "x", boost)))); + var ex = Assert.Throws(() => + Sql(_db.Articles.Where(a => EF.Functions.MatchesBoosted(a.Content, "x", boost))) + ); Assert.Contains("compile-time constants", ex.Message); } @@ -435,7 +535,8 @@ public void MatchesBoosted_with_captured_boost_throws_at_translation() { /// take their pass-through branches. The int-keyed tests don't exercise either path. /// [Fact] - public void JsonSearch_with_reference_type_key_emits_at_operator() { + public void JsonSearch_with_reference_type_key_emits_at_operator() + { var sql = Sql(_db.Chunks.JsonSearch(c => c.Content, ParadeDbJsonQuery.Parse("revenue"))); Assert.Contains("@@@", sql); Assert.Contains("::jsonb", sql); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.Tests/SqlOutputTests.cs b/Equibles.ParadeDB.EntityFrameworkCore.Tests/SqlOutputTests.cs index 760ef07..e1f1a3d 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.Tests/SqlOutputTests.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.Tests/SqlOutputTests.cs @@ -5,64 +5,185 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.Tests; /// /// Prints generated SQL for visual inspection. Not assertions — just output. /// -public class SqlOutputTests : IDisposable { +public class SqlOutputTests : IDisposable +{ private readonly TestDbContext _db = new(); public void Dispose() => _db.Dispose(); private string Sql(IQueryable query) => query.ToQueryString(); - private IQueryable JsonSearchQuery() { + private IQueryable JsonSearchQuery() + { var json = ParadeDbJsonQuery.Parse("revenue growth").ToJson(); return _db.Chunks.Where(c => EF.Functions.JsonSearch(c.Id, json)); } - private IQueryable JsonSearchBooleanQuery() { - var json = ParadeDbJsonQuery.Boolean(b => b - .Must( - ParadeDbJsonQuery.Parse("revenue growth"), - ParadeDbJsonQuery.Term("DocumentType", 10))).ToJson(); + private IQueryable JsonSearchBooleanQuery() + { + var json = ParadeDbJsonQuery + .Boolean(b => + b.Must( + ParadeDbJsonQuery.Parse("revenue growth"), + ParadeDbJsonQuery.Term("DocumentType", 10) + ) + ) + .ToJson(); return _db.Chunks.Where(c => EF.Functions.JsonSearch(c.Id, json)); } [Fact] - public void Print_all_query_translations() { - var queries = new (string Label, string Sql)[] { + public void Print_all_query_translations() + { + var queries = new (string Label, string Sql)[] + { ("Matches", Sql(_db.Articles.Where(a => EF.Functions.Matches(a.Content, "shoes")))), - ("MatchesAll", Sql(_db.Articles.Where(a => EF.Functions.MatchesAll(a.Content, "shoes")))), - ("MatchesPhrase", Sql(_db.Articles.Where(a => EF.Functions.MatchesPhrase(a.Content, "neural networks")))), - ("MatchesPhrase+slop", Sql(_db.Articles.Where(a => EF.Functions.MatchesPhrase(a.Content, "neural networks", 2)))), - ("MatchesTerm", Sql(_db.Articles.Where(a => EF.Functions.MatchesTerm(a.Content, "gpu")))), - ("MatchesTermSet", Sql(_db.Articles.Where(a => EF.Functions.MatchesTermSet(a.Content, "gpu", "tpu")))), - ("MatchesFuzzy", Sql(_db.Articles.Where(a => EF.Functions.MatchesFuzzy(a.Content, "machin", 2)))), - ("MatchesFuzzy+opts", Sql(_db.Articles.Where(a => EF.Functions.MatchesFuzzy(a.Content, "machin", 2, true, false)))), - ("MatchesAllFuzzy", Sql(_db.Articles.Where(a => EF.Functions.MatchesAllFuzzy(a.Content, "machin", 2)))), - ("MatchesTermFuzzy", Sql(_db.Articles.Where(a => EF.Functions.MatchesTermFuzzy(a.Content, "machin", 1)))), - ("MatchesBoosted", Sql(_db.Articles.Where(a => EF.Functions.MatchesBoosted(a.Content, "shoes", 2.0)))), - ("MatchesAllBoosted", Sql(_db.Articles.Where(a => EF.Functions.MatchesAllBoosted(a.Content, "shoes", 1.5)))), - ("MatchesFuzzyBoosted", Sql(_db.Articles.Where(a => EF.Functions.MatchesFuzzyBoosted(a.Content, "shoes", 2, 2.0)))), - ("MatchesAllFuzzyBoosted", Sql(_db.Articles.Where(a => EF.Functions.MatchesAllFuzzyBoosted(a.Content, "shoes", 1, 3.0)))), - ("Score", Sql(_db.Articles.Where(a => EF.Functions.Matches(a.Content, "test")).Select(a => new { Score = EF.Functions.Score(a.Id) }))), - ("Snippet", Sql(_db.Articles.Where(a => EF.Functions.Matches(a.Content, "test")).Select(a => new { Snip = EF.Functions.Snippet(a.Content) }))), - ("Snippet+params", Sql(_db.Articles.Where(a => EF.Functions.Matches(a.Content, "test")).Select(a => new { Snip = EF.Functions.Snippet(a.Content, "", "", 100) }))), - ("Snippets", Sql(_db.Articles.Where(a => EF.Functions.Matches(a.Content, "test")).Select(a => new { Snips = EF.Functions.Snippets(a.Content, 15, 5, 0) }))), + ( + "MatchesAll", + Sql(_db.Articles.Where(a => EF.Functions.MatchesAll(a.Content, "shoes"))) + ), + ( + "MatchesPhrase", + Sql( + _db.Articles.Where(a => + EF.Functions.MatchesPhrase(a.Content, "neural networks") + ) + ) + ), + ( + "MatchesPhrase+slop", + Sql( + _db.Articles.Where(a => + EF.Functions.MatchesPhrase(a.Content, "neural networks", 2) + ) + ) + ), + ( + "MatchesTerm", + Sql(_db.Articles.Where(a => EF.Functions.MatchesTerm(a.Content, "gpu"))) + ), + ( + "MatchesTermSet", + Sql(_db.Articles.Where(a => EF.Functions.MatchesTermSet(a.Content, "gpu", "tpu"))) + ), + ( + "MatchesFuzzy", + Sql(_db.Articles.Where(a => EF.Functions.MatchesFuzzy(a.Content, "machin", 2))) + ), + ( + "MatchesFuzzy+opts", + Sql( + _db.Articles.Where(a => + EF.Functions.MatchesFuzzy(a.Content, "machin", 2, true, false) + ) + ) + ), + ( + "MatchesAllFuzzy", + Sql(_db.Articles.Where(a => EF.Functions.MatchesAllFuzzy(a.Content, "machin", 2))) + ), + ( + "MatchesTermFuzzy", + Sql(_db.Articles.Where(a => EF.Functions.MatchesTermFuzzy(a.Content, "machin", 1))) + ), + ( + "MatchesBoosted", + Sql(_db.Articles.Where(a => EF.Functions.MatchesBoosted(a.Content, "shoes", 2.0))) + ), + ( + "MatchesAllBoosted", + Sql( + _db.Articles.Where(a => EF.Functions.MatchesAllBoosted(a.Content, "shoes", 1.5)) + ) + ), + ( + "MatchesFuzzyBoosted", + Sql( + _db.Articles.Where(a => + EF.Functions.MatchesFuzzyBoosted(a.Content, "shoes", 2, 2.0) + ) + ) + ), + ( + "MatchesAllFuzzyBoosted", + Sql( + _db.Articles.Where(a => + EF.Functions.MatchesAllFuzzyBoosted(a.Content, "shoes", 1, 3.0) + ) + ) + ), + ( + "Score", + Sql( + _db.Articles.Where(a => EF.Functions.Matches(a.Content, "test")) + .Select(a => new { Score = EF.Functions.Score(a.Id) }) + ) + ), + ( + "Snippet", + Sql( + _db.Articles.Where(a => EF.Functions.Matches(a.Content, "test")) + .Select(a => new { Snip = EF.Functions.Snippet(a.Content) }) + ) + ), + ( + "Snippet+params", + Sql( + _db.Articles.Where(a => EF.Functions.Matches(a.Content, "test")) + .Select(a => new + { + Snip = EF.Functions.Snippet(a.Content, "", "", 100), + }) + ) + ), + ( + "Snippets", + Sql( + _db.Articles.Where(a => EF.Functions.Matches(a.Content, "test")) + .Select(a => new { Snips = EF.Functions.Snippets(a.Content, 15, 5, 0) }) + ) + ), ("Parse", Sql(_db.Articles.Where(a => EF.Functions.Parse(a.Id, "title:shoes")))), - ("Parse+opts", Sql(_db.Articles.Where(a => EF.Functions.Parse(a.Id, "shoes", true, true)))), + ( + "Parse+opts", + Sql(_db.Articles.Where(a => EF.Functions.Parse(a.Id, "shoes", true, true))) + ), ("Regex", Sql(_db.Articles.Where(a => EF.Functions.Regex(a.Content, "neuro.*")))), - ("PhrasePrefix", Sql(_db.Articles.Where(a => EF.Functions.PhrasePrefix(a.Content, "running", "sh")))), - ("PhrasePrefix+max", Sql(_db.Articles.Where(a => EF.Functions.PhrasePrefix(a.Content, 10, "running", "sh")))), + ( + "PhrasePrefix", + Sql(_db.Articles.Where(a => EF.Functions.PhrasePrefix(a.Content, "running", "sh"))) + ), + ( + "PhrasePrefix+max", + Sql( + _db.Articles.Where(a => + EF.Functions.PhrasePrefix(a.Content, 10, "running", "sh") + ) + ) + ), ("MoreLikeThis", Sql(_db.Articles.Where(a => EF.Functions.MoreLikeThis(a.Id, 3)))), - ("MoreLikeThis+fields", Sql(_db.Articles.Where(a => EF.Functions.MoreLikeThis(a.Id, 3, "description")))), + ( + "MoreLikeThis+fields", + Sql(_db.Articles.Where(a => EF.Functions.MoreLikeThis(a.Id, 3, "description"))) + ), ("JsonSearch", Sql(JsonSearchQuery())), ("JsonSearch+boolean", Sql(JsonSearchBooleanQuery())), - ("JsonSearch+extension", Sql(_db.Chunks.JsonSearch(c => c.Id, ParadeDbJsonQuery.Parse("test")))), - ("JsonSearch+score+limit", Sql(_db.Chunks - .JsonSearch(c => c.Id, ParadeDbJsonQuery.Parse("test")) - .OrderByScoreDescending(c => c.Id) - .Take(5))), + ( + "JsonSearch+extension", + Sql(_db.Chunks.JsonSearch(c => c.Id, ParadeDbJsonQuery.Parse("test"))) + ), + ( + "JsonSearch+score+limit", + Sql( + _db.Chunks.JsonSearch(c => c.Id, ParadeDbJsonQuery.Parse("test")) + .OrderByScoreDescending(c => c.Id) + .Take(5) + ) + ), }; - foreach (var (label, sql) in queries) { + foreach (var (label, sql) in queries) + { Console.WriteLine($"── {label} ──"); Console.WriteLine(sql); Console.WriteLine(); diff --git a/Equibles.ParadeDB.EntityFrameworkCore.Tests/TestDbContext.cs b/Equibles.ParadeDB.EntityFrameworkCore.Tests/TestDbContext.cs index f0e3661..67ff217 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore.Tests/TestDbContext.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore.Tests/TestDbContext.cs @@ -3,7 +3,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.Tests; [Bm25Index(nameof(Id), nameof(Title), nameof(Content))] -public class Article { +public class Article +{ public int Id { get; set; } public string Title { get; set; } = null!; public string Content { get; set; } = null!; @@ -11,18 +12,21 @@ public class Article { } [Bm25Index(nameof(Id), nameof(Content), nameof(DocumentId), nameof(DocumentType))] -public class Chunk { +public class Chunk +{ public int Id { get; set; } public string Content { get; set; } = null!; public Guid DocumentId { get; set; } public int DocumentType { get; set; } } -public class TestDbContext : DbContext { +public class TestDbContext : DbContext +{ public DbSet
Articles { get; set; } = null!; public DbSet Chunks { get; set; } = null!; - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { optionsBuilder.UseNpgsql("Host=localhost;Database=test", npgsql => npgsql.UseParadeDb()); } } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/Bm25BooleanAttribute.cs b/Equibles.ParadeDB.EntityFrameworkCore/Bm25BooleanAttribute.cs index 6224466..a9e81cf 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/Bm25BooleanAttribute.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/Bm25BooleanAttribute.cs @@ -4,7 +4,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; /// Configures a boolean column inside a BM25 index. /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] -public sealed class Bm25BooleanAttribute : Attribute { +public sealed class Bm25BooleanAttribute : Attribute +{ public bool Fast { get; set; } public bool Indexed { get; set; } = true; } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/Bm25DateTimeAttribute.cs b/Equibles.ParadeDB.EntityFrameworkCore/Bm25DateTimeAttribute.cs index 4798442..790260b 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/Bm25DateTimeAttribute.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/Bm25DateTimeAttribute.cs @@ -4,7 +4,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; /// Configures a datetime column inside a BM25 index. /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] -public sealed class Bm25DateTimeAttribute : Attribute { +public sealed class Bm25DateTimeAttribute : Attribute +{ public bool Fast { get; set; } public bool Indexed { get; set; } = true; } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/Bm25IndexAttribute.cs b/Equibles.ParadeDB.EntityFrameworkCore/Bm25IndexAttribute.cs index af6b637..b9c1bb9 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/Bm25IndexAttribute.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/Bm25IndexAttribute.cs @@ -1,12 +1,14 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] -public sealed class Bm25IndexAttribute : Attribute { +public sealed class Bm25IndexAttribute : Attribute +{ public string KeyField { get; } public string[] Columns { get; } - public Bm25IndexAttribute(string keyField, params string[] columns) { + public Bm25IndexAttribute(string keyField, params string[] columns) + { KeyField = keyField; - Columns = [keyField, ..columns]; + Columns = [keyField, .. columns]; } } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/Bm25JsonAttribute.cs b/Equibles.ParadeDB.EntityFrameworkCore/Bm25JsonAttribute.cs index 75fecf2..d4e429a 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/Bm25JsonAttribute.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/Bm25JsonAttribute.cs @@ -5,7 +5,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; /// settings as plus . /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] -public sealed class Bm25JsonAttribute : Attribute { +public sealed class Bm25JsonAttribute : Attribute +{ public Bm25Tokenizer Tokenizer { get; set; } /// Required when is . diff --git a/Equibles.ParadeDB.EntityFrameworkCore/Bm25Language.cs b/Equibles.ParadeDB.EntityFrameworkCore/Bm25Language.cs index a302443..f07b8dc 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/Bm25Language.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/Bm25Language.cs @@ -4,7 +4,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; /// Languages supported by pg_search's stemmer and stopwords token filters. /// means the property carries no language setting; the filter is not applied. /// -public enum Bm25Language { +public enum Bm25Language +{ Unspecified = 0, Arabic, Czech, diff --git a/Equibles.ParadeDB.EntityFrameworkCore/Bm25NumericAttribute.cs b/Equibles.ParadeDB.EntityFrameworkCore/Bm25NumericAttribute.cs index e1b0eb4..a1bc528 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/Bm25NumericAttribute.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/Bm25NumericAttribute.cs @@ -4,7 +4,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; /// Configures a numeric column inside a BM25 index. /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] -public sealed class Bm25NumericAttribute : Attribute { +public sealed class Bm25NumericAttribute : Attribute +{ public bool Fast { get; set; } public bool Indexed { get; set; } = true; } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/Bm25Record.cs b/Equibles.ParadeDB.EntityFrameworkCore/Bm25Record.cs index 01750d3..333958f 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/Bm25Record.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/Bm25Record.cs @@ -5,7 +5,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; /// Position is required for phrase queries; Basic and Freq use less disk. /// means the property carries no record setting; pg_search uses its default. /// -public enum Bm25Record { +public enum Bm25Record +{ Unspecified = 0, Basic, Freq, diff --git a/Equibles.ParadeDB.EntityFrameworkCore/Bm25TextAttribute.cs b/Equibles.ParadeDB.EntityFrameworkCore/Bm25TextAttribute.cs index 7309c5d..9dc527f 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/Bm25TextAttribute.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/Bm25TextAttribute.cs @@ -5,7 +5,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; /// in the entity's column set. /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] -public sealed class Bm25TextAttribute : Attribute { +public sealed class Bm25TextAttribute : Attribute +{ public Bm25Tokenizer Tokenizer { get; set; } /// Required when is . diff --git a/Equibles.ParadeDB.EntityFrameworkCore/Bm25Tokenizer.cs b/Equibles.ParadeDB.EntityFrameworkCore/Bm25Tokenizer.cs index 9329910..b866333 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/Bm25Tokenizer.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/Bm25Tokenizer.cs @@ -5,7 +5,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; /// Names map to pg_search tokenizer type strings. /// means the property carries no tokenizer setting; pg_search uses its default. /// -public enum Bm25Tokenizer { +public enum Bm25Tokenizer +{ Unspecified = 0, Default, Whitespace, diff --git a/Equibles.ParadeDB.EntityFrameworkCore/Equibles.ParadeDB.EntityFrameworkCore.csproj b/Equibles.ParadeDB.EntityFrameworkCore/Equibles.ParadeDB.EntityFrameworkCore.csproj index 7a2e7f9..ba75b06 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/Equibles.ParadeDB.EntityFrameworkCore.csproj +++ b/Equibles.ParadeDB.EntityFrameworkCore/Equibles.ParadeDB.EntityFrameworkCore.csproj @@ -1,27 +1,25 @@ + + net8.0;net9.0;net10.0 + EF Core integration for ParadeDB pg_search BM25 full-text search indexes on PostgreSQL. Provides a [Bm25Index] attribute, UseParadeDb() extension, and LINQ query methods for BM25 search, scoring, and snippets. + Equibles.ParadeDB.EntityFrameworkCore + + EF1001 + - - net8.0;net9.0;net10.0 - EF Core integration for ParadeDB pg_search BM25 full-text search indexes on PostgreSQL. Provides a [Bm25Index] attribute, UseParadeDb() extension, and LINQ query methods for BM25 search, scoring, and snippets. - Equibles.ParadeDB.EntityFrameworkCore - - EF1001 - + + + - - - + + + - - - - - - - - - - - + + + + + + diff --git a/Equibles.ParadeDB.EntityFrameworkCore/Internal/Bm25EnumExtensions.cs b/Equibles.ParadeDB.EntityFrameworkCore/Internal/Bm25EnumExtensions.cs index 30c0607..dfdc26e 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/Internal/Bm25EnumExtensions.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/Internal/Bm25EnumExtensions.cs @@ -1,54 +1,70 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.Internal; -internal static class Bm25EnumExtensions { - public static string ToParadeDbString(this Bm25Tokenizer tokenizer) => tokenizer switch { - Bm25Tokenizer.Unspecified => throw new ArgumentException("Bm25Tokenizer.Unspecified has no pg_search representation.", nameof(tokenizer)), - Bm25Tokenizer.Default => "default", - Bm25Tokenizer.Whitespace => "whitespace", - Bm25Tokenizer.Raw => "raw", - Bm25Tokenizer.Keyword => "keyword", - Bm25Tokenizer.SourceCode => "source_code", - Bm25Tokenizer.Icu => "icu", - Bm25Tokenizer.Ngram => "ngram", - Bm25Tokenizer.Regex => "regex", - Bm25Tokenizer.ChineseCompatible => "chinese_compatible", - Bm25Tokenizer.ChineseLindera => "chinese_lindera", - Bm25Tokenizer.JapaneseLindera => "japanese_lindera", - Bm25Tokenizer.KoreanLindera => "korean_lindera", - Bm25Tokenizer.Jieba => "jieba", - _ => throw new ArgumentOutOfRangeException(nameof(tokenizer), tokenizer, null), - }; +internal static class Bm25EnumExtensions +{ + public static string ToParadeDbString(this Bm25Tokenizer tokenizer) => + tokenizer switch + { + Bm25Tokenizer.Unspecified => throw new ArgumentException( + "Bm25Tokenizer.Unspecified has no pg_search representation.", + nameof(tokenizer) + ), + Bm25Tokenizer.Default => "default", + Bm25Tokenizer.Whitespace => "whitespace", + Bm25Tokenizer.Raw => "raw", + Bm25Tokenizer.Keyword => "keyword", + Bm25Tokenizer.SourceCode => "source_code", + Bm25Tokenizer.Icu => "icu", + Bm25Tokenizer.Ngram => "ngram", + Bm25Tokenizer.Regex => "regex", + Bm25Tokenizer.ChineseCompatible => "chinese_compatible", + Bm25Tokenizer.ChineseLindera => "chinese_lindera", + Bm25Tokenizer.JapaneseLindera => "japanese_lindera", + Bm25Tokenizer.KoreanLindera => "korean_lindera", + Bm25Tokenizer.Jieba => "jieba", + _ => throw new ArgumentOutOfRangeException(nameof(tokenizer), tokenizer, null), + }; - public static string ToParadeDbString(this Bm25Language language) => language switch { - Bm25Language.Unspecified => throw new ArgumentException("Bm25Language.Unspecified has no pg_search representation.", nameof(language)), - Bm25Language.Arabic => "Arabic", - Bm25Language.Czech => "Czech", - Bm25Language.Danish => "Danish", - Bm25Language.Dutch => "Dutch", - Bm25Language.English => "English", - Bm25Language.Finnish => "Finnish", - Bm25Language.French => "French", - Bm25Language.German => "German", - Bm25Language.Greek => "Greek", - Bm25Language.Hungarian => "Hungarian", - Bm25Language.Italian => "Italian", - Bm25Language.Norwegian => "Norwegian", - Bm25Language.Polish => "Polish", - Bm25Language.Portuguese => "Portuguese", - Bm25Language.Romanian => "Romanian", - Bm25Language.Russian => "Russian", - Bm25Language.Spanish => "Spanish", - Bm25Language.Swedish => "Swedish", - Bm25Language.Tamil => "Tamil", - Bm25Language.Turkish => "Turkish", - _ => throw new ArgumentOutOfRangeException(nameof(language), language, null), - }; + public static string ToParadeDbString(this Bm25Language language) => + language switch + { + Bm25Language.Unspecified => throw new ArgumentException( + "Bm25Language.Unspecified has no pg_search representation.", + nameof(language) + ), + Bm25Language.Arabic => "Arabic", + Bm25Language.Czech => "Czech", + Bm25Language.Danish => "Danish", + Bm25Language.Dutch => "Dutch", + Bm25Language.English => "English", + Bm25Language.Finnish => "Finnish", + Bm25Language.French => "French", + Bm25Language.German => "German", + Bm25Language.Greek => "Greek", + Bm25Language.Hungarian => "Hungarian", + Bm25Language.Italian => "Italian", + Bm25Language.Norwegian => "Norwegian", + Bm25Language.Polish => "Polish", + Bm25Language.Portuguese => "Portuguese", + Bm25Language.Romanian => "Romanian", + Bm25Language.Russian => "Russian", + Bm25Language.Spanish => "Spanish", + Bm25Language.Swedish => "Swedish", + Bm25Language.Tamil => "Tamil", + Bm25Language.Turkish => "Turkish", + _ => throw new ArgumentOutOfRangeException(nameof(language), language, null), + }; - public static string ToParadeDbString(this Bm25Record record) => record switch { - Bm25Record.Unspecified => throw new ArgumentException("Bm25Record.Unspecified has no pg_search representation.", nameof(record)), - Bm25Record.Basic => "basic", - Bm25Record.Freq => "freq", - Bm25Record.Position => "position", - _ => throw new ArgumentOutOfRangeException(nameof(record), record, null), - }; + public static string ToParadeDbString(this Bm25Record record) => + record switch + { + Bm25Record.Unspecified => throw new ArgumentException( + "Bm25Record.Unspecified has no pg_search representation.", + nameof(record) + ), + Bm25Record.Basic => "basic", + Bm25Record.Freq => "freq", + Bm25Record.Position => "position", + _ => throw new ArgumentOutOfRangeException(nameof(record), record, null), + }; } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/Internal/Bm25StorageParameterBuilder.cs b/Equibles.ParadeDB.EntityFrameworkCore/Internal/Bm25StorageParameterBuilder.cs index 248451d..4376a63 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/Internal/Bm25StorageParameterBuilder.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/Internal/Bm25StorageParameterBuilder.cs @@ -2,117 +2,189 @@ namespace Equibles.ParadeDB.EntityFrameworkCore.Internal; -internal static class Bm25StorageParameterBuilder { +internal static class Bm25StorageParameterBuilder +{ public static string Serialize(JsonObject root) => root.ToJsonString(new System.Text.Json.JsonSerializerOptions { WriteIndented = false }); - public static JsonObject BuildTextField(Bm25TextAttribute attr, string propertyName) { + public static JsonObject BuildTextField(Bm25TextAttribute attr, string propertyName) + { ValidateTokenizerParameters( propertyName, attr.Tokenizer, - attr.MinGram, attr.MaxGram, attr.PrefixOnly, - attr.RegexPattern); + attr.MinGram, + attr.MaxGram, + attr.PrefixOnly, + attr.RegexPattern + ); var node = new JsonObject(); - AddTokenizerKey(node, attr.Tokenizer, attr.MinGram, attr.MaxGram, attr.PrefixOnly, attr.RegexPattern, - attr.Stemmer, attr.StopwordsLanguage); + AddTokenizerKey( + node, + attr.Tokenizer, + attr.MinGram, + attr.MaxGram, + attr.PrefixOnly, + attr.RegexPattern, + attr.Stemmer, + attr.StopwordsLanguage + ); AddSharedKeys(node, attr.Fast, attr.Record, attr.Indexed, attr.Fieldnorms); return node; } - public static JsonObject BuildNumericField(Bm25NumericAttribute attr) { + public static JsonObject BuildNumericField(Bm25NumericAttribute attr) + { var node = new JsonObject(); - if (attr.Fast) node["fast"] = true; - if (!attr.Indexed) node["indexed"] = false; + if (attr.Fast) + node["fast"] = true; + if (!attr.Indexed) + node["indexed"] = false; return node; } - public static JsonObject BuildBooleanField(Bm25BooleanAttribute attr) { + public static JsonObject BuildBooleanField(Bm25BooleanAttribute attr) + { var node = new JsonObject(); - if (attr.Fast) node["fast"] = true; - if (!attr.Indexed) node["indexed"] = false; + if (attr.Fast) + node["fast"] = true; + if (!attr.Indexed) + node["indexed"] = false; return node; } - public static JsonObject BuildDateTimeField(Bm25DateTimeAttribute attr) { + public static JsonObject BuildDateTimeField(Bm25DateTimeAttribute attr) + { var node = new JsonObject(); - if (attr.Fast) node["fast"] = true; - if (!attr.Indexed) node["indexed"] = false; + if (attr.Fast) + node["fast"] = true; + if (!attr.Indexed) + node["indexed"] = false; return node; } - public static JsonObject BuildJsonField(Bm25JsonAttribute attr, string propertyName) { + public static JsonObject BuildJsonField(Bm25JsonAttribute attr, string propertyName) + { ValidateTokenizerParameters( propertyName, attr.Tokenizer, - attr.MinGram, attr.MaxGram, attr.PrefixOnly, - attr.RegexPattern); + attr.MinGram, + attr.MaxGram, + attr.PrefixOnly, + attr.RegexPattern + ); var node = new JsonObject(); - AddTokenizerKey(node, attr.Tokenizer, attr.MinGram, attr.MaxGram, attr.PrefixOnly, attr.RegexPattern, - attr.Stemmer, attr.StopwordsLanguage); + AddTokenizerKey( + node, + attr.Tokenizer, + attr.MinGram, + attr.MaxGram, + attr.PrefixOnly, + attr.RegexPattern, + attr.Stemmer, + attr.StopwordsLanguage + ); AddSharedKeys(node, attr.Fast, attr.Record, attr.Indexed, attr.Fieldnorms); - if (attr.ExpandDots) node["expand_dots"] = true; + if (attr.ExpandDots) + node["expand_dots"] = true; return node; } - private static void AddTokenizerKey(JsonObject node, Bm25Tokenizer tokenizer, - int minGram, int maxGram, bool prefixOnly, string regexPattern, - Bm25Language stemmer, Bm25Language stopwordsLanguage) { - var hasAnySetting = tokenizer != Bm25Tokenizer.Unspecified + private static void AddTokenizerKey( + JsonObject node, + Bm25Tokenizer tokenizer, + int minGram, + int maxGram, + bool prefixOnly, + string regexPattern, + Bm25Language stemmer, + Bm25Language stopwordsLanguage + ) + { + var hasAnySetting = + tokenizer != Bm25Tokenizer.Unspecified || stemmer != Bm25Language.Unspecified || stopwordsLanguage != Bm25Language.Unspecified; - if (!hasAnySetting) return; + if (!hasAnySetting) + return; - var effectiveTokenizer = tokenizer == Bm25Tokenizer.Unspecified - ? Bm25Tokenizer.Default - : tokenizer; + var effectiveTokenizer = + tokenizer == Bm25Tokenizer.Unspecified ? Bm25Tokenizer.Default : tokenizer; - var tok = new JsonObject { - ["type"] = effectiveTokenizer.ToParadeDbString(), - }; + var tok = new JsonObject { ["type"] = effectiveTokenizer.ToParadeDbString() }; - if (effectiveTokenizer == Bm25Tokenizer.Ngram) { + if (effectiveTokenizer == Bm25Tokenizer.Ngram) + { tok["min_gram"] = minGram; tok["max_gram"] = maxGram; tok["prefix_only"] = prefixOnly; } - if (effectiveTokenizer == Bm25Tokenizer.Regex) { + if (effectiveTokenizer == Bm25Tokenizer.Regex) + { tok["pattern"] = regexPattern; } - if (stemmer != Bm25Language.Unspecified) tok["stemmer"] = stemmer.ToParadeDbString(); - if (stopwordsLanguage != Bm25Language.Unspecified) tok["stopwords_language"] = stopwordsLanguage.ToParadeDbString(); + if (stemmer != Bm25Language.Unspecified) + tok["stemmer"] = stemmer.ToParadeDbString(); + if (stopwordsLanguage != Bm25Language.Unspecified) + tok["stopwords_language"] = stopwordsLanguage.ToParadeDbString(); node["tokenizer"] = tok; } - private static void AddSharedKeys(JsonObject node, bool fast, Bm25Record record, bool indexed, bool fieldnorms) { - if (fast) node["fast"] = true; - if (record != Bm25Record.Unspecified) node["record"] = record.ToParadeDbString(); - if (!indexed) node["indexed"] = false; - if (!fieldnorms) node["fieldnorms"] = false; + private static void AddSharedKeys( + JsonObject node, + bool fast, + Bm25Record record, + bool indexed, + bool fieldnorms + ) + { + if (fast) + node["fast"] = true; + if (record != Bm25Record.Unspecified) + node["record"] = record.ToParadeDbString(); + if (!indexed) + node["indexed"] = false; + if (!fieldnorms) + node["fieldnorms"] = false; } - private static void ValidateTokenizerParameters(string propertyName, Bm25Tokenizer tokenizer, - int minGram, int maxGram, bool prefixOnly, string regexPattern) { + private static void ValidateTokenizerParameters( + string propertyName, + Bm25Tokenizer tokenizer, + int minGram, + int maxGram, + bool prefixOnly, + string regexPattern + ) + { var hasNgramParam = minGram != 0 || maxGram != 0 || prefixOnly; var hasRegexParam = regexPattern is not null; - if (hasNgramParam && tokenizer != Bm25Tokenizer.Ngram) { + if (hasNgramParam && tokenizer != Bm25Tokenizer.Ngram) + { throw new InvalidOperationException( - $"Property '{propertyName}': MinGram/MaxGram/PrefixOnly require Tokenizer = Bm25Tokenizer.Ngram."); + $"Property '{propertyName}': MinGram/MaxGram/PrefixOnly require Tokenizer = Bm25Tokenizer.Ngram." + ); } - if (tokenizer == Bm25Tokenizer.Ngram && (minGram == 0 || maxGram == 0)) { + if (tokenizer == Bm25Tokenizer.Ngram && (minGram == 0 || maxGram == 0)) + { throw new InvalidOperationException( - $"Property '{propertyName}': Tokenizer = Bm25Tokenizer.Ngram requires both MinGram and MaxGram (> 0)."); + $"Property '{propertyName}': Tokenizer = Bm25Tokenizer.Ngram requires both MinGram and MaxGram (> 0)." + ); } - if (hasRegexParam && tokenizer != Bm25Tokenizer.Regex) { + if (hasRegexParam && tokenizer != Bm25Tokenizer.Regex) + { throw new InvalidOperationException( - $"Property '{propertyName}': RegexPattern requires Tokenizer = Bm25Tokenizer.Regex."); + $"Property '{propertyName}': RegexPattern requires Tokenizer = Bm25Tokenizer.Regex." + ); } - if (tokenizer == Bm25Tokenizer.Regex && !hasRegexParam) { + if (tokenizer == Bm25Tokenizer.Regex && !hasRegexParam) + { throw new InvalidOperationException( - $"Property '{propertyName}': Tokenizer = Bm25Tokenizer.Regex requires a RegexPattern."); + $"Property '{propertyName}': Tokenizer = Bm25Tokenizer.Regex requires a RegexPattern." + ); } } } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbBooleanQuery.cs b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbBooleanQuery.cs index 05aa9db..4bbfb53 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbBooleanQuery.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbBooleanQuery.cs @@ -5,40 +5,50 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; /// /// Mutable builder for boolean query clauses used within . /// -public sealed class ParadeDbBooleanQuery { +public sealed class ParadeDbBooleanQuery +{ internal List MustClauses { get; } = []; internal List ShouldClauses { get; } = []; internal List MustNotClauses { get; } = []; /// Adds must (AND) clauses. - public ParadeDbBooleanQuery Must(params ParadeDbJsonQuery[] queries) { + public ParadeDbBooleanQuery Must(params ParadeDbJsonQuery[] queries) + { MustClauses.AddRange(queries); return this; } /// Adds should (OR) clauses. - public ParadeDbBooleanQuery Should(params ParadeDbJsonQuery[] queries) { + public ParadeDbBooleanQuery Should(params ParadeDbJsonQuery[] queries) + { ShouldClauses.AddRange(queries); return this; } /// Adds must_not (NOT) clauses. - public ParadeDbBooleanQuery MustNot(params ParadeDbJsonQuery[] queries) { + public ParadeDbBooleanQuery MustNot(params ParadeDbJsonQuery[] queries) + { MustNotClauses.AddRange(queries); return this; } - internal JsonNode ToJsonNode() { + internal JsonNode ToJsonNode() + { var inner = new JsonObject(); - if (MustClauses.Count > 0) inner["must"] = ToArray(MustClauses); - if (ShouldClauses.Count > 0) inner["should"] = ToArray(ShouldClauses); - if (MustNotClauses.Count > 0) inner["must_not"] = ToArray(MustNotClauses); + if (MustClauses.Count > 0) + inner["must"] = ToArray(MustClauses); + if (ShouldClauses.Count > 0) + inner["should"] = ToArray(ShouldClauses); + if (MustNotClauses.Count > 0) + inner["must_not"] = ToArray(MustNotClauses); return new JsonObject { ["boolean"] = inner }; } - private static JsonArray ToArray(List queries) { + private static JsonArray ToArray(List queries) + { var arr = new JsonArray(); - foreach (var q in queries) arr.Add(q.CloneNode()); + foreach (var q in queries) + arr.Add(q.CloneNode()); return arr; } } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbConventionSetPlugin.cs b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbConventionSetPlugin.cs index 4bb885e..db32c50 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbConventionSetPlugin.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbConventionSetPlugin.cs @@ -3,8 +3,10 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; -public sealed class ParadeDbConventionSetPlugin : IConventionSetPlugin { - public ConventionSet ModifyConventions(ConventionSet conventionSet) { +public sealed class ParadeDbConventionSetPlugin : IConventionSetPlugin +{ + public ConventionSet ModifyConventions(ConventionSet conventionSet) + { conventionSet.ModelFinalizingConventions.Add(new ParadeDbModelFinalizingConvention()); return conventionSet; } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbDbContextOptionsBuilderExtensions.cs b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbDbContextOptionsBuilderExtensions.cs index 29639c4..e30cedf 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbDbContextOptionsBuilderExtensions.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbDbContextOptionsBuilderExtensions.cs @@ -5,17 +5,27 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; -public static class ParadeDbDbContextOptionsBuilderExtensions { - public static NpgsqlDbContextOptionsBuilder UseParadeDb(this NpgsqlDbContextOptionsBuilder npgsqlBuilder) { - var builder = ((IRelationalDbContextOptionsBuilderInfrastructure)npgsqlBuilder).OptionsBuilder; +public static class ParadeDbDbContextOptionsBuilderExtensions +{ + public static NpgsqlDbContextOptionsBuilder UseParadeDb( + this NpgsqlDbContextOptionsBuilder npgsqlBuilder + ) + { + var builder = ( + (IRelationalDbContextOptionsBuilderInfrastructure)npgsqlBuilder + ).OptionsBuilder; - var extension = builder.Options.FindExtension() - ?? new ParadeDbDbContextOptionsExtension(); + var extension = + builder.Options.FindExtension() + ?? new ParadeDbDbContextOptionsExtension(); ((IDbContextOptionsBuilderInfrastructure)builder).AddOrUpdateExtension(extension); builder.ReplaceService(); - builder.ReplaceService(); + builder.ReplaceService< + IRelationalParameterBasedSqlProcessorFactory, + ParadeDbParameterBasedSqlProcessorFactory + >(); return npgsqlBuilder; } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbDbContextOptionsExtension.cs b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbDbContextOptionsExtension.cs index 3ed6e65..89b784e 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbDbContextOptionsExtension.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbDbContextOptionsExtension.cs @@ -5,10 +5,12 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; -public sealed class ParadeDbDbContextOptionsExtension : IDbContextOptionsExtension { +public sealed class ParadeDbDbContextOptionsExtension : IDbContextOptionsExtension +{ public DbContextOptionsExtensionInfo Info => new ParadeDbExtensionInfo(this); - public void ApplyServices(IServiceCollection services) { + public void ApplyServices(IServiceCollection services) + { new EntityFrameworkRelationalServicesBuilder(services) .TryAdd() .TryAdd(); @@ -16,13 +18,20 @@ public void ApplyServices(IServiceCollection services) { public void Validate(IDbContextOptions options) { } - private sealed class ParadeDbExtensionInfo : DbContextOptionsExtensionInfo { - public ParadeDbExtensionInfo(IDbContextOptionsExtension extension) : base(extension) { } + private sealed class ParadeDbExtensionInfo : DbContextOptionsExtensionInfo + { + public ParadeDbExtensionInfo(IDbContextOptionsExtension extension) + : base(extension) { } public override bool IsDatabaseProvider => false; public override string LogFragment => "using ParadeDB "; + public override int GetServiceProviderHashCode() => 0; - public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => other is ParadeDbExtensionInfo; - public override void PopulateDebugInfo(IDictionary debugInfo) => debugInfo["ParadeDB:BM25"] = "1"; + + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => + other is ParadeDbExtensionInfo; + + public override void PopulateDebugInfo(IDictionary debugInfo) => + debugInfo["ParadeDB:BM25"] = "1"; } } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbFunctions.cs b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbFunctions.cs index 4bf1acb..243f19c 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbFunctions.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbFunctions.cs @@ -6,7 +6,8 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; /// Provides CLR methods that translate to ParadeDB pg_search SQL operators and functions. /// These methods can only be used in EF Core LINQ queries — they have no in-memory implementation. /// -public static class ParadeDbFunctions { +public static class ParadeDbFunctions +{ private const string OnlyInLinq = "This method can only be used in EF Core LINQ queries."; // ── Basic Search ────────────────────────────────────────────────── @@ -15,15 +16,15 @@ public static class ParadeDbFunctions { /// BM25 disjunction match (OR). Translates to: column ||| 'query'. /// Matches documents containing any of the query terms. /// - public static bool Matches(this DbFunctions _, string column, string query) - => throw new InvalidOperationException(OnlyInLinq); + public static bool Matches(this DbFunctions _, string column, string query) => + throw new InvalidOperationException(OnlyInLinq); /// /// BM25 conjunction match (AND). Translates to: column &&& 'query'. /// Matches documents containing all of the query terms. /// - public static bool MatchesAll(this DbFunctions _, string column, string query) - => throw new InvalidOperationException(OnlyInLinq); + public static bool MatchesAll(this DbFunctions _, string column, string query) => + throw new InvalidOperationException(OnlyInLinq); // ── Phrase Search ───────────────────────────────────────────────── @@ -31,15 +32,15 @@ public static bool MatchesAll(this DbFunctions _, string column, string query) /// Phrase match. Translates to: column ### 'query'. /// Matches terms in exact order. /// - public static bool MatchesPhrase(this DbFunctions _, string column, string query) - => throw new InvalidOperationException(OnlyInLinq); + public static bool MatchesPhrase(this DbFunctions _, string column, string query) => + throw new InvalidOperationException(OnlyInLinq); /// /// Phrase match with slop. Translates to: column ### 'query'::pdb.slop(N). /// Allows N words between terms or transposition of adjacent terms. /// - public static bool MatchesPhrase(this DbFunctions _, string column, string query, int slop) - => throw new InvalidOperationException(OnlyInLinq); + public static bool MatchesPhrase(this DbFunctions _, string column, string query, int slop) => + throw new InvalidOperationException(OnlyInLinq); // ── Term Search ─────────────────────────────────────────────────── @@ -47,15 +48,15 @@ public static bool MatchesPhrase(this DbFunctions _, string column, string query /// Exact term match. Translates to: column === 'query'. /// The query is NOT tokenized — no stemming or lowercasing is applied. /// - public static bool MatchesTerm(this DbFunctions _, string column, string query) - => throw new InvalidOperationException(OnlyInLinq); + public static bool MatchesTerm(this DbFunctions _, string column, string query) => + throw new InvalidOperationException(OnlyInLinq); /// /// Multi-term match. Translates to: column === ARRAY['a', 'b']. /// Matches any of the exact terms. /// - public static bool MatchesTermSet(this DbFunctions _, string column, params string[] terms) - => throw new InvalidOperationException(OnlyInLinq); + public static bool MatchesTermSet(this DbFunctions _, string column, params string[] terms) => + throw new InvalidOperationException(OnlyInLinq); // ── Fuzzy Search (Levenshtein Distance) ─────────────────────────── @@ -63,43 +64,70 @@ public static bool MatchesTermSet(this DbFunctions _, string column, params stri /// Fuzzy OR match. Translates to: column ||| 'query'::pdb.fuzzy(distance). /// Tolerates typos by allowing up to N single-character edits. Max distance is 2. /// - public static bool MatchesFuzzy(this DbFunctions _, string column, string query, int distance) - => throw new InvalidOperationException(OnlyInLinq); + public static bool MatchesFuzzy( + this DbFunctions _, + string column, + string query, + int distance + ) => throw new InvalidOperationException(OnlyInLinq); /// /// Fuzzy OR match with options. Translates to: column @@@ pdb.match('query', distance => D, transposition_cost_one => T, prefix => P). /// exempts the initial substring from edit distance. /// counts swapping two adjacent characters as one edit. /// - public static bool MatchesFuzzy(this DbFunctions _, string column, string query, int distance, - bool prefix, bool transpositionCostOne) - => throw new InvalidOperationException(OnlyInLinq); + public static bool MatchesFuzzy( + this DbFunctions _, + string column, + string query, + int distance, + bool prefix, + bool transpositionCostOne + ) => throw new InvalidOperationException(OnlyInLinq); /// /// Fuzzy AND match. Translates to: column &&& 'query'::pdb.fuzzy(distance). /// - public static bool MatchesAllFuzzy(this DbFunctions _, string column, string query, int distance) - => throw new InvalidOperationException(OnlyInLinq); + public static bool MatchesAllFuzzy( + this DbFunctions _, + string column, + string query, + int distance + ) => throw new InvalidOperationException(OnlyInLinq); /// /// Fuzzy AND match with options. Translates to: column @@@ pdb.match('query', distance => D, transposition_cost_one => T, prefix => P, conjunction_mode => true). /// - public static bool MatchesAllFuzzy(this DbFunctions _, string column, string query, int distance, - bool prefix, bool transpositionCostOne) - => throw new InvalidOperationException(OnlyInLinq); + public static bool MatchesAllFuzzy( + this DbFunctions _, + string column, + string query, + int distance, + bool prefix, + bool transpositionCostOne + ) => throw new InvalidOperationException(OnlyInLinq); /// /// Fuzzy term match. Translates to: column === 'query'::pdb.fuzzy(distance). /// - public static bool MatchesTermFuzzy(this DbFunctions _, string column, string query, int distance) - => throw new InvalidOperationException(OnlyInLinq); + public static bool MatchesTermFuzzy( + this DbFunctions _, + string column, + string query, + int distance + ) => throw new InvalidOperationException(OnlyInLinq); /// /// Fuzzy term match with options. Translates to: column @@@ pdb.fuzzy_term('query', distance, transposition_cost_one, prefix). /// - public static bool MatchesTermFuzzy(this DbFunctions _, string column, string query, int distance, - bool prefix, bool transpositionCostOne) - => throw new InvalidOperationException(OnlyInLinq); + public static bool MatchesTermFuzzy( + this DbFunctions _, + string column, + string query, + int distance, + bool prefix, + bool transpositionCostOne + ) => throw new InvalidOperationException(OnlyInLinq); // ── Boost ───────────────────────────────────────────────────────── @@ -107,28 +135,46 @@ public static bool MatchesTermFuzzy(this DbFunctions _, string column, string qu /// Boosted OR match. Translates to: column ||| 'query'::pdb.boost(factor). /// Increases the BM25 relevance weight. Factor range: -2048 to 2048. /// - public static bool MatchesBoosted(this DbFunctions _, string column, string query, double boost) - => throw new InvalidOperationException(OnlyInLinq); + public static bool MatchesBoosted( + this DbFunctions _, + string column, + string query, + double boost + ) => throw new InvalidOperationException(OnlyInLinq); /// /// Boosted AND match. Translates to: column &&& 'query'::pdb.boost(factor). /// - public static bool MatchesAllBoosted(this DbFunctions _, string column, string query, double boost) - => throw new InvalidOperationException(OnlyInLinq); + public static bool MatchesAllBoosted( + this DbFunctions _, + string column, + string query, + double boost + ) => throw new InvalidOperationException(OnlyInLinq); // ── Fuzzy + Boost Combined ──────────────────────────────────────── /// /// Fuzzy OR match with boost. Translates to: column ||| 'query'::pdb.fuzzy(distance)::pdb.boost(factor). /// - public static bool MatchesFuzzyBoosted(this DbFunctions _, string column, string query, int distance, double boost) - => throw new InvalidOperationException(OnlyInLinq); + public static bool MatchesFuzzyBoosted( + this DbFunctions _, + string column, + string query, + int distance, + double boost + ) => throw new InvalidOperationException(OnlyInLinq); /// /// Fuzzy AND match with boost. Translates to: column &&& 'query'::pdb.fuzzy(distance)::pdb.boost(factor). /// - public static bool MatchesAllFuzzyBoosted(this DbFunctions _, string column, string query, int distance, double boost) - => throw new InvalidOperationException(OnlyInLinq); + public static bool MatchesAllFuzzyBoosted( + this DbFunctions _, + string column, + string query, + int distance, + double boost + ) => throw new InvalidOperationException(OnlyInLinq); // ── BM25 Scoring ────────────────────────────────────────────────── @@ -136,8 +182,8 @@ public static bool MatchesAllFuzzyBoosted(this DbFunctions _, string column, str /// BM25 relevance score. Translates to: pdb.score(key_field). /// Returns the BM25 relevance score for the matched document. /// - public static double Score(this DbFunctions _, object keyField) - => throw new InvalidOperationException(OnlyInLinq); + public static double Score(this DbFunctions _, object keyField) => + throw new InvalidOperationException(OnlyInLinq); // ── Snippets ────────────────────────────────────────────────────── @@ -145,21 +191,31 @@ public static double Score(this DbFunctions _, object keyField) /// Basic snippet. Translates to: pdb.snippet(column). /// Returns a text excerpt with matched terms highlighted. /// - public static string Snippet(this DbFunctions _, string column) - => throw new InvalidOperationException(OnlyInLinq); + public static string Snippet(this DbFunctions _, string column) => + throw new InvalidOperationException(OnlyInLinq); /// /// Parameterized snippet. Translates to: pdb.snippet(column, start_tag => '...', end_tag => '...', max_num_chars => N). /// - public static string Snippet(this DbFunctions _, string column, string startTag, string endTag, int maxNumChars) - => throw new InvalidOperationException(OnlyInLinq); + public static string Snippet( + this DbFunctions _, + string column, + string startTag, + string endTag, + int maxNumChars + ) => throw new InvalidOperationException(OnlyInLinq); /// /// Multiple snippets. Translates to: pdb.snippets(column, max_num_chars => N, "limit" => L, "offset" => O). /// Returns a text[] of highlighted excerpts. /// - public static string[] Snippets(this DbFunctions _, string column, int maxNumChars, int limit, int offset) - => throw new InvalidOperationException(OnlyInLinq); + public static string[] Snippets( + this DbFunctions _, + string column, + int maxNumChars, + int limit, + int offset + ) => throw new InvalidOperationException(OnlyInLinq); // ── Parse Query (Tantivy Syntax) ────────────────────────────────── @@ -167,15 +223,20 @@ public static string[] Snippets(this DbFunctions _, string column, int maxNumCha /// Parse query. Translates to: key @@@ pdb.parse('query'). /// Full query parser supporting field:value, boolean operators, ranges, and wildcards. /// - public static bool Parse(this DbFunctions _, object keyField, string query) - => throw new InvalidOperationException(OnlyInLinq); + public static bool Parse(this DbFunctions _, object keyField, string query) => + throw new InvalidOperationException(OnlyInLinq); /// /// Parse query with options. Translates to: key @@@ pdb.parse('query', lenient => true, conjunction_mode => true). /// : ignores syntax errors. : defaults terms to AND. /// - public static bool Parse(this DbFunctions _, object keyField, string query, bool lenient, bool conjunctionMode) - => throw new InvalidOperationException(OnlyInLinq); + public static bool Parse( + this DbFunctions _, + object keyField, + string query, + bool lenient, + bool conjunctionMode + ) => throw new InvalidOperationException(OnlyInLinq); // ── Regex Search ────────────────────────────────────────────────── @@ -183,8 +244,8 @@ public static bool Parse(this DbFunctions _, object keyField, string query, bool /// Regex match. Translates to: column @@@ pdb.regex('pattern'). /// Matches indexed tokens against a regular expression (Rust regex syntax). /// - public static bool Regex(this DbFunctions _, string column, string pattern) - => throw new InvalidOperationException(OnlyInLinq); + public static bool Regex(this DbFunctions _, string column, string pattern) => + throw new InvalidOperationException(OnlyInLinq); // ── Phrase Prefix ───────────────────────────────────────────────── @@ -192,14 +253,18 @@ public static bool Regex(this DbFunctions _, string column, string pattern) /// Phrase prefix match. Translates to: column @@@ pdb.phrase_prefix(ARRAY['term1', 'term2']). /// The last term is treated as a prefix — useful for autocomplete/type-ahead. /// - public static bool PhrasePrefix(this DbFunctions _, string column, params string[] terms) - => throw new InvalidOperationException(OnlyInLinq); + public static bool PhrasePrefix(this DbFunctions _, string column, params string[] terms) => + throw new InvalidOperationException(OnlyInLinq); /// /// Phrase prefix match with max expansions. Translates to: column @@@ pdb.phrase_prefix(ARRAY[...], max_expansion => N). /// - public static bool PhrasePrefix(this DbFunctions _, string column, int maxExpansions, params string[] terms) - => throw new InvalidOperationException(OnlyInLinq); + public static bool PhrasePrefix( + this DbFunctions _, + string column, + int maxExpansions, + params string[] terms + ) => throw new InvalidOperationException(OnlyInLinq); // ── More Like This ──────────────────────────────────────────────── @@ -207,14 +272,18 @@ public static bool PhrasePrefix(this DbFunctions _, string column, int maxExpans /// More-like-this search. Translates to: key @@@ pdb.more_like_this(documentId). /// Finds documents similar to a given document. /// - public static bool MoreLikeThis(this DbFunctions _, object keyField, int documentId) - => throw new InvalidOperationException(OnlyInLinq); + public static bool MoreLikeThis(this DbFunctions _, object keyField, int documentId) => + throw new InvalidOperationException(OnlyInLinq); /// /// More-like-this with field restriction. Translates to: key @@@ pdb.more_like_this(documentId, ARRAY['field1', ...]). /// - public static bool MoreLikeThis(this DbFunctions _, object keyField, int documentId, params string[] fields) - => throw new InvalidOperationException(OnlyInLinq); + public static bool MoreLikeThis( + this DbFunctions _, + object keyField, + int documentId, + params string[] fields + ) => throw new InvalidOperationException(OnlyInLinq); // ── JSON Query Search ──────────────────────────────────────────── @@ -223,6 +292,6 @@ public static bool MoreLikeThis(this DbFunctions _, object keyField, int documen /// Executes a structured query using ParadeDB's JSON query syntax. /// Build the query string using . /// - public static bool JsonSearch(this DbFunctions _, object keyField, string jsonQuery) - => throw new InvalidOperationException(OnlyInLinq); + public static bool JsonSearch(this DbFunctions _, object keyField, string jsonQuery) => + throw new InvalidOperationException(OnlyInLinq); } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbJsonQuery.cs b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbJsonQuery.cs index 0904add..ef18e67 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbJsonQuery.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbJsonQuery.cs @@ -6,17 +6,18 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; /// Builds a ParadeDB JSON query for use with the @@@ operator. /// Use static factory methods to create query nodes, then call to serialize. /// -public sealed class ParadeDbJsonQuery { +public sealed class ParadeDbJsonQuery +{ private readonly JsonNode _node; - internal ParadeDbJsonQuery(JsonNode node) { + internal ParadeDbJsonQuery(JsonNode node) + { _node = node; } /// Serializes the query to a compact JSON string. - public string ToJson() => _node.ToJsonString(new System.Text.Json.JsonSerializerOptions { - WriteIndented = false - }); + public string ToJson() => + _node.ToJsonString(new System.Text.Json.JsonSerializerOptions { WriteIndented = false }); /// public override string ToString() => ToJson(); @@ -35,11 +36,13 @@ public static ParadeDbJsonQuery Parse(string queryString) => /// /// Parse query with options. Produces: {"parse":{"query_string":"...","lenient":true,"conjunction_mode":true}}. /// - public static ParadeDbJsonQuery Parse(string queryString, bool lenient, bool conjunctionMode) { - var inner = new JsonObject { + public static ParadeDbJsonQuery Parse(string queryString, bool lenient, bool conjunctionMode) + { + var inner = new JsonObject + { ["query_string"] = queryString, ["lenient"] = lenient, - ["conjunction_mode"] = conjunctionMode + ["conjunction_mode"] = conjunctionMode, }; return new(new JsonObject { ["parse"] = inner }); } @@ -50,17 +53,29 @@ public static ParadeDbJsonQuery Parse(string queryString, bool lenient, bool con /// Exact term match. Produces: {"term":{"field":"...","value":...}}. /// public static ParadeDbJsonQuery Term(string field, object value) => - new(new JsonObject { ["term"] = new JsonObject { ["field"] = field, ["value"] = CreateJsonValue(value) } }); + new( + new JsonObject + { + ["term"] = new JsonObject { ["field"] = field, ["value"] = CreateJsonValue(value) }, + } + ); // ── Term Set ───────────────────────────────────────────────────── /// /// Multi-term match. Produces: {"term_set":{"field":"...","terms":[...]}}. /// - public static ParadeDbJsonQuery TermSet(string field, params object[] terms) { + public static ParadeDbJsonQuery TermSet(string field, params object[] terms) + { var arr = new JsonArray(); - foreach (var t in terms) arr.Add(CreateJsonValue(t)); - return new(new JsonObject { ["term_set"] = new JsonObject { ["field"] = field, ["terms"] = arr } }); + foreach (var t in terms) + arr.Add(CreateJsonValue(t)); + return new( + new JsonObject + { + ["term_set"] = new JsonObject { ["field"] = field, ["terms"] = arr }, + } + ); } // ── Match ──────────────────────────────────────────────────────── @@ -69,17 +84,29 @@ public static ParadeDbJsonQuery TermSet(string field, params object[] terms) { /// Match query with field. Produces: {"match":{"field":"...","value":"..."}}. /// public static ParadeDbJsonQuery Match(string value, string field) => - new(new JsonObject { ["match"] = new JsonObject { ["field"] = field, ["value"] = value } }); + new( + new JsonObject + { + ["match"] = new JsonObject { ["field"] = field, ["value"] = value }, + } + ); /// /// Match query with field and options. /// - public static ParadeDbJsonQuery Match(string value, string field, int distance, bool conjunctionMode) { - var inner = new JsonObject { + public static ParadeDbJsonQuery Match( + string value, + string field, + int distance, + bool conjunctionMode + ) + { + var inner = new JsonObject + { ["field"] = field, ["value"] = value, ["distance"] = distance, - ["conjunction_mode"] = conjunctionMode + ["conjunction_mode"] = conjunctionMode, }; return new(new JsonObject { ["match"] = inner }); } @@ -90,18 +117,36 @@ public static ParadeDbJsonQuery Match(string value, string field, int distance, /// Fuzzy term match. Produces: {"fuzzy_term":{"field":"...","value":"...","distance":N}}. /// public static ParadeDbJsonQuery FuzzyTerm(string field, string value, int distance) => - new(new JsonObject { ["fuzzy_term"] = new JsonObject { ["field"] = field, ["value"] = value, ["distance"] = distance } }); + new( + new JsonObject + { + ["fuzzy_term"] = new JsonObject + { + ["field"] = field, + ["value"] = value, + ["distance"] = distance, + }, + } + ); /// /// Fuzzy term match with full options. /// - public static ParadeDbJsonQuery FuzzyTerm(string field, string value, int distance, bool prefix, bool transpositionCostOne) { - var inner = new JsonObject { + public static ParadeDbJsonQuery FuzzyTerm( + string field, + string value, + int distance, + bool prefix, + bool transpositionCostOne + ) + { + var inner = new JsonObject + { ["field"] = field, ["value"] = value, ["distance"] = distance, ["prefix"] = prefix, - ["transposition_cost_one"] = transpositionCostOne + ["transposition_cost_one"] = transpositionCostOne, }; return new(new JsonObject { ["fuzzy_term"] = inner }); } @@ -111,19 +156,38 @@ public static ParadeDbJsonQuery FuzzyTerm(string field, string value, int distan /// /// Phrase match. Produces: {"phrase":{"field":"...","phrases":[...]}}. /// - public static ParadeDbJsonQuery Phrase(string field, params string[] phrases) { + public static ParadeDbJsonQuery Phrase(string field, params string[] phrases) + { var arr = new JsonArray(); - foreach (var p in phrases) arr.Add(JsonValue.Create(p)); - return new(new JsonObject { ["phrase"] = new JsonObject { ["field"] = field, ["phrases"] = arr } }); + foreach (var p in phrases) + arr.Add(JsonValue.Create(p)); + return new( + new JsonObject + { + ["phrase"] = new JsonObject { ["field"] = field, ["phrases"] = arr }, + } + ); } /// /// Phrase match with slop. /// - public static ParadeDbJsonQuery Phrase(string field, int slop, params string[] phrases) { + public static ParadeDbJsonQuery Phrase(string field, int slop, params string[] phrases) + { var arr = new JsonArray(); - foreach (var p in phrases) arr.Add(JsonValue.Create(p)); - return new(new JsonObject { ["phrase"] = new JsonObject { ["field"] = field, ["phrases"] = arr, ["slop"] = slop } }); + foreach (var p in phrases) + arr.Add(JsonValue.Create(p)); + return new( + new JsonObject + { + ["phrase"] = new JsonObject + { + ["field"] = field, + ["phrases"] = arr, + ["slop"] = slop, + }, + } + ); } // ── Phrase Prefix ──────────────────────────────────────────────── @@ -131,10 +195,17 @@ public static ParadeDbJsonQuery Phrase(string field, int slop, params string[] p /// /// Phrase prefix match. Produces: {"phrase_prefix":{"field":"...","phrases":[...]}}. /// - public static ParadeDbJsonQuery PhrasePrefix(string field, params string[] phrases) { + public static ParadeDbJsonQuery PhrasePrefix(string field, params string[] phrases) + { var arr = new JsonArray(); - foreach (var p in phrases) arr.Add(JsonValue.Create(p)); - return new(new JsonObject { ["phrase_prefix"] = new JsonObject { ["field"] = field, ["phrases"] = arr } }); + foreach (var p in phrases) + arr.Add(JsonValue.Create(p)); + return new( + new JsonObject + { + ["phrase_prefix"] = new JsonObject { ["field"] = field, ["phrases"] = arr }, + } + ); } // ── Regex ──────────────────────────────────────────────────────── @@ -143,7 +214,12 @@ public static ParadeDbJsonQuery PhrasePrefix(string field, params string[] phras /// Regex match. Produces: {"regex":{"field":"...","pattern":"..."}}. /// public static ParadeDbJsonQuery Regex(string field, string pattern) => - new(new JsonObject { ["regex"] = new JsonObject { ["field"] = field, ["pattern"] = pattern } }); + new( + new JsonObject + { + ["regex"] = new JsonObject { ["field"] = field, ["pattern"] = pattern }, + } + ); // ── Range ──────────────────────────────────────────────────────── @@ -153,18 +229,35 @@ public static ParadeDbJsonQuery Regex(string field, string pattern) => /// which pg_search requires — omitting the key throws a deserialization panic). /// Set to true for DateTime range queries (adds "is_datetime":true). /// - public static ParadeDbJsonQuery Range(string field, object lowerBound, object upperBound, - bool lowerInclusive = true, bool upperInclusive = false, bool isDatetime = false) { - var inner = new JsonObject { + public static ParadeDbJsonQuery Range( + string field, + object lowerBound, + object upperBound, + bool lowerInclusive = true, + bool upperInclusive = false, + bool isDatetime = false + ) + { + var inner = new JsonObject + { ["field"] = field, - ["lower_bound"] = lowerBound != null - ? new JsonObject { [lowerInclusive ? "included" : "excluded"] = CreateJsonValue(lowerBound) } - : null, - ["upper_bound"] = upperBound != null - ? new JsonObject { [upperInclusive ? "included" : "excluded"] = CreateJsonValue(upperBound) } - : null + ["lower_bound"] = + lowerBound != null + ? new JsonObject + { + [lowerInclusive ? "included" : "excluded"] = CreateJsonValue(lowerBound), + } + : null, + ["upper_bound"] = + upperBound != null + ? new JsonObject + { + [upperInclusive ? "included" : "excluded"] = CreateJsonValue(upperBound), + } + : null, }; - if (isDatetime) inner["is_datetime"] = true; + if (isDatetime) + inner["is_datetime"] = true; return new(new JsonObject { ["range"] = inner }); } @@ -174,7 +267,12 @@ public static ParadeDbJsonQuery Range(string field, object lowerBound, object up /// Wraps a query with a boost factor. Produces: {"boost":{"query":{...},"factor":N}}. /// public static ParadeDbJsonQuery Boost(ParadeDbJsonQuery query, double factor) => - new(new JsonObject { ["boost"] = new JsonObject { ["query"] = query.CloneNode(), ["factor"] = factor } }); + new( + new JsonObject + { + ["boost"] = new JsonObject { ["query"] = query.CloneNode(), ["factor"] = factor }, + } + ); // ── Const Score ────────────────────────────────────────────────── @@ -182,7 +280,16 @@ public static ParadeDbJsonQuery Boost(ParadeDbJsonQuery query, double factor) => /// Wraps a query with a constant score. Produces: {"const_score":{"query":{...},"score":N}}. /// public static ParadeDbJsonQuery ConstScore(ParadeDbJsonQuery query, double score) => - new(new JsonObject { ["const_score"] = new JsonObject { ["query"] = query.CloneNode(), ["score"] = score } }); + new( + new JsonObject + { + ["const_score"] = new JsonObject + { + ["query"] = query.CloneNode(), + ["score"] = score, + }, + } + ); // ── Exists ─────────────────────────────────────────────────────── @@ -197,17 +304,18 @@ public static ParadeDbJsonQuery Exists(string field) => /// /// Match all documents. Produces: {"all":null}. /// - public static ParadeDbJsonQuery All() => - new(new JsonObject { ["all"] = null }); + public static ParadeDbJsonQuery All() => new(new JsonObject { ["all"] = null }); // ── Disjunction Max ────────────────────────────────────────────── /// /// Disjunction max query. Produces: {"disjunction_max":{"disjuncts":[...]}}. /// - public static ParadeDbJsonQuery DisjunctionMax(params ParadeDbJsonQuery[] queries) { + public static ParadeDbJsonQuery DisjunctionMax(params ParadeDbJsonQuery[] queries) + { var arr = new JsonArray(); - foreach (var q in queries) arr.Add(q.CloneNode()); + foreach (var q in queries) + arr.Add(q.CloneNode()); return new(new JsonObject { ["disjunction_max"] = new JsonObject { ["disjuncts"] = arr } }); } @@ -224,7 +332,8 @@ public static ParadeDbJsonQuery MoreLikeThis(int documentId) => /// /// Boolean query combining must/should/must_not clauses. /// - public static ParadeDbJsonQuery Boolean(Action configure) { + public static ParadeDbJsonQuery Boolean(Action configure) + { var builder = new ParadeDbBooleanQuery(); configure(builder); return new(builder.ToJsonNode()); @@ -232,18 +341,20 @@ public static ParadeDbJsonQuery Boolean(Action configure) // ── Helpers ────────────────────────────────────────────────────── - internal static JsonNode CreateJsonValue(object value) => value switch { - string s => JsonValue.Create(s), - int i => JsonValue.Create(i), - long l => JsonValue.Create(l), - double d => JsonValue.Create(d), - float f => JsonValue.Create(f), - bool b => JsonValue.Create(b), - Guid g => JsonValue.Create(g.ToString()), - DateTime dt => JsonValue.Create(dt.Kind == DateTimeKind.Utc - ? dt.ToString("yyyy-MM-ddTHH:mm:ssZ") - : dt.ToString("O")), - Enum e => JsonValue.Create(Convert.ToInt32(e)), - _ => JsonValue.Create(value.ToString()) - }; + internal static JsonNode CreateJsonValue(object value) => + value switch + { + string s => JsonValue.Create(s), + int i => JsonValue.Create(i), + long l => JsonValue.Create(l), + double d => JsonValue.Create(d), + float f => JsonValue.Create(f), + bool b => JsonValue.Create(b), + Guid g => JsonValue.Create(g.ToString()), + DateTime dt => JsonValue.Create( + dt.Kind == DateTimeKind.Utc ? dt.ToString("yyyy-MM-ddTHH:mm:ssZ") : dt.ToString("O") + ), + Enum e => JsonValue.Create(Convert.ToInt32(e)), + _ => JsonValue.Create(value.ToString()), + }; } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbMethodCallTranslator.cs b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbMethodCallTranslator.cs index 9f8591c..9987ac5 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbMethodCallTranslator.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbMethodCallTranslator.cs @@ -8,114 +8,208 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; -public sealed class ParadeDbMethodCallTranslator : IMethodCallTranslator { +public sealed class ParadeDbMethodCallTranslator : IMethodCallTranslator +{ private readonly ISqlExpressionFactory _sql; private readonly IRelationalTypeMappingSource _typeMappingSource; // ── Basic Search ────────────────────────────────────────────────── - private static readonly MethodInfo MatchesMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.Matches), [typeof(DbFunctions), typeof(string), typeof(string)])!; + private static readonly MethodInfo MatchesMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.Matches), + [typeof(DbFunctions), typeof(string), typeof(string)] + )!; - private static readonly MethodInfo MatchesAllMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.MatchesAll), [typeof(DbFunctions), typeof(string), typeof(string)])!; + private static readonly MethodInfo MatchesAllMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.MatchesAll), + [typeof(DbFunctions), typeof(string), typeof(string)] + )!; // ── Phrase Search ───────────────────────────────────────────────── - private static readonly MethodInfo MatchesPhraseMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.MatchesPhrase), [typeof(DbFunctions), typeof(string), typeof(string)])!; + private static readonly MethodInfo MatchesPhraseMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.MatchesPhrase), + [typeof(DbFunctions), typeof(string), typeof(string)] + )!; - private static readonly MethodInfo MatchesPhraseSlopMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.MatchesPhrase), [typeof(DbFunctions), typeof(string), typeof(string), typeof(int)])!; + private static readonly MethodInfo MatchesPhraseSlopMethod = + typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.MatchesPhrase), + [typeof(DbFunctions), typeof(string), typeof(string), typeof(int)] + )!; // ── Term Search ─────────────────────────────────────────────────── - private static readonly MethodInfo MatchesTermMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.MatchesTerm), [typeof(DbFunctions), typeof(string), typeof(string)])!; + private static readonly MethodInfo MatchesTermMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.MatchesTerm), + [typeof(DbFunctions), typeof(string), typeof(string)] + )!; - private static readonly MethodInfo MatchesTermSetMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.MatchesTermSet), [typeof(DbFunctions), typeof(string), typeof(string[])])!; + private static readonly MethodInfo MatchesTermSetMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.MatchesTermSet), + [typeof(DbFunctions), typeof(string), typeof(string[])] + )!; // ── Fuzzy Search ────────────────────────────────────────────────── - private static readonly MethodInfo MatchesFuzzyMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.MatchesFuzzy), [typeof(DbFunctions), typeof(string), typeof(string), typeof(int)])!; - - private static readonly MethodInfo MatchesFuzzyFullMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.MatchesFuzzy), [typeof(DbFunctions), typeof(string), typeof(string), typeof(int), typeof(bool), typeof(bool)])!; - - private static readonly MethodInfo MatchesAllFuzzyMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.MatchesAllFuzzy), [typeof(DbFunctions), typeof(string), typeof(string), typeof(int)])!; - - private static readonly MethodInfo MatchesAllFuzzyFullMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.MatchesAllFuzzy), [typeof(DbFunctions), typeof(string), typeof(string), typeof(int), typeof(bool), typeof(bool)])!; - - private static readonly MethodInfo MatchesTermFuzzyMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.MatchesTermFuzzy), [typeof(DbFunctions), typeof(string), typeof(string), typeof(int)])!; - - private static readonly MethodInfo MatchesTermFuzzyFullMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.MatchesTermFuzzy), [typeof(DbFunctions), typeof(string), typeof(string), typeof(int), typeof(bool), typeof(bool)])!; + private static readonly MethodInfo MatchesFuzzyMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.MatchesFuzzy), + [typeof(DbFunctions), typeof(string), typeof(string), typeof(int)] + )!; + + private static readonly MethodInfo MatchesFuzzyFullMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.MatchesFuzzy), + [ + typeof(DbFunctions), + typeof(string), + typeof(string), + typeof(int), + typeof(bool), + typeof(bool), + ] + )!; + + private static readonly MethodInfo MatchesAllFuzzyMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.MatchesAllFuzzy), + [typeof(DbFunctions), typeof(string), typeof(string), typeof(int)] + )!; + + private static readonly MethodInfo MatchesAllFuzzyFullMethod = + typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.MatchesAllFuzzy), + [ + typeof(DbFunctions), + typeof(string), + typeof(string), + typeof(int), + typeof(bool), + typeof(bool), + ] + )!; + + private static readonly MethodInfo MatchesTermFuzzyMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.MatchesTermFuzzy), + [typeof(DbFunctions), typeof(string), typeof(string), typeof(int)] + )!; + + private static readonly MethodInfo MatchesTermFuzzyFullMethod = + typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.MatchesTermFuzzy), + [ + typeof(DbFunctions), + typeof(string), + typeof(string), + typeof(int), + typeof(bool), + typeof(bool), + ] + )!; // ── Boost ───────────────────────────────────────────────────────── - private static readonly MethodInfo MatchesBoostedMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.MatchesBoosted), [typeof(DbFunctions), typeof(string), typeof(string), typeof(double)])!; + private static readonly MethodInfo MatchesBoostedMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.MatchesBoosted), + [typeof(DbFunctions), typeof(string), typeof(string), typeof(double)] + )!; - private static readonly MethodInfo MatchesAllBoostedMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.MatchesAllBoosted), [typeof(DbFunctions), typeof(string), typeof(string), typeof(double)])!; + private static readonly MethodInfo MatchesAllBoostedMethod = + typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.MatchesAllBoosted), + [typeof(DbFunctions), typeof(string), typeof(string), typeof(double)] + )!; // ── Fuzzy + Boost Combined ──────────────────────────────────────── - private static readonly MethodInfo MatchesFuzzyBoostedMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.MatchesFuzzyBoosted), [typeof(DbFunctions), typeof(string), typeof(string), typeof(int), typeof(double)])!; - - private static readonly MethodInfo MatchesAllFuzzyBoostedMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.MatchesAllFuzzyBoosted), [typeof(DbFunctions), typeof(string), typeof(string), typeof(int), typeof(double)])!; + private static readonly MethodInfo MatchesFuzzyBoostedMethod = + typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.MatchesFuzzyBoosted), + [typeof(DbFunctions), typeof(string), typeof(string), typeof(int), typeof(double)] + )!; + + private static readonly MethodInfo MatchesAllFuzzyBoostedMethod = + typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.MatchesAllFuzzyBoosted), + [typeof(DbFunctions), typeof(string), typeof(string), typeof(int), typeof(double)] + )!; // ── BM25 Scoring ────────────────────────────────────────────────── - private static readonly MethodInfo ScoreMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.Score), [typeof(DbFunctions), typeof(object)])!; + private static readonly MethodInfo ScoreMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.Score), + [typeof(DbFunctions), typeof(object)] + )!; // ── Snippets ────────────────────────────────────────────────────── - private static readonly MethodInfo SnippetMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.Snippet), [typeof(DbFunctions), typeof(string)])!; + private static readonly MethodInfo SnippetMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.Snippet), + [typeof(DbFunctions), typeof(string)] + )!; - private static readonly MethodInfo SnippetParamsMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.Snippet), [typeof(DbFunctions), typeof(string), typeof(string), typeof(string), typeof(int)])!; + private static readonly MethodInfo SnippetParamsMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.Snippet), + [typeof(DbFunctions), typeof(string), typeof(string), typeof(string), typeof(int)] + )!; - private static readonly MethodInfo SnippetsMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.Snippets), [typeof(DbFunctions), typeof(string), typeof(int), typeof(int), typeof(int)])!; + private static readonly MethodInfo SnippetsMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.Snippets), + [typeof(DbFunctions), typeof(string), typeof(int), typeof(int), typeof(int)] + )!; // ── Parse Query ─────────────────────────────────────────────────── - private static readonly MethodInfo ParseMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.Parse), [typeof(DbFunctions), typeof(object), typeof(string)])!; + private static readonly MethodInfo ParseMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.Parse), + [typeof(DbFunctions), typeof(object), typeof(string)] + )!; - private static readonly MethodInfo ParseFullMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.Parse), [typeof(DbFunctions), typeof(object), typeof(string), typeof(bool), typeof(bool)])!; + private static readonly MethodInfo ParseFullMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.Parse), + [typeof(DbFunctions), typeof(object), typeof(string), typeof(bool), typeof(bool)] + )!; // ── Regex Search ────────────────────────────────────────────────── - private static readonly MethodInfo RegexMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.Regex), [typeof(DbFunctions), typeof(string), typeof(string)])!; + private static readonly MethodInfo RegexMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.Regex), + [typeof(DbFunctions), typeof(string), typeof(string)] + )!; // ── Phrase Prefix ───────────────────────────────────────────────── - private static readonly MethodInfo PhrasePrefixMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.PhrasePrefix), [typeof(DbFunctions), typeof(string), typeof(string[])])!; + private static readonly MethodInfo PhrasePrefixMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.PhrasePrefix), + [typeof(DbFunctions), typeof(string), typeof(string[])] + )!; - private static readonly MethodInfo PhrasePrefixMaxMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.PhrasePrefix), [typeof(DbFunctions), typeof(string), typeof(int), typeof(string[])])!; + private static readonly MethodInfo PhrasePrefixMaxMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.PhrasePrefix), + [typeof(DbFunctions), typeof(string), typeof(int), typeof(string[])] + )!; // ── More Like This ──────────────────────────────────────────────── - private static readonly MethodInfo MoreLikeThisMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.MoreLikeThis), [typeof(DbFunctions), typeof(object), typeof(int)])!; + private static readonly MethodInfo MoreLikeThisMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.MoreLikeThis), + [typeof(DbFunctions), typeof(object), typeof(int)] + )!; - private static readonly MethodInfo MoreLikeThisFieldsMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.MoreLikeThis), [typeof(DbFunctions), typeof(object), typeof(int), typeof(string[])])!; + private static readonly MethodInfo MoreLikeThisFieldsMethod = + typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.MoreLikeThis), + [typeof(DbFunctions), typeof(object), typeof(int), typeof(string[])] + )!; // ── JSON Query Search ────────────────────────────────────────── - private static readonly MethodInfo JsonSearchMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.JsonSearch), [typeof(DbFunctions), typeof(object), typeof(string)])!; - - public ParadeDbMethodCallTranslator(ISqlExpressionFactory sql, IRelationalTypeMappingSource typeMappingSource) { + private static readonly MethodInfo JsonSearchMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.JsonSearch), + [typeof(DbFunctions), typeof(object), typeof(string)] + )!; + + public ParadeDbMethodCallTranslator( + ISqlExpressionFactory sql, + IRelationalTypeMappingSource typeMappingSource + ) + { _sql = sql; _typeMappingSource = typeMappingSource; } - public SqlExpression Translate(SqlExpression instance, MethodInfo method, IReadOnlyList arguments, - IDiagnosticsLogger logger) { + public SqlExpression Translate( + SqlExpression instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger + ) + { // arguments[0] is always DbFunctions (ignored) // ── Basic Search ────────────────────────────────────────── @@ -130,7 +224,11 @@ public SqlExpression Translate(SqlExpression instance, MethodInfo method, IReadO return MakeBinaryBool(arguments[1], "###", arguments[2]); if (method == MatchesPhraseSlopMethod) - return MakeBinaryBool(arguments[1], "###", WithModifier(arguments[2], BuildSlopSuffix(arguments[3]))); + return MakeBinaryBool( + arguments[1], + "###", + WithModifier(arguments[2], BuildSlopSuffix(arguments[3])) + ); // ── Term Search ─────────────────────────────────────────── if (method == MatchesTermMethod) @@ -141,137 +239,246 @@ public SqlExpression Translate(SqlExpression instance, MethodInfo method, IReadO // ── Fuzzy Search ────────────────────────────────────────── if (method == MatchesFuzzyMethod) - return MakeBinaryBool(arguments[1], "|||", WithModifier(arguments[2], BuildFuzzySuffix(arguments[3]))); + return MakeBinaryBool( + arguments[1], + "|||", + WithModifier(arguments[2], BuildFuzzySuffix(arguments[3])) + ); if (method == MatchesFuzzyFullMethod) - return MakeBinaryBool(arguments[1], "@@@", - BuildFuzzyMatchFunc(arguments[2], arguments[3], arguments[4], arguments[5], conjunctionMode: false)); + return MakeBinaryBool( + arguments[1], + "@@@", + BuildFuzzyMatchFunc( + arguments[2], + arguments[3], + arguments[4], + arguments[5], + conjunctionMode: false + ) + ); if (method == MatchesAllFuzzyMethod) - return MakeBinaryBool(arguments[1], "&&&", WithModifier(arguments[2], BuildFuzzySuffix(arguments[3]))); + return MakeBinaryBool( + arguments[1], + "&&&", + WithModifier(arguments[2], BuildFuzzySuffix(arguments[3])) + ); if (method == MatchesAllFuzzyFullMethod) - return MakeBinaryBool(arguments[1], "@@@", - BuildFuzzyMatchFunc(arguments[2], arguments[3], arguments[4], arguments[5], conjunctionMode: true)); + return MakeBinaryBool( + arguments[1], + "@@@", + BuildFuzzyMatchFunc( + arguments[2], + arguments[3], + arguments[4], + arguments[5], + conjunctionMode: true + ) + ); if (method == MatchesTermFuzzyMethod) - return MakeBinaryBool(arguments[1], "===", WithModifier(arguments[2], BuildFuzzySuffix(arguments[3]))); + return MakeBinaryBool( + arguments[1], + "===", + WithModifier(arguments[2], BuildFuzzySuffix(arguments[3])) + ); if (method == MatchesTermFuzzyFullMethod) - return MakeBinaryBool(arguments[1], "@@@", - BuildFuzzyTermFunc(arguments[2], arguments[3], arguments[4], arguments[5])); + return MakeBinaryBool( + arguments[1], + "@@@", + BuildFuzzyTermFunc(arguments[2], arguments[3], arguments[4], arguments[5]) + ); // ── Boost ───────────────────────────────────────────────── if (method == MatchesBoostedMethod) - return MakeBinaryBool(arguments[1], "|||", WithModifier(arguments[2], BuildBoostSuffix(arguments[3]))); + return MakeBinaryBool( + arguments[1], + "|||", + WithModifier(arguments[2], BuildBoostSuffix(arguments[3])) + ); if (method == MatchesAllBoostedMethod) - return MakeBinaryBool(arguments[1], "&&&", WithModifier(arguments[2], BuildBoostSuffix(arguments[3]))); + return MakeBinaryBool( + arguments[1], + "&&&", + WithModifier(arguments[2], BuildBoostSuffix(arguments[3])) + ); // ── Fuzzy + Boost Combined ──────────────────────────────── if (method == MatchesFuzzyBoostedMethod) - return MakeBinaryBool(arguments[1], "|||", - WithModifier(arguments[2], BuildFuzzySuffix(arguments[3]) + BuildBoostSuffix(arguments[4]))); + return MakeBinaryBool( + arguments[1], + "|||", + WithModifier( + arguments[2], + BuildFuzzySuffix(arguments[3]) + BuildBoostSuffix(arguments[4]) + ) + ); if (method == MatchesAllFuzzyBoostedMethod) - return MakeBinaryBool(arguments[1], "&&&", - WithModifier(arguments[2], BuildFuzzySuffix(arguments[3]) + BuildBoostSuffix(arguments[4]))); + return MakeBinaryBool( + arguments[1], + "&&&", + WithModifier( + arguments[2], + BuildFuzzySuffix(arguments[3]) + BuildBoostSuffix(arguments[4]) + ) + ); // ── BM25 Scoring ────────────────────────────────────────── if (method == ScoreMethod) - return _sql.Function("pdb.score", [Map(arguments[1])], - nullable: true, argumentsPropagateNullability: [true], - typeof(double), _typeMappingSource.FindMapping(typeof(double))); + return _sql.Function( + "pdb.score", + [Map(arguments[1])], + nullable: true, + argumentsPropagateNullability: [true], + typeof(double), + _typeMappingSource.FindMapping(typeof(double)) + ); // ── Snippets ────────────────────────────────────────────── if (method == SnippetMethod) - return _sql.Function("pdb.snippet", [Map(arguments[1])], - nullable: true, argumentsPropagateNullability: [true], - typeof(string), _typeMappingSource.FindMapping(typeof(string))); + return _sql.Function( + "pdb.snippet", + [Map(arguments[1])], + nullable: true, + argumentsPropagateNullability: [true], + typeof(string), + _typeMappingSource.FindMapping(typeof(string)) + ); if (method == SnippetParamsMethod) - return new ParadeDbNamedArgFunctionExpression("pdb.snippet", + return new ParadeDbNamedArgFunctionExpression( + "pdb.snippet", [Map(arguments[1])], [ ("start_tag", Map(arguments[2])), ("end_tag", Map(arguments[3])), - ("max_num_chars", Map(arguments[4])) + ("max_num_chars", Map(arguments[4])), ], - typeof(string), _typeMappingSource.FindMapping(typeof(string))); + typeof(string), + _typeMappingSource.FindMapping(typeof(string)) + ); if (method == SnippetsMethod) - return new ParadeDbNamedArgFunctionExpression("pdb.snippets", + return new ParadeDbNamedArgFunctionExpression( + "pdb.snippets", [Map(arguments[1])], [ ("max_num_chars", Map(arguments[2])), ("\"limit\"", Map(arguments[3])), - ("\"offset\"", Map(arguments[4])) + ("\"offset\"", Map(arguments[4])), ], - typeof(string[]), _typeMappingSource.FindMapping(typeof(string[]))); + typeof(string[]), + _typeMappingSource.FindMapping(typeof(string[])) + ); // ── Parse Query ─────────────────────────────────────────── - if (method == ParseMethod) { - var parseFunc = _sql.Function("pdb.parse", [Map(arguments[2])], - nullable: true, argumentsPropagateNullability: [true], - typeof(bool), _typeMappingSource.FindMapping(typeof(bool))); + if (method == ParseMethod) + { + var parseFunc = _sql.Function( + "pdb.parse", + [Map(arguments[2])], + nullable: true, + argumentsPropagateNullability: [true], + typeof(bool), + _typeMappingSource.FindMapping(typeof(bool)) + ); return MakeBinaryBool(arguments[1], "@@@", parseFunc); } - if (method == ParseFullMethod) { - var parseFunc = new ParadeDbNamedArgFunctionExpression("pdb.parse", + if (method == ParseFullMethod) + { + var parseFunc = new ParadeDbNamedArgFunctionExpression( + "pdb.parse", [Map(arguments[2])], - [ - ("lenient", Map(arguments[3])), - ("conjunction_mode", Map(arguments[4])) - ], - typeof(bool), _typeMappingSource.FindMapping(typeof(bool))); + [("lenient", Map(arguments[3])), ("conjunction_mode", Map(arguments[4]))], + typeof(bool), + _typeMappingSource.FindMapping(typeof(bool)) + ); return MakeBinaryBool(arguments[1], "@@@", parseFunc); } // ── Regex Search ────────────────────────────────────────── - if (method == RegexMethod) { - var regexFunc = _sql.Function("pdb.regex", [Map(arguments[2])], - nullable: true, argumentsPropagateNullability: [true], - typeof(bool), _typeMappingSource.FindMapping(typeof(bool))); + if (method == RegexMethod) + { + var regexFunc = _sql.Function( + "pdb.regex", + [Map(arguments[2])], + nullable: true, + argumentsPropagateNullability: [true], + typeof(bool), + _typeMappingSource.FindMapping(typeof(bool)) + ); return MakeBinaryBool(arguments[1], "@@@", regexFunc); } // ── Phrase Prefix ───────────────────────────────────────── - if (method == PhrasePrefixMethod) { - var phrasePrefixFunc = _sql.Function("pdb.phrase_prefix", [Map(arguments[2])], - nullable: true, argumentsPropagateNullability: [true], - typeof(bool), _typeMappingSource.FindMapping(typeof(bool))); + if (method == PhrasePrefixMethod) + { + var phrasePrefixFunc = _sql.Function( + "pdb.phrase_prefix", + [Map(arguments[2])], + nullable: true, + argumentsPropagateNullability: [true], + typeof(bool), + _typeMappingSource.FindMapping(typeof(bool)) + ); return MakeBinaryBool(arguments[1], "@@@", phrasePrefixFunc); } - if (method == PhrasePrefixMaxMethod) { - var phrasePrefixFunc = new ParadeDbNamedArgFunctionExpression("pdb.phrase_prefix", + if (method == PhrasePrefixMaxMethod) + { + var phrasePrefixFunc = new ParadeDbNamedArgFunctionExpression( + "pdb.phrase_prefix", [Map(arguments[3])], [("max_expansion", Map(arguments[2]))], - typeof(bool), _typeMappingSource.FindMapping(typeof(bool))); + typeof(bool), + _typeMappingSource.FindMapping(typeof(bool)) + ); return MakeBinaryBool(arguments[1], "@@@", phrasePrefixFunc); } // ── More Like This ──────────────────────────────────────── - if (method == MoreLikeThisMethod) { - var mltFunc = _sql.Function("pdb.more_like_this", [Map(arguments[2])], - nullable: true, argumentsPropagateNullability: [true], - typeof(bool), _typeMappingSource.FindMapping(typeof(bool))); + if (method == MoreLikeThisMethod) + { + var mltFunc = _sql.Function( + "pdb.more_like_this", + [Map(arguments[2])], + nullable: true, + argumentsPropagateNullability: [true], + typeof(bool), + _typeMappingSource.FindMapping(typeof(bool)) + ); return MakeBinaryBool(arguments[1], "@@@", mltFunc); } - if (method == MoreLikeThisFieldsMethod) { - var mltFunc = _sql.Function("pdb.more_like_this", [Map(arguments[2]), Map(arguments[3])], - nullable: true, argumentsPropagateNullability: [true, true], - typeof(bool), _typeMappingSource.FindMapping(typeof(bool))); + if (method == MoreLikeThisFieldsMethod) + { + var mltFunc = _sql.Function( + "pdb.more_like_this", + [Map(arguments[2]), Map(arguments[3])], + nullable: true, + argumentsPropagateNullability: [true, true], + typeof(bool), + _typeMappingSource.FindMapping(typeof(bool)) + ); return MakeBinaryBool(arguments[1], "@@@", mltFunc); } // ── JSON Query Search ────────────────────────────────────── - if (method == JsonSearchMethod) { + if (method == JsonSearchMethod) + { var jsonExpr = Map(arguments[2]); var castExpr = new ParadeDbModifiedQueryExpression( - jsonExpr, "::jsonb", jsonExpr.Type, jsonExpr.TypeMapping); + jsonExpr, + "::jsonb", + jsonExpr.Type, + jsonExpr.TypeMapping + ); return MakeBinaryBool(arguments[1], "@@@", castExpr); } @@ -282,10 +489,14 @@ public SqlExpression Translate(SqlExpression instance, MethodInfo method, IReadO private SqlExpression Map(SqlExpression expr) => _sql.ApplyDefaultTypeMapping(expr); - private PgUnknownBinaryExpression MakeBinaryBool(SqlExpression left, string op, SqlExpression right) => - new(Map(left), Map(right), op, typeof(bool), _typeMappingSource.FindMapping(typeof(bool))); + private PgUnknownBinaryExpression MakeBinaryBool( + SqlExpression left, + string op, + SqlExpression right + ) => new(Map(left), Map(right), op, typeof(bool), _typeMappingSource.FindMapping(typeof(bool))); - private SqlExpression WithModifier(SqlExpression inner, string suffix) { + private SqlExpression WithModifier(SqlExpression inner, string suffix) + { var mapped = Map(inner); return new ParadeDbModifiedQueryExpression(mapped, suffix, mapped.Type, mapped.TypeMapping); } @@ -293,50 +504,82 @@ private SqlExpression WithModifier(SqlExpression inner, string suffix) { private static int ExtractInt(SqlExpression expr) => expr is SqlConstantExpression { Value: int value } ? value - : throw new InvalidOperationException("ParadeDB modifier parameters (distance, slop, maxExpansions) must be compile-time constants."); + : throw new InvalidOperationException( + "ParadeDB modifier parameters (distance, slop, maxExpansions) must be compile-time constants." + ); private static double ExtractDouble(SqlExpression expr) => expr is SqlConstantExpression { Value: double value } ? value - : throw new InvalidOperationException("ParadeDB modifier parameters (boost factor) must be compile-time constants."); + : throw new InvalidOperationException( + "ParadeDB modifier parameters (boost factor) must be compile-time constants." + ); - private static string BuildFuzzySuffix(SqlExpression distanceExpr) { + private static string BuildFuzzySuffix(SqlExpression distanceExpr) + { var distance = ExtractInt(distanceExpr); return $"::pdb.fuzzy({distance})"; } // pdb.fuzzy(...) typmod accepts only a single int; the 5-arg fuzzy overloads route // through pdb.match / pdb.fuzzy_term function calls instead. See README and GH-15. - private SqlExpression BuildFuzzyMatchFunc(SqlExpression queryExpr, SqlExpression distanceExpr, - SqlExpression prefixExpr, SqlExpression transpExpr, bool conjunctionMode) { - var named = new List<(string, SqlExpression)> { + private SqlExpression BuildFuzzyMatchFunc( + SqlExpression queryExpr, + SqlExpression distanceExpr, + SqlExpression prefixExpr, + SqlExpression transpExpr, + bool conjunctionMode + ) + { + var named = new List<(string, SqlExpression)> + { ("distance", Map(distanceExpr)), ("transposition_cost_one", Map(transpExpr)), ("prefix", Map(prefixExpr)), }; - if (conjunctionMode) named.Add(("conjunction_mode", _sql.Constant(true, _typeMappingSource.FindMapping(typeof(bool))))); - return new ParadeDbNamedArgFunctionExpression("pdb.match", - [Map(queryExpr)], named, - typeof(bool), _typeMappingSource.FindMapping(typeof(bool))); + if (conjunctionMode) + named.Add( + ( + "conjunction_mode", + _sql.Constant(true, _typeMappingSource.FindMapping(typeof(bool))) + ) + ); + return new ParadeDbNamedArgFunctionExpression( + "pdb.match", + [Map(queryExpr)], + named, + typeof(bool), + _typeMappingSource.FindMapping(typeof(bool)) + ); } - private SqlExpression BuildFuzzyTermFunc(SqlExpression queryExpr, SqlExpression distanceExpr, - SqlExpression prefixExpr, SqlExpression transpExpr) { + private SqlExpression BuildFuzzyTermFunc( + SqlExpression queryExpr, + SqlExpression distanceExpr, + SqlExpression prefixExpr, + SqlExpression transpExpr + ) + { // pdb.fuzzy_term(value, distance, transposition_cost_one, prefix) — positional order. - return _sql.Function("pdb.fuzzy_term", + return _sql.Function( + "pdb.fuzzy_term", [Map(queryExpr), Map(distanceExpr), Map(transpExpr), Map(prefixExpr)], - nullable: true, argumentsPropagateNullability: [true, true, true, true], - typeof(bool), _typeMappingSource.FindMapping(typeof(bool))); + nullable: true, + argumentsPropagateNullability: [true, true, true, true], + typeof(bool), + _typeMappingSource.FindMapping(typeof(bool)) + ); } - private static string BuildBoostSuffix(SqlExpression boostExpr) { + private static string BuildBoostSuffix(SqlExpression boostExpr) + { var boost = ExtractDouble(boostExpr); return FormattableString.Invariant($"::pdb.boost({boost})"); } - private static string BuildSlopSuffix(SqlExpression slopExpr) { + private static string BuildSlopSuffix(SqlExpression slopExpr) + { var slop = ExtractInt(slopExpr); return $"::pdb.slop({slop})"; } - } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbMethodCallTranslatorPlugin.cs b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbMethodCallTranslatorPlugin.cs index 9facb29..c2fc926 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbMethodCallTranslatorPlugin.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbMethodCallTranslatorPlugin.cs @@ -3,11 +3,15 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; -public sealed class ParadeDbMethodCallTranslatorPlugin : IMethodCallTranslatorPlugin { +public sealed class ParadeDbMethodCallTranslatorPlugin : IMethodCallTranslatorPlugin +{ public IEnumerable Translators { get; } - public ParadeDbMethodCallTranslatorPlugin(ISqlExpressionFactory sqlExpressionFactory, - IRelationalTypeMappingSource typeMappingSource) { + public ParadeDbMethodCallTranslatorPlugin( + ISqlExpressionFactory sqlExpressionFactory, + IRelationalTypeMappingSource typeMappingSource + ) + { Translators = [new ParadeDbMethodCallTranslator(sqlExpressionFactory, typeMappingSource)]; } } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbModelFinalizingConvention.cs b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbModelFinalizingConvention.cs index f4507d2..d297566 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbModelFinalizingConvention.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbModelFinalizingConvention.cs @@ -8,40 +8,66 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; -public sealed class ParadeDbModelFinalizingConvention : IModelFinalizingConvention { - public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) { +public sealed class ParadeDbModelFinalizingConvention : IModelFinalizingConvention +{ + public void ProcessModelFinalizing( + IConventionModelBuilder modelBuilder, + IConventionContext context + ) + { var hasBm25Index = false; - foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { var attribute = entityType.ClrType.GetCustomAttribute(); ValidateNoOrphanFieldAttributes(entityType, attribute); - if (attribute == null) continue; + if (attribute == null) + continue; - var indexBuilder = entityType.Builder.HasIndex(attribute.Columns, fromDataAnnotation: true); - if (indexBuilder == null) continue; + var indexBuilder = entityType.Builder.HasIndex( + attribute.Columns, + fromDataAnnotation: true + ); + if (indexBuilder == null) + continue; indexBuilder.HasAnnotation("Npgsql:IndexMethod", "bm25", fromDataAnnotation: true); var keyProperty = entityType.FindProperty(attribute.KeyField); var keyColumnName = keyProperty?.GetColumnName() ?? attribute.KeyField; - indexBuilder.HasAnnotation("Npgsql:StorageParameter:key_field", keyColumnName, fromDataAnnotation: true); + indexBuilder.HasAnnotation( + "Npgsql:StorageParameter:key_field", + keyColumnName, + fromDataAnnotation: true + ); BuildFieldTypeAnnotations(entityType, attribute, indexBuilder); hasBm25Index = true; } - if (hasBm25Index) { - modelBuilder.HasAnnotation("Npgsql:PostgresExtension:pg_search", ",,", fromDataAnnotation: true); + if (hasBm25Index) + { + modelBuilder.HasAnnotation( + "Npgsql:PostgresExtension:pg_search", + ",,", + fromDataAnnotation: true + ); } } - private static void ValidateNoOrphanFieldAttributes(IConventionEntityType entityType, Bm25IndexAttribute attribute) { - foreach (var property in entityType.GetProperties()) { + private static void ValidateNoOrphanFieldAttributes( + IConventionEntityType entityType, + Bm25IndexAttribute attribute + ) + { + foreach (var property in entityType.GetProperties()) + { var propertyInfo = property.PropertyInfo; - if (propertyInfo == null) continue; + if (propertyInfo == null) + continue; var hasFieldAttr = propertyInfo.GetCustomAttribute() != null @@ -50,62 +76,85 @@ private static void ValidateNoOrphanFieldAttributes(IConventionEntityType entity || propertyInfo.GetCustomAttribute() != null || propertyInfo.GetCustomAttribute() != null; - if (!hasFieldAttr) continue; + if (!hasFieldAttr) + continue; - if (attribute == null) { + if (attribute == null) + { throw new InvalidOperationException( - $"Property '{entityType.ClrType.Name}.{property.Name}' has a BM25 field attribute, " + - "but the entity has no [Bm25Index] attribute."); + $"Property '{entityType.ClrType.Name}.{property.Name}' has a BM25 field attribute, " + + "but the entity has no [Bm25Index] attribute." + ); } - if (Array.IndexOf(attribute.Columns, property.Name) < 0) { + if (Array.IndexOf(attribute.Columns, property.Name) < 0) + { throw new InvalidOperationException( - $"Property '{entityType.ClrType.Name}.{property.Name}' has a BM25 field attribute, " + - "but it is not listed in the [Bm25Index] columns."); + $"Property '{entityType.ClrType.Name}.{property.Name}' has a BM25 field attribute, " + + "but it is not listed in the [Bm25Index] columns." + ); } } } - private static void BuildFieldTypeAnnotations(IConventionEntityType entityType, Bm25IndexAttribute attribute, - IConventionIndexBuilder indexBuilder) { + private static void BuildFieldTypeAnnotations( + IConventionEntityType entityType, + Bm25IndexAttribute attribute, + IConventionIndexBuilder indexBuilder + ) + { var textFields = new JsonObject(); var numericFields = new JsonObject(); var booleanFields = new JsonObject(); var datetimeFields = new JsonObject(); var jsonFields = new JsonObject(); - foreach (var propertyName in attribute.Columns) { - if (propertyName == attribute.KeyField) continue; + foreach (var propertyName in attribute.Columns) + { + if (propertyName == attribute.KeyField) + continue; var property = entityType.FindProperty(propertyName); var propertyInfo = property?.PropertyInfo; - if (propertyInfo == null) continue; + if (propertyInfo == null) + continue; var columnName = property.GetColumnName() ?? propertyName; var textAttr = propertyInfo.GetCustomAttribute(); - if (textAttr != null) { - textFields[columnName] = Bm25StorageParameterBuilder.BuildTextField(textAttr, propertyName); + if (textAttr != null) + { + textFields[columnName] = Bm25StorageParameterBuilder.BuildTextField( + textAttr, + propertyName + ); continue; } var numAttr = propertyInfo.GetCustomAttribute(); - if (numAttr != null) { + if (numAttr != null) + { numericFields[columnName] = Bm25StorageParameterBuilder.BuildNumericField(numAttr); continue; } var boolAttr = propertyInfo.GetCustomAttribute(); - if (boolAttr != null) { + if (boolAttr != null) + { booleanFields[columnName] = Bm25StorageParameterBuilder.BuildBooleanField(boolAttr); continue; } var dtAttr = propertyInfo.GetCustomAttribute(); - if (dtAttr != null) { + if (dtAttr != null) + { datetimeFields[columnName] = Bm25StorageParameterBuilder.BuildDateTimeField(dtAttr); continue; } var jsonAttr = propertyInfo.GetCustomAttribute(); - if (jsonAttr != null) { - jsonFields[columnName] = Bm25StorageParameterBuilder.BuildJsonField(jsonAttr, propertyName); + if (jsonAttr != null) + { + jsonFields[columnName] = Bm25StorageParameterBuilder.BuildJsonField( + jsonAttr, + propertyName + ); } } @@ -116,11 +165,18 @@ private static void BuildFieldTypeAnnotations(IConventionEntityType entityType, AddFieldGroupParameter(indexBuilder, "json_fields", jsonFields); } - private static void AddFieldGroupParameter(IConventionIndexBuilder indexBuilder, string name, JsonObject group) { - if (group.Count == 0) return; + private static void AddFieldGroupParameter( + IConventionIndexBuilder indexBuilder, + string name, + JsonObject group + ) + { + if (group.Count == 0) + return; indexBuilder.HasAnnotation( $"Npgsql:StorageParameter:{name}", Bm25StorageParameterBuilder.Serialize(group), - fromDataAnnotation: true); + fromDataAnnotation: true + ); } } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbModifiedQueryExpression.cs b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbModifiedQueryExpression.cs index c94910f..13740fa 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbModifiedQueryExpression.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbModifiedQueryExpression.cs @@ -9,23 +9,33 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; /// Wraps an inner SQL expression with a ParadeDB modifier suffix. /// Example: 'shoes'::pdb.fuzzy(2)::pdb.boost(2.0) /// -public sealed class ParadeDbModifiedQueryExpression : SqlExpression { +public sealed class ParadeDbModifiedQueryExpression : SqlExpression +{ public SqlExpression InnerExpression { get; } public string ModifierSuffix { get; } - public ParadeDbModifiedQueryExpression(SqlExpression innerExpression, string modifierSuffix, - Type type, RelationalTypeMapping typeMapping) - : base(type, typeMapping) { + public ParadeDbModifiedQueryExpression( + SqlExpression innerExpression, + string modifierSuffix, + Type type, + RelationalTypeMapping typeMapping + ) + : base(type, typeMapping) + { InnerExpression = innerExpression; ModifierSuffix = modifierSuffix; } - protected override Expression VisitChildren(ExpressionVisitor visitor) { + protected override Expression VisitChildren(ExpressionVisitor visitor) + { var visited = (SqlExpression)visitor.Visit(InnerExpression); - return visited == InnerExpression ? this : new ParadeDbModifiedQueryExpression(visited, ModifierSuffix, Type, TypeMapping); + return visited == InnerExpression + ? this + : new ParadeDbModifiedQueryExpression(visited, ModifierSuffix, Type, TypeMapping); } - protected override void Print(ExpressionPrinter expressionPrinter) { + protected override void Print(ExpressionPrinter expressionPrinter) + { expressionPrinter.Visit(InnerExpression); expressionPrinter.Append(ModifierSuffix); } @@ -40,5 +50,6 @@ public override Expression Quote() => throw new NotSupportedException("ParadeDB expressions do not support precompiled queries."); #endif - public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), InnerExpression, ModifierSuffix); + public override int GetHashCode() => + HashCode.Combine(base.GetHashCode(), InnerExpression, ModifierSuffix); } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbNamedArgFunctionExpression.cs b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbNamedArgFunctionExpression.cs index 39f2768..f81ea99 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbNamedArgFunctionExpression.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbNamedArgFunctionExpression.cs @@ -9,52 +9,72 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; /// SQL expression for functions with PostgreSQL named parameters (name => value). /// Example: pdb.snippet("Content", start_tag => '<b>', max_num_chars => 100) /// -public sealed class ParadeDbNamedArgFunctionExpression : SqlExpression { +public sealed class ParadeDbNamedArgFunctionExpression : SqlExpression +{ public string FunctionName { get; } public IReadOnlyList PositionalArgs { get; } public IReadOnlyList<(string Name, SqlExpression Value)> NamedArgs { get; } - public ParadeDbNamedArgFunctionExpression(string functionName, + public ParadeDbNamedArgFunctionExpression( + string functionName, IReadOnlyList positionalArgs, IReadOnlyList<(string Name, SqlExpression Value)> namedArgs, - Type type, RelationalTypeMapping typeMapping) - : base(type, typeMapping) { + Type type, + RelationalTypeMapping typeMapping + ) + : base(type, typeMapping) + { FunctionName = functionName; PositionalArgs = positionalArgs; NamedArgs = namedArgs; } - protected override Expression VisitChildren(ExpressionVisitor visitor) { + protected override Expression VisitChildren(ExpressionVisitor visitor) + { var positionalChanged = false; var newPositional = new SqlExpression[PositionalArgs.Count]; - for (var i = 0; i < PositionalArgs.Count; i++) { + for (var i = 0; i < PositionalArgs.Count; i++) + { newPositional[i] = (SqlExpression)visitor.Visit(PositionalArgs[i]); positionalChanged |= newPositional[i] != PositionalArgs[i]; } var namedChanged = false; var newNamed = new (string Name, SqlExpression Value)[NamedArgs.Count]; - for (var i = 0; i < NamedArgs.Count; i++) { + for (var i = 0; i < NamedArgs.Count; i++) + { var visitedValue = (SqlExpression)visitor.Visit(NamedArgs[i].Value); newNamed[i] = (NamedArgs[i].Name, visitedValue); namedChanged |= visitedValue != NamedArgs[i].Value; } - if (!positionalChanged && !namedChanged) return this; + if (!positionalChanged && !namedChanged) + return this; - return new ParadeDbNamedArgFunctionExpression(FunctionName, newPositional, newNamed, Type, TypeMapping); + return new ParadeDbNamedArgFunctionExpression( + FunctionName, + newPositional, + newNamed, + Type, + TypeMapping + ); } - protected override void Print(ExpressionPrinter expressionPrinter) { + protected override void Print(ExpressionPrinter expressionPrinter) + { expressionPrinter.Append(FunctionName).Append("("); - for (var i = 0; i < PositionalArgs.Count; i++) { - if (i > 0) expressionPrinter.Append(", "); + for (var i = 0; i < PositionalArgs.Count; i++) + { + if (i > 0) + expressionPrinter.Append(", "); expressionPrinter.Visit(PositionalArgs[i]); } - for (var i = 0; i < NamedArgs.Count; i++) { - if (i > 0 || PositionalArgs.Count > 0) expressionPrinter.Append(", "); + for (var i = 0; i < NamedArgs.Count; i++) + { + if (i > 0 || PositionalArgs.Count > 0) + expressionPrinter.Append(", "); expressionPrinter.Append(NamedArgs[i].Name).Append(" => "); expressionPrinter.Visit(NamedArgs[i].Value); } @@ -62,19 +82,29 @@ protected override void Print(ExpressionPrinter expressionPrinter) { expressionPrinter.Append(")"); } - public override bool Equals(object obj) { - if (obj is not ParadeDbNamedArgFunctionExpression other) return false; - if (FunctionName != other.FunctionName) return false; - if (PositionalArgs.Count != other.PositionalArgs.Count) return false; - if (NamedArgs.Count != other.NamedArgs.Count) return false; - - for (var i = 0; i < PositionalArgs.Count; i++) { - if (!PositionalArgs[i].Equals(other.PositionalArgs[i])) return false; + public override bool Equals(object obj) + { + if (obj is not ParadeDbNamedArgFunctionExpression other) + return false; + if (FunctionName != other.FunctionName) + return false; + if (PositionalArgs.Count != other.PositionalArgs.Count) + return false; + if (NamedArgs.Count != other.NamedArgs.Count) + return false; + + for (var i = 0; i < PositionalArgs.Count; i++) + { + if (!PositionalArgs[i].Equals(other.PositionalArgs[i])) + return false; } - for (var i = 0; i < NamedArgs.Count; i++) { - if (NamedArgs[i].Name != other.NamedArgs[i].Name) return false; - if (!NamedArgs[i].Value.Equals(other.NamedArgs[i].Value)) return false; + for (var i = 0; i < NamedArgs.Count; i++) + { + if (NamedArgs[i].Name != other.NamedArgs[i].Name) + return false; + if (!NamedArgs[i].Value.Equals(other.NamedArgs[i].Value)) + return false; } return true; @@ -85,12 +115,15 @@ public override Expression Quote() => throw new NotSupportedException("ParadeDB expressions do not support precompiled queries."); #endif - public override int GetHashCode() { + public override int GetHashCode() + { var hash = new HashCode(); hash.Add(base.GetHashCode()); hash.Add(FunctionName); - foreach (var arg in PositionalArgs) hash.Add(arg); - foreach (var (name, value) in NamedArgs) { + foreach (var arg in PositionalArgs) + hash.Add(arg); + foreach (var (name, value) in NamedArgs) + { hash.Add(name); hash.Add(value); } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbParameterBasedSqlProcessor.cs b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbParameterBasedSqlProcessor.cs index 0ada972..07510ab 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbParameterBasedSqlProcessor.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbParameterBasedSqlProcessor.cs @@ -4,52 +4,82 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; -public class ParadeDbParameterBasedSqlProcessor : NpgsqlParameterBasedSqlProcessor { +public class ParadeDbParameterBasedSqlProcessor : NpgsqlParameterBasedSqlProcessor +{ private readonly RelationalParameterBasedSqlProcessorDependencies _dependencies; #if NET8_0 private readonly bool _useRelationalNulls; - public ParadeDbParameterBasedSqlProcessor(RelationalParameterBasedSqlProcessorDependencies dependencies, - bool useRelationalNulls) - : base(dependencies, useRelationalNulls) { + public ParadeDbParameterBasedSqlProcessor( + RelationalParameterBasedSqlProcessorDependencies dependencies, + bool useRelationalNulls + ) + : base(dependencies, useRelationalNulls) + { _dependencies = dependencies; _useRelationalNulls = useRelationalNulls; } - protected override Expression ProcessSqlNullability(Expression selectExpression, - IReadOnlyDictionary parametersValues, out bool canCache) { - return new ParadeDbSqlNullabilityProcessor(_dependencies, _useRelationalNulls) - .Process(selectExpression, parametersValues, out canCache); + protected override Expression ProcessSqlNullability( + Expression selectExpression, + IReadOnlyDictionary parametersValues, + out bool canCache + ) + { + return new ParadeDbSqlNullabilityProcessor(_dependencies, _useRelationalNulls).Process( + selectExpression, + parametersValues, + out canCache + ); } #elif NET9_0 private readonly RelationalParameterBasedSqlProcessorParameters _parameters; - public ParadeDbParameterBasedSqlProcessor(RelationalParameterBasedSqlProcessorDependencies dependencies, - RelationalParameterBasedSqlProcessorParameters parameters) - : base(dependencies, parameters) { + public ParadeDbParameterBasedSqlProcessor( + RelationalParameterBasedSqlProcessorDependencies dependencies, + RelationalParameterBasedSqlProcessorParameters parameters + ) + : base(dependencies, parameters) + { _dependencies = dependencies; _parameters = parameters; } - protected override Expression ProcessSqlNullability(Expression selectExpression, - IReadOnlyDictionary parametersValues, out bool canCache) { - return new ParadeDbSqlNullabilityProcessor(_dependencies, _parameters) - .Process(selectExpression, parametersValues, out canCache); + protected override Expression ProcessSqlNullability( + Expression selectExpression, + IReadOnlyDictionary parametersValues, + out bool canCache + ) + { + return new ParadeDbSqlNullabilityProcessor(_dependencies, _parameters).Process( + selectExpression, + parametersValues, + out canCache + ); } #else private readonly RelationalParameterBasedSqlProcessorParameters _parameters; - public ParadeDbParameterBasedSqlProcessor(RelationalParameterBasedSqlProcessorDependencies dependencies, - RelationalParameterBasedSqlProcessorParameters parameters) - : base(dependencies, parameters) { + public ParadeDbParameterBasedSqlProcessor( + RelationalParameterBasedSqlProcessorDependencies dependencies, + RelationalParameterBasedSqlProcessorParameters parameters + ) + : base(dependencies, parameters) + { _dependencies = dependencies; _parameters = parameters; } - protected override Expression ProcessSqlNullability(Expression expression, ParametersCacheDecorator parametersDecorator) { - return new ParadeDbSqlNullabilityProcessor(_dependencies, _parameters) - .Process(expression, parametersDecorator); + protected override Expression ProcessSqlNullability( + Expression expression, + ParametersCacheDecorator parametersDecorator + ) + { + return new ParadeDbSqlNullabilityProcessor(_dependencies, _parameters).Process( + expression, + parametersDecorator + ); } #endif } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbParameterBasedSqlProcessorFactory.cs b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbParameterBasedSqlProcessorFactory.cs index 0203c04..5d5997b 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbParameterBasedSqlProcessorFactory.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbParameterBasedSqlProcessorFactory.cs @@ -3,19 +3,24 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; -public class ParadeDbParameterBasedSqlProcessorFactory : NpgsqlParameterBasedSqlProcessorFactory { +public class ParadeDbParameterBasedSqlProcessorFactory : NpgsqlParameterBasedSqlProcessorFactory +{ private readonly RelationalParameterBasedSqlProcessorDependencies _dependencies; - public ParadeDbParameterBasedSqlProcessorFactory(RelationalParameterBasedSqlProcessorDependencies dependencies) - : base(dependencies) { + public ParadeDbParameterBasedSqlProcessorFactory( + RelationalParameterBasedSqlProcessorDependencies dependencies + ) + : base(dependencies) + { _dependencies = dependencies; } #if NET8_0 - public override RelationalParameterBasedSqlProcessor Create(bool useRelationalNulls) - => new ParadeDbParameterBasedSqlProcessor(_dependencies, useRelationalNulls); + public override RelationalParameterBasedSqlProcessor Create(bool useRelationalNulls) => + new ParadeDbParameterBasedSqlProcessor(_dependencies, useRelationalNulls); #else - public override RelationalParameterBasedSqlProcessor Create(RelationalParameterBasedSqlProcessorParameters parameters) - => new ParadeDbParameterBasedSqlProcessor(_dependencies, parameters); + public override RelationalParameterBasedSqlProcessor Create( + RelationalParameterBasedSqlProcessorParameters parameters + ) => new ParadeDbParameterBasedSqlProcessor(_dependencies, parameters); #endif } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbQuerySqlGenerator.cs b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbQuerySqlGenerator.cs index 6c7bb41..51271df 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbQuerySqlGenerator.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbQuerySqlGenerator.cs @@ -5,31 +5,40 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; -public class ParadeDbQuerySqlGenerator : NpgsqlQuerySqlGenerator { - public ParadeDbQuerySqlGenerator(QuerySqlGeneratorDependencies dependencies, +public class ParadeDbQuerySqlGenerator : NpgsqlQuerySqlGenerator +{ + public ParadeDbQuerySqlGenerator( + QuerySqlGeneratorDependencies dependencies, IRelationalTypeMappingSource typeMappingSource, bool reverseNullOrderingEnabled, - Version postgresVersion) - : base(dependencies, typeMappingSource, reverseNullOrderingEnabled, postgresVersion) { - } - - protected override Expression VisitExtension(Expression expression) { - if (expression is ParadeDbModifiedQueryExpression modified) { + Version postgresVersion + ) + : base(dependencies, typeMappingSource, reverseNullOrderingEnabled, postgresVersion) { } + + protected override Expression VisitExtension(Expression expression) + { + if (expression is ParadeDbModifiedQueryExpression modified) + { Visit(modified.InnerExpression); Sql.Append(modified.ModifierSuffix); return expression; } - if (expression is ParadeDbNamedArgFunctionExpression namedArg) { + if (expression is ParadeDbNamedArgFunctionExpression namedArg) + { Sql.Append(namedArg.FunctionName).Append("("); - for (var i = 0; i < namedArg.PositionalArgs.Count; i++) { - if (i > 0) Sql.Append(", "); + for (var i = 0; i < namedArg.PositionalArgs.Count; i++) + { + if (i > 0) + Sql.Append(", "); Visit(namedArg.PositionalArgs[i]); } - for (var i = 0; i < namedArg.NamedArgs.Count; i++) { - if (i > 0 || namedArg.PositionalArgs.Count > 0) Sql.Append(", "); + for (var i = 0; i < namedArg.NamedArgs.Count; i++) + { + if (i > 0 || namedArg.PositionalArgs.Count > 0) + Sql.Append(", "); Sql.Append(namedArg.NamedArgs[i].Name).Append(" => "); Visit(namedArg.NamedArgs[i].Value); } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbQuerySqlGeneratorFactory.cs b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbQuerySqlGeneratorFactory.cs index 46ed022..88ab98f 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbQuerySqlGeneratorFactory.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbQuerySqlGeneratorFactory.cs @@ -5,15 +5,19 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; -public class ParadeDbQuerySqlGeneratorFactory : NpgsqlQuerySqlGeneratorFactory { +public class ParadeDbQuerySqlGeneratorFactory : NpgsqlQuerySqlGeneratorFactory +{ private readonly QuerySqlGeneratorDependencies _dependencies; private readonly IRelationalTypeMappingSource _typeMappingSource; private readonly INpgsqlSingletonOptions _npgsqlOptions; - public ParadeDbQuerySqlGeneratorFactory(QuerySqlGeneratorDependencies dependencies, + public ParadeDbQuerySqlGeneratorFactory( + QuerySqlGeneratorDependencies dependencies, IRelationalTypeMappingSource typeMappingSource, - INpgsqlSingletonOptions npgsqlOptions) - : base(dependencies, typeMappingSource, npgsqlOptions) { + INpgsqlSingletonOptions npgsqlOptions + ) + : base(dependencies, typeMappingSource, npgsqlOptions) + { _dependencies = dependencies; _typeMappingSource = typeMappingSource; _npgsqlOptions = npgsqlOptions; @@ -24,5 +28,6 @@ public override QuerySqlGenerator Create() => _dependencies, _typeMappingSource, _npgsqlOptions.ReverseNullOrderingEnabled, - _npgsqlOptions.PostgresVersion); + _npgsqlOptions.PostgresVersion + ); } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbSearchExtensions.cs b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbSearchExtensions.cs index a26957d..ed5a02c 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbSearchExtensions.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbSearchExtensions.cs @@ -7,19 +7,29 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; /// /// IQueryable extension methods for ParadeDB JSON query search and BM25 score ordering. /// -public static class ParadeDbSearchExtensions { - private static readonly MethodInfo JsonSearchMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.JsonSearch), [typeof(DbFunctions), typeof(object), typeof(string)])!; +public static class ParadeDbSearchExtensions +{ + private static readonly MethodInfo JsonSearchMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.JsonSearch), + [typeof(DbFunctions), typeof(object), typeof(string)] + )!; - private static readonly MethodInfo ScoreMethod = typeof(ParadeDbFunctions) - .GetMethod(nameof(ParadeDbFunctions.Score), [typeof(DbFunctions), typeof(object)])!; + private static readonly MethodInfo ScoreMethod = typeof(ParadeDbFunctions).GetMethod( + nameof(ParadeDbFunctions.Score), + [typeof(DbFunctions), typeof(object)] + )!; /// /// Adds a WHERE clause with a ParadeDB boolean JSON query built inline. /// Translates to: keyField @@@ 'json'::jsonb. /// - public static IQueryable JsonSearch(this IQueryable source, - Expression> keyField, Action configure) where T : class { + public static IQueryable JsonSearch( + this IQueryable source, + Expression> keyField, + Action configure + ) + where T : class + { return source.JsonSearch(keyField, ParadeDbJsonQuery.Boolean(configure)); } @@ -27,8 +37,13 @@ public static IQueryable JsonSearch(this IQueryable source, /// Adds a WHERE clause with a ParadeDB JSON query. /// Translates to: keyField @@@ 'json'::jsonb. /// - public static IQueryable JsonSearch(this IQueryable source, - Expression> keyField, ParadeDbJsonQuery query) where T : class { + public static IQueryable JsonSearch( + this IQueryable source, + Expression> keyField, + ParadeDbJsonQuery query + ) + where T : class + { var efFunctionsExpr = Expression.Property(null, typeof(EF), nameof(EF.Functions)); var keyBody = BoxIfNeeded(StripConvert(keyField.Body)); var jsonConstant = Expression.Constant(query.ToJson(), typeof(string)); @@ -42,28 +57,42 @@ public static IQueryable JsonSearch(this IQueryable source, /// /// Orders by BM25 score descending (highest relevance first). /// - public static IOrderedQueryable OrderByScoreDescending(this IQueryable source, - Expression> keyField) where T : class { + public static IOrderedQueryable OrderByScoreDescending( + this IQueryable source, + Expression> keyField + ) + where T : class + { return ApplyScoreOrdering(source, keyField, nameof(Queryable.OrderByDescending)); } /// /// Orders by BM25 score ascending. /// - public static IOrderedQueryable OrderByScore(this IQueryable source, - Expression> keyField) where T : class { + public static IOrderedQueryable OrderByScore( + this IQueryable source, + Expression> keyField + ) + where T : class + { return ApplyScoreOrdering(source, keyField, nameof(Queryable.OrderBy)); } - private static IOrderedQueryable ApplyScoreOrdering(IQueryable source, - Expression> keyField, string methodName) where T : class { + private static IOrderedQueryable ApplyScoreOrdering( + IQueryable source, + Expression> keyField, + string methodName + ) + where T : class + { var efFunctionsExpr = Expression.Property(null, typeof(EF), nameof(EF.Functions)); var keyBody = BoxIfNeeded(StripConvert(keyField.Body)); var scoreCall = Expression.Call(ScoreMethod, efFunctionsExpr, keyBody); var scoreSelector = Expression.Lambda>(scoreCall, keyField.Parameters); - var orderByMethod = typeof(Queryable).GetMethods() + var orderByMethod = typeof(Queryable) + .GetMethods() .First(m => m.Name == methodName && m.GetParameters().Length == 2) .MakeGenericMethod(typeof(T), typeof(double)); @@ -77,7 +106,5 @@ private static Expression StripConvert(Expression expression) => : expression; private static Expression BoxIfNeeded(Expression expression) => - expression.Type.IsValueType - ? Expression.Convert(expression, typeof(object)) - : expression; + expression.Type.IsValueType ? Expression.Convert(expression, typeof(object)) : expression; } diff --git a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbSqlNullabilityProcessor.cs b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbSqlNullabilityProcessor.cs index 6094bf2..771bfbb 100644 --- a/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbSqlNullabilityProcessor.cs +++ b/Equibles.ParadeDB.EntityFrameworkCore/ParadeDbSqlNullabilityProcessor.cs @@ -4,50 +4,76 @@ namespace Equibles.ParadeDB.EntityFrameworkCore; -public class ParadeDbSqlNullabilityProcessor : NpgsqlSqlNullabilityProcessor { +public class ParadeDbSqlNullabilityProcessor : NpgsqlSqlNullabilityProcessor +{ #if NET8_0 - public ParadeDbSqlNullabilityProcessor(RelationalParameterBasedSqlProcessorDependencies dependencies, - bool useRelationalNulls) - : base(dependencies, useRelationalNulls) { - } + public ParadeDbSqlNullabilityProcessor( + RelationalParameterBasedSqlProcessorDependencies dependencies, + bool useRelationalNulls + ) + : base(dependencies, useRelationalNulls) { } #else - public ParadeDbSqlNullabilityProcessor(RelationalParameterBasedSqlProcessorDependencies dependencies, - RelationalParameterBasedSqlProcessorParameters parameters) - : base(dependencies, parameters) { - } + public ParadeDbSqlNullabilityProcessor( + RelationalParameterBasedSqlProcessorDependencies dependencies, + RelationalParameterBasedSqlProcessorParameters parameters + ) + : base(dependencies, parameters) { } #endif - protected override SqlExpression VisitCustomSqlExpression(SqlExpression sqlExpression, - bool allowOptimizedExpansion, out bool nullable) { - if (sqlExpression is ParadeDbModifiedQueryExpression modified) { + protected override SqlExpression VisitCustomSqlExpression( + SqlExpression sqlExpression, + bool allowOptimizedExpansion, + out bool nullable + ) + { + if (sqlExpression is ParadeDbModifiedQueryExpression modified) + { nullable = false; var inner = Visit(modified.InnerExpression, allowOptimizedExpansion, out _); return inner == modified.InnerExpression ? modified - : new ParadeDbModifiedQueryExpression(inner, modified.ModifierSuffix, modified.Type, modified.TypeMapping); + : new ParadeDbModifiedQueryExpression( + inner, + modified.ModifierSuffix, + modified.Type, + modified.TypeMapping + ); } - if (sqlExpression is ParadeDbNamedArgFunctionExpression namedArg) { + if (sqlExpression is ParadeDbNamedArgFunctionExpression namedArg) + { nullable = true; var positionalChanged = false; var newPositional = new SqlExpression[namedArg.PositionalArgs.Count]; - for (var i = 0; i < namedArg.PositionalArgs.Count; i++) { - newPositional[i] = Visit(namedArg.PositionalArgs[i], allowOptimizedExpansion, out _); + for (var i = 0; i < namedArg.PositionalArgs.Count; i++) + { + newPositional[i] = Visit( + namedArg.PositionalArgs[i], + allowOptimizedExpansion, + out _ + ); positionalChanged |= newPositional[i] != namedArg.PositionalArgs[i]; } var namedChanged = false; var newNamed = new (string Name, SqlExpression Value)[namedArg.NamedArgs.Count]; - for (var i = 0; i < namedArg.NamedArgs.Count; i++) { + for (var i = 0; i < namedArg.NamedArgs.Count; i++) + { var visited = Visit(namedArg.NamedArgs[i].Value, allowOptimizedExpansion, out _); newNamed[i] = (namedArg.NamedArgs[i].Name, visited); namedChanged |= visited != namedArg.NamedArgs[i].Value; } - if (!positionalChanged && !namedChanged) return namedArg; - return new ParadeDbNamedArgFunctionExpression(namedArg.FunctionName, newPositional, newNamed, - namedArg.Type, namedArg.TypeMapping); + if (!positionalChanged && !namedChanged) + return namedArg; + return new ParadeDbNamedArgFunctionExpression( + namedArg.FunctionName, + newPositional, + newNamed, + namedArg.Type, + namedArg.TypeMapping + ); } return base.VisitCustomSqlExpression(sqlExpression, allowOptimizedExpansion, out nullable);