From b70c3364f13c7f68408734707995289eebdf0995 Mon Sep 17 00:00:00 2001 From: Jake Rote Date: Thu, 12 May 2016 21:05:07 +0100 Subject: [PATCH] First phase of Queryable Provider --- .../RethinkDb.Driver.Linq.Tests/AllTests.cs | 137 +++++++ .../RethinkDb.Driver.Linq.Tests/AnyTests.cs | 236 ++++++++++++ .../AverageTests.cs | 116 ++++++ .../BaseLinqTest.cs | 109 ++++++ .../ComplexQueryTests.cs | 68 ++++ .../ContainsTests.cs | 45 +++ .../RethinkDb.Driver.Linq.Tests/CountTests.cs | 144 +++++++ .../FirstOrDefaultTests.cs | 230 ++++++++++++ .../RethinkDb.Driver.Linq.Tests/FirstTests.cs | 128 +++++++ .../GroupByTests.cs | 152 ++++++++ .../LastOrDefaultTests.cs | 84 +++++ .../RethinkDb.Driver.Linq.Tests/LastTests.cs | 86 +++++ .../OrderByTests.cs | 147 ++++++++ .../Properties/AssemblyInfo.cs | 25 ++ .../RethinkDb.Driver.Linq.Tests.xproj | 21 ++ .../TestRethinkQueryExecuter.cs | 21 ++ .../WhereComparisonTests.cs | 352 ++++++++++++++++++ .../RethinkDb.Driver.Linq.Tests/project.json | 35 ++ .../Attributes/PrimaryIndexAttribute.cs | 9 + .../Attributes/SecondaryIndexAttribute.cs | 8 + .../RethinkDb.Driver.Linq/LinqExtensions.cs | 26 ++ .../MemberNameResolver.cs | 48 +++ .../Properties/AssemblyInfo.cs | 22 ++ .../RethinkDb.Driver.Linq.xproj | 20 + .../RethinkDb.Driver.Linq/RethinkDbGroup.cs | 23 ++ .../RethinkDbQueryModelVisitor.cs | 186 +++++++++ .../RethinkQueryExecutor.cs | 125 +++++++ .../RethinkDb.Driver.Linq/RethinkQueryable.cs | 22 ++ .../SelectionProjector.cs | 26 ++ .../WhereClauseParsers/BaseIndexParser.cs | 40 ++ .../WhereClauseParsers/DefaultParser.cs | 32 ++ .../WhereClauseParsers/ExprHelper.cs | 60 +++ .../WhereClauseParsers/ExpressionVisitor.cs | 111 ++++++ .../WhereClauseParsers/GroupItemsParser.cs | 49 +++ .../WhereClauseParsers/IWhereClauseParser.cs | 13 + .../WhereClauseParsers/Poco.cs | 46 +++ .../WhereClauseParsers/PrimaryIndexParser.cs | 18 + .../SecondaryIndexParser.cs | 24 ++ .../SubQueryVisitor/AllSubQueryVisitor.cs | 19 + .../SubQueryVisitor/AnySubQueryVisitor.cs | 11 + .../BaseFilterableSubQueryVisitor.cs | 23 ++ .../SubQueryVisitor/BaseSubQueryVisitor.cs | 33 ++ .../ContainsSubQueryVisitor.cs | 22 ++ .../SubQueryVisitor/CountSubQueryVisitor.cs | 11 + .../SubQueryVisitor/FirstSubQueryVisitor.cs | 24 ++ .../SubQueryVisitor/ISubQueryVisitor.cs | 11 + .../WhereClauseParsers/Util.cs | 140 +++++++ Source/RethinkDb.Driver.Linq/project.json | 24 ++ Source/RethinkDb.Driver.sln | 14 +- 49 files changed, 3375 insertions(+), 1 deletion(-) create mode 100644 Source/RethinkDb.Driver.Linq.Tests/AllTests.cs create mode 100644 Source/RethinkDb.Driver.Linq.Tests/AnyTests.cs create mode 100644 Source/RethinkDb.Driver.Linq.Tests/AverageTests.cs create mode 100644 Source/RethinkDb.Driver.Linq.Tests/BaseLinqTest.cs create mode 100644 Source/RethinkDb.Driver.Linq.Tests/ComplexQueryTests.cs create mode 100644 Source/RethinkDb.Driver.Linq.Tests/ContainsTests.cs create mode 100644 Source/RethinkDb.Driver.Linq.Tests/CountTests.cs create mode 100644 Source/RethinkDb.Driver.Linq.Tests/FirstOrDefaultTests.cs create mode 100644 Source/RethinkDb.Driver.Linq.Tests/FirstTests.cs create mode 100644 Source/RethinkDb.Driver.Linq.Tests/GroupByTests.cs create mode 100644 Source/RethinkDb.Driver.Linq.Tests/LastOrDefaultTests.cs create mode 100644 Source/RethinkDb.Driver.Linq.Tests/LastTests.cs create mode 100644 Source/RethinkDb.Driver.Linq.Tests/OrderByTests.cs create mode 100644 Source/RethinkDb.Driver.Linq.Tests/Properties/AssemblyInfo.cs create mode 100644 Source/RethinkDb.Driver.Linq.Tests/RethinkDb.Driver.Linq.Tests.xproj create mode 100644 Source/RethinkDb.Driver.Linq.Tests/TestRethinkQueryExecuter.cs create mode 100644 Source/RethinkDb.Driver.Linq.Tests/WhereComparisonTests.cs create mode 100644 Source/RethinkDb.Driver.Linq.Tests/project.json create mode 100644 Source/RethinkDb.Driver.Linq/Attributes/PrimaryIndexAttribute.cs create mode 100644 Source/RethinkDb.Driver.Linq/Attributes/SecondaryIndexAttribute.cs create mode 100644 Source/RethinkDb.Driver.Linq/LinqExtensions.cs create mode 100644 Source/RethinkDb.Driver.Linq/MemberNameResolver.cs create mode 100644 Source/RethinkDb.Driver.Linq/Properties/AssemblyInfo.cs create mode 100644 Source/RethinkDb.Driver.Linq/RethinkDb.Driver.Linq.xproj create mode 100644 Source/RethinkDb.Driver.Linq/RethinkDbGroup.cs create mode 100644 Source/RethinkDb.Driver.Linq/RethinkDbQueryModelVisitor.cs create mode 100644 Source/RethinkDb.Driver.Linq/RethinkQueryExecutor.cs create mode 100644 Source/RethinkDb.Driver.Linq/RethinkQueryable.cs create mode 100644 Source/RethinkDb.Driver.Linq/SelectionProjector.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/BaseIndexParser.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/DefaultParser.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/ExprHelper.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/ExpressionVisitor.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/GroupItemsParser.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/IWhereClauseParser.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/Poco.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/PrimaryIndexParser.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/SecondaryIndexParser.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/AllSubQueryVisitor.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/AnySubQueryVisitor.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/BaseFilterableSubQueryVisitor.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/BaseSubQueryVisitor.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/ContainsSubQueryVisitor.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/CountSubQueryVisitor.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/FirstSubQueryVisitor.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/ISubQueryVisitor.cs create mode 100644 Source/RethinkDb.Driver.Linq/WhereClauseParsers/Util.cs create mode 100644 Source/RethinkDb.Driver.Linq/project.json diff --git a/Source/RethinkDb.Driver.Linq.Tests/AllTests.cs b/Source/RethinkDb.Driver.Linq.Tests/AllTests.cs new file mode 100644 index 0000000..1fd3037 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/AllTests.cs @@ -0,0 +1,137 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace RethinkDb.Driver.Linq.Tests +{ + public class AllTests : BaseLinqTest + { + [Fact] + public void ForAll_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject1" + }, + new TestObject + { + Name = "TestObject1" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Filter( x => x["Name"].Eq( "TestObject1" ).Not() ).Count(); + + var result = GetQueryable( TableName, expected ).All( x => x.Name == "TestObject1" ); + + Assert.True( result ); + } + + [Fact] + public void ForAllOnSubProperty_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Information = new Information + { + Name = "TestObject1" + } + }, + new TestObject + { + Information = new Information + { + Name = "TestObject1" + } + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Filter( x => x["Information"]["Name"].Eq( "TestObject1" ).Not() ).Count(); + + var result = GetQueryable( TableName, expected ).All( x => x.Information.Name == "TestObject1" ); + + Assert.True( result ); + } + + [Fact] + public void WhenIsFilteredByAllClause_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject1", + Locations = new List {"Hello" } + }, + new TestObject + { + Name = "TestObject2", + Locations = new List { "Hello2" } + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .Filter( x => x["Locations"].Filter( l => l.Eq( "Hello" ).Not() ).Count().Eq( 0 ) ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => x.Locations.All( l => l == "Hello" ) ) + .ToList(); + + Assert.Equal( 1, result.Count ); + } + + [Fact] + public void WhenIsFilteredByAllClauseNot_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject1", + Locations = new List {"Hello" } + }, + new TestObject + { + Name = "TestObject2", + Locations = new List { "Hello2" } + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .Filter( x => x["Locations"].Filter( l => l.Eq( "Hello" ).Not() ).Count().Eq( 0 ).Not() ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => !x.Locations.All( l => l == "Hello" ) ) + .ToList(); + + Assert.Equal( 1, result.Count ); + } + + public class TestObject + { + public string Name { get; set; } + public Information Information { get; set; } + public List Locations { get; set; } + } + } + + public class Information + { + public string Name { get; set; } + } +} diff --git a/Source/RethinkDb.Driver.Linq.Tests/AnyTests.cs b/Source/RethinkDb.Driver.Linq.Tests/AnyTests.cs new file mode 100644 index 0000000..2ce823f --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/AnyTests.cs @@ -0,0 +1,236 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace RethinkDb.Driver.Linq.Tests +{ + public class AnyTests : BaseLinqTest + { + [Fact] + public void ForSimpleAnyQuery_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject1" + }, + new TestObject + { + Name = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Count(); + + var result = GetQueryable( TableName, expected ).Any(); + + Assert.True( result ); + } + + [Fact] + public void ForSimpleAnyQueryWithCondition_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject1" + }, + new TestObject + { + Name = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Filter( x => x["Name"].Eq( "TestObject1" ) ).Count(); + + var result = GetQueryable( TableName, expected ).Any( x => x.Name == "TestObject1" ); + + Assert.True( result ); + } + + [Fact] + public void WhenIsFilteredByAnyClauseAndNot_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject1", + Locations = new List {"Hello" } + }, + new TestObject + { + Name = "TestObject2", + Locations = new List() + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .Filter( x => x["Locations"].Count().Gt( 0 ).Not() ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => !x.Locations.Any() ) + .ToList(); + + Assert.Equal( 1, result.Count ); + } + + [Fact] + public void WhenIsFilteredByAnyClauseWithSubAny_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject1", + Locations = new List {"Hello"} + }, + new TestObject + { + Name = "TestObject2", + Resources = new List + { + new Resource + { + Locations = new List + { + new Location + { + Usages = new List + { + "First usage" + } + } + } + } + } + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .Filter( x => x["Resources"].Filter( resource => resource["Locations"].Filter( location => location["Usages"].Count().Gt( 0 ) ).Count().Gt( 0 ) ).Count().Gt( 0 ) ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => x.Resources.Any( resource => resource.Locations.Any( l => l.Usages.Any() ) ) ) + .ToList(); + + Assert.Equal( 1, result.Count ); + } + + [Fact] + public void WhenIsFilteredByAnyClauseAndEqualsFalse_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject1", + Locations = new List {"Hello" } + }, + new TestObject + { + Name = "TestObject2", + Locations = new List() + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .Filter( x => x["Locations"].Count().Gt( 0 ).Eq( false ) ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => x.Locations.Any() == false ) + .ToList(); + + Assert.Equal( 1, result.Count ); + Assert.Equal( 0, result[0].Locations.Count ); + } + + + [Fact] + public void WhenIsFilteredByAnyClause_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject1", + Locations = new List {"Hello" } + }, + new TestObject + { + Name = "TestObject2", + Locations = new List() + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .Filter( x => x["Locations"].Count().Gt( 0 ) ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => x.Locations.Any() ) + .ToList(); + + Assert.Equal( 1, result.Count ); + } + + [Fact] + public void WhenIsFilteredByAnyClauseWithFilter_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject1", + Locations = new List {"Hello" } + }, + new TestObject + { + Name = "TestObject2", + Locations = new List {"Hello1" } + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .Filter( x => x["Locations"].Filter( l => l.Eq( "Hello" ) ).Count().Gt( 0 ) ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => x.Locations.Any( l => l == "Hello" ) ) + .ToList(); + + Assert.Equal( 1, result.Count ); + } + + public class TestObject + { + public string Name { get; set; } + public List Locations { get; set; } + public List Resources { get; set; } + } + } +} diff --git a/Source/RethinkDb.Driver.Linq.Tests/AverageTests.cs b/Source/RethinkDb.Driver.Linq.Tests/AverageTests.cs new file mode 100644 index 0000000..ad3db1c --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/AverageTests.cs @@ -0,0 +1,116 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace RethinkDb.Driver.Linq.Tests +{ + public class AverageTests : BaseLinqTest + { + [Fact] + public void ForSimpleAverage_GeneratesCorrectQuery() + { + var data = new List + { + new TestObject + { + Size = 1 + }, + new TestObject + { + Size = 3 + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Avg( x => x["Size"] ); + + var result = GetQueryable( TableName, expected ).Average( x => x.Size ); + + Assert.Equal( 2, result ); + } + + [Fact] + public void ForSimpleAverageForSubProperty_GeneratesCorrectQuery() + { + var data = new List + { + new TestObject + { + Information = new Information + { + Size = 1 + } + }, + new TestObject + { + Information = new Information + { + Size = 3 + } + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Avg( x => x["Information"]["Size"] ); + + var result = GetQueryable( TableName, expected ).Average( x => x.Information.Size ); + + Assert.Equal( 2, result ); + } + + [Fact] + public void ForSimpleAverageForSubSubProperty_GeneratesCorrectQuery() + { + var data = new List + { + new TestObject + { + MainInformation = new MainInformation + { + Information = new Information + { + Size = 1 + } + } + }, + new TestObject + { + MainInformation = new MainInformation + { + Information = new Information + { + Size = 3 + } + } + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Avg( x => x["MainInformation"]["Information"]["Size"] ); + + var result = GetQueryable( TableName, expected ).Average( x => x.MainInformation.Information.Size ); + + Assert.Equal( 2, result ); + } + + public class MainInformation + { + public Information Information { get; set; } + } + + public class Information + { + public int Size { get; set; } + } + + public class TestObject + { + public int Size { get; set; } + public Information Information { get; set; } + public MainInformation MainInformation { get; set; } + } + } +} diff --git a/Source/RethinkDb.Driver.Linq.Tests/BaseLinqTest.cs b/Source/RethinkDb.Driver.Linq.Tests/BaseLinqTest.cs new file mode 100644 index 0000000..114ad88 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/BaseLinqTest.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Remotion.Linq; +using Remotion.Linq.Parsing.Structure; +using RethinkDb.Driver.Ast; +using RethinkDb.Driver.Linq.Attributes; +using RethinkDb.Driver.Net; +using RethinkDb.Driver.Proto; +using Xunit; + +namespace RethinkDb.Driver.Linq.Tests +{ + public class BaseLinqTest : IDisposable + { + protected readonly string TableName; + protected readonly IConnection Connection; + + public BaseLinqTest() + { + TableName = Guid.NewGuid().ToString().Replace( "-", "" ); + Connection = SetupConnection(); + } + + private void QueriesAreTheSame( ReqlAst expected, ReqlAst actual ) + { + var buildMethod = typeof( ReqlAst ).GetMethod( "Build", BindingFlags.Instance | BindingFlags.NonPublic ); + + var expectedJson = ( (JArray)buildMethod.Invoke( expected, new object[0] ) ).ToString( Formatting.None ); + var actualJson = ( (JArray)buildMethod.Invoke( actual, new object[0] ) ).ToString( Formatting.None ); + + var removeIdsRegex = new Regex( @"\[\d+\]" ); + + var expectedParsed = removeIdsRegex.Replace( ParseReql( expectedJson ), "" ); + var actualParsed = removeIdsRegex.Replace( ParseReql( actualJson ), "" ); + + Assert.Equal( expectedParsed, actualParsed ); + } + + private static string ParseReql( string expectedJson ) + { + var replacer = new Regex( @"(?<=\[)\d+(?!\d*])" ); + return replacer.Replace( expectedJson, m => + { + TermType termType; + if( Enum.TryParse( m.Value, out termType ) ) + { + return m.Result( termType.ToString() ); + } + return m.Value; + } ); + } + + protected RethinkQueryable GetQueryable( string table,ReqlAst expected ) + { + var executor = new TestRethinkQueryExecutor( RethinkDB.R.Table( table ), Connection, reql => + { + QueriesAreTheSame( expected, reql ); + } ); + return new RethinkQueryable( + new DefaultQueryProvider( + typeof( RethinkQueryable<> ), + QueryParser.CreateDefault(), + executor ) + ); + } + + protected void SpawnData( List data ) + { + var reql = RethinkDB.R.TableCreate( TableName ); + + var primaryIndex = typeof( T ).GetProperties().FirstOrDefault( x => x.CustomAttributes.Any( a => a.AttributeType == typeof( PrimaryIndexAttribute ) ) ); + if( primaryIndex != null ) + reql = reql.OptArg( "primary_key", primaryIndex.Name ); + reql.Run( Connection ); + + var secondaryIndexes = typeof( T ).GetProperties().Where( x => x.CustomAttributes.Any( a => a.AttributeType == typeof( SecondaryIndexAttribute ) ) ); + foreach( var secondaryIndex in secondaryIndexes ) + { + RethinkDB.R.Table( TableName ).IndexCreate( secondaryIndex.Name ).Run( Connection ); + } + RethinkDB.R.Table( TableName ).IndexWait().Run( Connection ); + + + foreach( var testObject in data ) + RethinkDB.R.Table( TableName ).Insert( testObject ).Run( Connection ); + } + + private static Connection SetupConnection() + { + return RethinkDB.R.Connection() + .Db( "tests" ) + .Hostname( RethinkDBConstants.DefaultHostname ) + .Port( RethinkDBConstants.DefaultPort ) + .Timeout( RethinkDBConstants.DefaultTimeout ) + .Connect(); + } + + public void Dispose() + { + RethinkDB.R.TableDrop( TableName ).Run( Connection ); + Connection.Dispose(); + } + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq.Tests/ComplexQueryTests.cs b/Source/RethinkDb.Driver.Linq.Tests/ComplexQueryTests.cs new file mode 100644 index 0000000..a61174f --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/ComplexQueryTests.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using RethinkDb.Driver.Linq.Attributes; +using Xunit; + +namespace RethinkDb.Driver.Linq.Tests +{ + public class ComplexQueryTests : BaseLinqTest + { + [Fact] + public void ComplexQuery() + { + var data = new List + { + new ComplexObject + { + Name = "My First Name", + Length = 10, + CreatedDate = new DateTime( 2016, 1, 2 ) + }, + new ComplexObject + { + Name = "My First Name", + Length = 10, + CreatedDate = new DateTime( 2016, 1, 1 ) + }, + new ComplexObject + { + Name = "My First Name", + Length = 5, + CreatedDate = new DateTime( 2016, 1, 1 ) + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .GetAll( 10 ) + .OptArg( "index", "Length" ) + .Filter( x => x["Name"].Eq( "My First Name" ) ) + .Filter( x => x["Length"].Gt( 10 ) ) + .OrderBy( "CreatedDate" ); + + var queryable = GetQueryable( TableName, expected ); + + var result = ( from complexObject in queryable + where complexObject.Length == 10 + where complexObject.Name == "My First Name" + where complexObject.Length > 10 + orderby complexObject.CreatedDate + select complexObject ).ToList(); + + Assert.NotNull( result ); + } + + public class ComplexObject + { + public string Name { get; set; } + + [SecondaryIndex] + public int Length { get; set; } + + [SecondaryIndex] + public DateTime CreatedDate { get; set; } + } + } +} diff --git a/Source/RethinkDb.Driver.Linq.Tests/ContainsTests.cs b/Source/RethinkDb.Driver.Linq.Tests/ContainsTests.cs new file mode 100644 index 0000000..ebffe44 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/ContainsTests.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace RethinkDb.Driver.Linq.Tests +{ + public class ContainsTests : BaseLinqTest + { + [Fact] + public void ContainsForProperty_ReturnsCorrectReql() + { + var strings = new[] + { + "Hello" + }; + + var data = new List + { + new TestObject + { + Name = "Hello" + } + }; + ; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .Filter( x => RethinkDB.R.Expr( RethinkDB.R.Array( strings ) ).Contains( x["Name"] ) ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => strings.Contains( x.Name ) ) + .ToList(); + + Assert.Equal( 1, result.Count ); + } + + class TestObject + { + public string Name { get; set; } + } + } +} diff --git a/Source/RethinkDb.Driver.Linq.Tests/CountTests.cs b/Source/RethinkDb.Driver.Linq.Tests/CountTests.cs new file mode 100644 index 0000000..359d42f --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/CountTests.cs @@ -0,0 +1,144 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace RethinkDb.Driver.Linq.Tests +{ + public class CountTests : BaseLinqTest + { + [Fact] + public void Count_GeneratesCorrectReql() + { + var data = new List + { + new TestObject(), + new TestObject() + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Count(); + + var result = GetQueryable( TableName, expected ).Count(); + + Assert.Equal( 2, result ); + } + + [Fact] + public void CountWithFilter_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "One" + }, + new TestObject + { + Name = "Two" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Filter( x => x["Name"].Eq( "One" ) ).Count(); + + var result = GetQueryable( TableName, expected ).Count( x => x.Name == "One" ); + + Assert.Equal( 1, result ); + } + + [Fact] + public void WhereWithCount_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "One" + }, + new TestObject + { + Name = "Two", + Values = new List + { + "One", + "Two" + } + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Filter( x => x["Values"].Count().Eq( 2 ) ); + + var result = GetQueryable( TableName, expected ).Where( x => x.Values.Count() == 2 ).ToList(); + + Assert.Equal( 1, result.Count ); + } + + [Fact] + public void WhereWithListCount_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "One" + }, + new TestObject + { + Name = "Two", + Values = new List + { + "One", + "Two" + } + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Filter( x => x["Values"].Count().Eq( 2 ) ); + + var result = GetQueryable( TableName, expected ).Where( x => x.Values.Count == 2 ).ToList(); + + Assert.Equal( 1, result.Count ); + } + + [Fact] + public void WhereWithCountWithFilter_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "One" + }, + new TestObject + { + Name = "Two", + Values = new List + { + "One", + "Two" + } + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Filter( x => x["Values"].Filter( v => v.Eq( "One" ) ).Count().Eq( 1 ) ); + + var result = GetQueryable( TableName, expected ).Where( x => x.Values.Count( v => v == "One" ) == 1 ).ToList(); + + Assert.Equal( 1, result.Count ); + } + + public class TestObject + { + public string Name { get; set; } + public List Values { get; set; } + } + } +} diff --git a/Source/RethinkDb.Driver.Linq.Tests/FirstOrDefaultTests.cs b/Source/RethinkDb.Driver.Linq.Tests/FirstOrDefaultTests.cs new file mode 100644 index 0000000..e5ab044 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/FirstOrDefaultTests.cs @@ -0,0 +1,230 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace RethinkDb.Driver.Linq.Tests +{ + public class FirstOrDefaultTests : BaseLinqTest + { + [Fact] + public void FirstOrDefaultWithNoFilter_GeneratesCorrectQuery() + { + var data = new List + { + new TestObject + { + Name = "TestObject1" + }, + new TestObject + { + Name = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Nth( 0 ); + + var result = GetQueryable( TableName, expected ).FirstOrDefault(); + } + + [Fact] + public void FirstWithFilter_GeneratesCorrectQuery() + { + var data = new List + { + new TestObject + { + Name = "TestObject1" + }, + new TestObject + { + Name = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Filter( x => x["Name"].Eq( "TestObject2" ) ).Nth( 0 ); + + var result = GetQueryable( TableName, expected ).FirstOrDefault( x => x.Name == "TestObject2" ); + + Assert.Equal( "TestObject2", result.Name ); + } + + + [Fact] + public void WhenIsFilteredByFirstOrDefaultClause_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject2", + Resources = new List + { + new Resource + { + Locations = new List + { + new Location() + } + } + } + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .Filter( x => x["Resources"].Nth( 0 ).Default_( (object)null ).Eq( null ).Not() ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => x.Resources.FirstOrDefault() != null ) + .ToList(); + + Assert.Equal( 1, result.Count ); + } + + [Fact] + public void FirstWithFilterAndNoMatches_ReturnsNull() + { + var data = new List + { + new TestObject + { + Name = "TestObject1" + }, + new TestObject + { + Name = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Filter( x => x["Name"].Eq( "TestObject3" ) ).Nth( 0 ); + + var result = GetQueryable( TableName, expected ).FirstOrDefault( x => x.Name == "TestObject3" ); + + Assert.Null( result ); + } + + [Fact] + public void WhenIsFilteredByFirstClause_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject1", + Locations = new List {"Hello"} + }, + new TestObject + { + Name = "TestObject2", + Locations = new List {"Hello2"} + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .Filter( x => x["Locations"].Nth( 0 ).Eq( "Hello" ) ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => x.Locations.First() == "Hello" ) + .ToList(); + + Assert.Equal( 1, result.Count ); + Assert.Equal( 1, result[0].Locations.Count ); + } + + [Fact] + public void WhenIsFilteredByFirstClauseAndThenUsingSubProperty_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject2", + Resources = new List + { + new Resource + { + Locations = new List + { + new Location() + } + } + } + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .Filter( x => x["Resources"].Nth( 0 )["Locations"].Count().Gt( 0 ) ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => x.Resources.First().Locations.Any() ) + .ToList(); + + Assert.Equal( 1, result.Count ); + } + + [Fact] + public void WhenIsFilteredByFirstClauseAndThenUsingSubPropertyComplex_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject2", + Resources = new List + { + new Resource + { + Locations = new List + { + new Location + { + Usages = new List + { + "Main" + } + } + } + } + } + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .Filter( x => x["Resources"].Nth( 0 )["Locations"].Nth( 0 )["Usages"].Filter( u => u.Eq( "Main" ) ).Count().Gt( 0 ) ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => x.Resources.First().Locations.First().Usages.Any( u => u == "Main" ) ) + .ToList(); + + Assert.Equal( 1, result.Count ); + } + + public class TestObject + { + public string Name { get; set; } + public List Resources { get; set; } + public List Locations { get; set; } + } + } +} diff --git a/Source/RethinkDb.Driver.Linq.Tests/FirstTests.cs b/Source/RethinkDb.Driver.Linq.Tests/FirstTests.cs new file mode 100644 index 0000000..a62918e --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/FirstTests.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace RethinkDb.Driver.Linq.Tests +{ + public class FirstTests : BaseLinqTest + { + [Fact] + public void FirstWithNoFilter_GeneratesCorrectQuery() + { + var data = new List + { + new TestObject + { + Name = "TestObject1" + }, + new TestObject + { + Name = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Nth( 0 ); + + var result = GetQueryable( TableName, expected ).First(); + } + + [Fact] + public void FirstWithFilter_GeneratesCorrectQuery() + { + var data = new List + { + new TestObject + { + Name = "TestObject1" + }, + new TestObject + { + Name = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Filter( x => x["Name"].Eq( "TestObject2" ) ).Nth( 0 ); + + var result = GetQueryable( TableName, expected ).First( x => x.Name == "TestObject2" ); + + Assert.Equal( "TestObject2", result.Name ); + } + + [Fact] + public void FirstWithFilterAndNoMatches_ThrowExecpetion() + { + var data = new List + { + new TestObject + { + Name = "TestObject1" + }, + new TestObject + { + Name = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Filter( x => x["Name"].Eq( "TestObject3" ) ).Nth( 0 ); + + Assert.Throws( () => + { + var result = GetQueryable( TableName, expected ).First( x => x.Name == "TestObject3" ); + } ); + } + [Fact] + public void WhenIsFilteredByFirstWithFilter_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject2", + Resources = new List + { + new Resource + { + Name = "First", + Locations = new List + { + new Location + { + Usages = new List + { + "Main" + } + } + } + } + } + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .Filter( x => x["Resources"].Filter( r => r["Locations"].Filter( l => l["Usages"].Filter( u => u.Eq( "Main" ) ).Nth( 0 ).Default_( (object)null ).Eq( null ).Not() ).Count().Gt( 0 ) ).Nth( 0 )["Name"].Eq( "First" ) ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => x.Resources.First( r => r.Locations.Any( l => l.Usages.FirstOrDefault( u => u == "Main" ) != null ) ).Name == "First" ) + .ToList(); + + Assert.Equal( 1, result.Count ); + } + + public class TestObject + { + public string Name { get; set; } + public List Resources { get; set; } + } + } +} diff --git a/Source/RethinkDb.Driver.Linq.Tests/GroupByTests.cs b/Source/RethinkDb.Driver.Linq.Tests/GroupByTests.cs new file mode 100644 index 0000000..c1fa5a5 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/GroupByTests.cs @@ -0,0 +1,152 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace RethinkDb.Driver.Linq.Tests +{ + public class GroupByTests : BaseLinqTest + { + [Fact] + public void WhenGroupingOnPropertyGroupsData() + { + var data = new List + { + new GroupObject + { + Name = "Name1", + Area = "Area1" + }, + new GroupObject + { + Name = "Name2", + Area = "Area1" + }, + new GroupObject + { + Name = "Name3", + Area = "Area2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Group( "Area" ).Ungroup(); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .GroupBy( x => x.Area ) + .ToList(); + } + + [Fact] + public void WhenGroupingOnPropertyWithSelectorGroupsData() + { + var data = new List + { + new GroupObject + { + Name = "Name1", + Area = "Area1" + }, + new GroupObject + { + Name = "Name2", + Area = "Area1" + }, + new GroupObject + { + Name = "Name3", + Area = "Area2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Group( "Area" ).GetField( "Name" ).Ungroup(); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .GroupBy( x => x.Area, x => x.Name ) + .ToList(); + } + + [Fact] + public void WhenGroupingOnPropertyAndThenFilteringOnKeyGroupsData() + { + var data = new List + { + new GroupObject + { + Name = "Name1", + Area = "Area1" + }, + new GroupObject + { + Name = "Name2", + Area = "Area1" + }, + new GroupObject + { + Name = "Name3", + Area = "Area2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Group( "Area" ).Ungroup().Filter( x=>x["group"].Eq( "Area2" ) ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .GroupBy( x => x.Area ) + .Where( x => x.Key == "Area2" ) + .ToList(); + } + + [Fact] + public void WhenGroupingOnPropertyAndFilteringOnGroupList_GeneratesCorrectReql() + { + var data = new List + { + new GroupObject + { + Name = "Name1", + Area = "Area1" + }, + new GroupObject + { + Name = "Name2", + Area = "Area1" + }, + new GroupObject + { + Name = "Name3", + Area = "Area2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .Group( "Area" ) + .Ungroup() + .Filter( x => x["reduction"].Contains( obj => obj["Name"].Eq( "Name1" ) ) ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .GroupBy( x => x.Area ) + .Where( x => x.Any( g => g.Name == "Name1" ) ) + .ToList(); + } + + public class GroupObject + { + public string Area { get; set; } + public string Name { get; set; } + } + } +} diff --git a/Source/RethinkDb.Driver.Linq.Tests/LastOrDefaultTests.cs b/Source/RethinkDb.Driver.Linq.Tests/LastOrDefaultTests.cs new file mode 100644 index 0000000..3833088 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/LastOrDefaultTests.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace RethinkDb.Driver.Linq.Tests +{ + public class LastOrDefaultTests : BaseLinqTest + { + [Fact] + public void LastOrDefaultWithNoFilter_GeneratesCorrectQuery() + { + var data = new List + { + new TestObject + { + Name = "TestObject1" + }, + new TestObject + { + Name = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Nth( -1 ); + + var result = GetQueryable( TableName, expected ).LastOrDefault(); + } + + [Fact] + public void LastWithFilter_GeneratesCorrectQuery() + { + var data = new List + { + new TestObject + { + Name = "TestObject1" + }, + new TestObject + { + Name = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Filter( x => x["Name"].Eq( "TestObject2" ) ).Nth( -1 ); + + var result = GetQueryable( TableName, expected ).LastOrDefault( x => x.Name == "TestObject2" ); + + Assert.Equal( "TestObject2", result.Name ); + } + + [Fact] + public void LastWithFilterAndNoMatches_ReturnsNull() + { + var data = new List + { + new TestObject + { + Name = "TestObject1" + }, + new TestObject + { + Name = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Filter( x => x["Name"].Eq( "TestObject3" ) ).Nth( -1 ); + + var result = GetQueryable( TableName, expected ).LastOrDefault( x => x.Name == "TestObject3" ); + + Assert.Null( result ); + } + + public class TestObject + { + public string Name { get; set; } + } + } +} diff --git a/Source/RethinkDb.Driver.Linq.Tests/LastTests.cs b/Source/RethinkDb.Driver.Linq.Tests/LastTests.cs new file mode 100644 index 0000000..6f1c0df --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/LastTests.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace RethinkDb.Driver.Linq.Tests +{ + public class LastTests : BaseLinqTest + { + [Fact] + public void LastWithNoFilter_GeneratesCorrectQuery() + { + var data = new List + { + new TestObject + { + Name = "TestObject1" + }, + new TestObject + { + Name = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Nth( -1 ); + + var result = GetQueryable( TableName, expected ).Last(); + } + + [Fact] + public void LastWithFilter_GeneratesCorrectQuery() + { + var data = new List + { + new TestObject + { + Name = "TestObject1" + }, + new TestObject + { + Name = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Filter( x => x["Name"].Eq( "TestObject2" ) ).Nth( -1 ); + + var result = GetQueryable( TableName, expected ).Last( x => x.Name == "TestObject2" ); + + Assert.Equal( "TestObject2", result.Name ); + } + + [Fact] + public void LastWithFilterAndNoMatches_ThrowExecpetion() + { + var data = new List + { + new TestObject + { + Name = "TestObject1" + }, + new TestObject + { + Name = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Filter( x => x["Name"].Eq( "TestObject3" ) ).Nth( -1 ); + + Assert.Throws( () => + { + var result = GetQueryable( TableName, expected ).Last( x => x.Name == "TestObject3" ); + } ); + } + + public class TestObject + { + public string Name { get; set; } + } + } +} diff --git a/Source/RethinkDb.Driver.Linq.Tests/OrderByTests.cs b/Source/RethinkDb.Driver.Linq.Tests/OrderByTests.cs new file mode 100644 index 0000000..06e2144 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/OrderByTests.cs @@ -0,0 +1,147 @@ +using System.Collections.Generic; +using System.Linq; +using RethinkDb.Driver.Linq.Attributes; +using Xunit; + +namespace RethinkDb.Driver.Linq.Tests +{ + public class OrderByTests : BaseLinqTest + { + [Fact] + public void For1OrderBy_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject1", + Name2 = "1" + }, + new TestObject + { + Name = "TestObject2", + Name2 = "2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).OrderBy( "Name" ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .OrderBy( x => x.Name ) + .ToList(); + + Assert.Equal( 2, result.Count ); + Assert.Equal( data[0].Name, result[0].Name ); + Assert.Equal( data[1].Name, result[1].Name ); + } + + [Fact] + public void For2OrderByDescending_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject1", + Name2 = "Name1" + }, + new TestObject + { + Name = "TestObject2", + Name2 = "Name2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).OrderBy( RethinkDB.R.Desc("Name") ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .OrderByDescending( x => x.Name ) + .ToList(); + + Assert.Equal( 2, result.Count ); + Assert.Equal( data[1].Name, result[0].Name ); + Assert.Equal( data[0].Name, result[1].Name ); + } + + [Fact] + public void ForOrderByOnPrimaryIndex_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name2 = "TestObject1" + }, + new TestObject + { + Name2 = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).OrderBy( "Name2" ).OptArg( "index", "Name2" ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .OrderBy( x => x.Name2 ) + .ToList(); + + Assert.Equal( 2, result.Count ); + Assert.Equal( data[0].Name2, result[0].Name2 ); + Assert.Equal( data[1].Name2, result[1].Name2 ); + } + + [Fact] + public void ForOrderByOnSecondaryIndex_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name3 = "TestObject1", + Name2 = "1" + }, + new TestObject + { + Name3 = "TestObject2", + Name2 = "2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).OrderBy( "Name3" ).OptArg( "index", "Name3" ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .OrderBy( x => x.Name3 ) + .ToList(); + + Assert.Equal( 2, result.Count ); + Assert.Equal( data[0].Name3, result[0].Name3 ); + Assert.Equal( data[1].Name3, result[1].Name3 ); + } + + public class TestObject + { + public string Name { get; set; } + + [PrimaryIndex] + public string Name2 { get; set; } + + [SecondaryIndex] + public string Name3 { get; set; } + } + } +} diff --git a/Source/RethinkDb.Driver.Linq.Tests/Properties/AssemblyInfo.cs b/Source/RethinkDb.Driver.Linq.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..145797e --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,25 @@ +using System.Reflection; +using System.Runtime.InteropServices; +using Xunit; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("RethinkDb.Driver.Linq.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("RethinkDb.Driver.Linq.Tests")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("836cd478-2df3-4a8b-a57b-58d3fcb01a7d")] + +[assembly: CollectionBehavior( DisableTestParallelization = true )] \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq.Tests/RethinkDb.Driver.Linq.Tests.xproj b/Source/RethinkDb.Driver.Linq.Tests/RethinkDb.Driver.Linq.Tests.xproj new file mode 100644 index 0000000..db6cae6 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/RethinkDb.Driver.Linq.Tests.xproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 836cd478-2df3-4a8b-a57b-58d3fcb01a7d + RethinkDb.Driver.Linq.Tests + ..\artifacts\obj\$(MSBuildProjectName) + ..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + + + + \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq.Tests/TestRethinkQueryExecuter.cs b/Source/RethinkDb.Driver.Linq.Tests/TestRethinkQueryExecuter.cs new file mode 100644 index 0000000..01b9412 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/TestRethinkQueryExecuter.cs @@ -0,0 +1,21 @@ +using System; +using RethinkDb.Driver.Ast; +using RethinkDb.Driver.Net; + +namespace RethinkDb.Driver.Linq.Tests +{ + public class TestRethinkQueryExecutor : RethinkQueryExecutor + { + private readonly Action _validate; + + public TestRethinkQueryExecutor( Table table, IConnection connection, Action validate ) : base( table, connection ) + { + _validate = validate; + } + + protected override void ProcessQuery( ReqlAst query ) + { + _validate( query ); + } + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq.Tests/WhereComparisonTests.cs b/Source/RethinkDb.Driver.Linq.Tests/WhereComparisonTests.cs new file mode 100644 index 0000000..f1952fc --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/WhereComparisonTests.cs @@ -0,0 +1,352 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using RethinkDb.Driver.Linq.Attributes; +using Xunit; + +namespace RethinkDb.Driver.Linq.Tests +{ + public class WhereComparisonTests : BaseLinqTest + { + [Fact] + public void ForSimpleToList_DoesNotCallAnyReqlMethods() + { + var data = new List + { + new TestObject + { + Name = "TestObject1" + }, + new TestObject + { + Name = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ); + + var result = GetQueryable( TableName, expected ).ToList(); + + Assert.Equal( 2, result.Count ); + foreach( var testObject in data ) + Assert.True( result.Any( x => x.Name == testObject.Name ) ); + } + + [Fact] + public void ForSimpleEqualOnNonIndex_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "Hello" + }, + new TestObject + { + Name = "TestObject1" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Filter( x => x["Name"].Eq( "Hello" ) ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => x.Name == "Hello" ) + .ToList(); + + Assert.Equal( 1, result.Count ); + Assert.Equal( data[0].Name, result[0].Name ); + } + + [Fact] + public void ForSimpleNotEqualOnNonIndex_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "Hello" + }, + new TestObject + { + Name = "TestObject1" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Filter( x => x["Name"].Eq( "Hello" ).Not() ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => x.Name != "Hello" ) + .ToList(); + + Assert.Equal( 1, result.Count ); + Assert.Equal( data[1].Name, result[0].Name ); + } + + [Fact] + public void ForSimpleEqualOnPrimaryIndex_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Id = Guid.NewGuid(), + Name = "TestObject1" + }, + new TestObject + { + Id = Guid.NewGuid(), + Name = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Get( data[0].Id ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => x.Id == data[0].Id ) + .ToList(); + + Assert.Equal( 1, result.Count ); + Assert.Equal( data[0].Id, result[0].Id ); + Assert.Equal( data[0].Name, result[0].Name ); + } + + [Fact] + public void ForSimpleEqualOnPrimaryIndexWhenIndexIsOnRight_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Id = Guid.NewGuid(), + Name = "TestObject1" + }, + new TestObject + { + Id = Guid.NewGuid(), + Name = "TestObject2" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).Get( data[0].Id ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => data[0].Id == x.Id ) + .ToList(); + + Assert.Equal( 1, result.Count ); + Assert.Equal( data[0].Id, result[0].Id ); + Assert.Equal( data[0].Name, result[0].Name ); + } + + [Fact] + public void ForSimpleEqualOnSecondaryIndex_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject1", + Location = "LocationA" + }, + new TestObject + { + Name = "TestObject2", + Location = "LocationB" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ).GetAll( data[0].Location ).OptArg( "index", "Location" ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => x.Location == data[0].Location ) + .ToList(); + + Assert.Equal( 1, result.Count ); + Assert.Equal( data[0].Id, result[0].Id ); + Assert.Equal( data[0].Name, result[0].Name ); + } + + [Fact] + public void ForEqualWith1AndOnSecondaryIndex_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject1", + Location = "LocationA" + }, + new TestObject + { + Name = "TestObject2", + Location = "LocationA" + }, + new TestObject + { + Name = "TestObject3", + Location = "LocationB" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .GetAll( data[0].Location ) + .OptArg( "index", "Location" ) + .Filter( x => x["Name"].Eq( data[0].Name ) ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => x.Location == data[0].Location && x.Name == data[0].Name ) + .ToList(); + + Assert.Equal( 1, result.Count ); + Assert.Equal( data[0].Id, result[0].Id ); + Assert.Equal( data[0].Name, result[0].Name ); + } + + [Fact] + public void ForEqualWith2AndOnSecondaryIndex_GeneratesCorrectReql() + { + var data = new List + { + new TestObject + { + Name = "TestObject1", + Location = "LocationA" + }, + new TestObject + { + Name = "TestObject2", + Location = "LocationA" + }, + new TestObject + { + Name = "TestObject3", + Location = "LocationB" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .GetAll( data[0].Location ) + .OptArg( "index", "Location" ) + .Filter( x => x["Name"].Eq( data[0].Name ) ); + expected = expected.Filter( x => x["Name2"].Eq( data[1].Name2 ) ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => + x.Location == data[0].Location && + x.Name == data[0].Name && + x.Name2 == data[0].Name2 ) + .ToList(); + + Assert.Equal( 1, result.Count ); + Assert.Equal( data[0].Id, result[0].Id ); + Assert.Equal( data[0].Name, result[0].Name ); + } + + [Fact] + public void WhenIsFilteredByOnceColumnThenByIndex_DoesNotUseIndex() + { + var data = new List + { + new TestObject + { + Name = "TestObject1", + Location = "LocationA" + }, + new TestObject + { + Name = "TestObject2", + Location = "LocationB" + } + }; + + SpawnData( data ); + + var expected = RethinkDB.R.Table( TableName ) + .Filter( x => x["Name"].Eq( "TestObject1" ) ) + .Filter( x => x["Location"].Eq( data[0].Location ) ); + + var queryable = GetQueryable( TableName, expected ); + + var result = queryable + .Where( x => x.Name == "TestObject1" ) + .Where( x => x.Location == data[0].Location ) + .ToList(); + + Assert.Equal( 1, result.Count ); + Assert.Equal( data[0].Id, result[0].Id ); + Assert.Equal( data[0].Name, result[0].Name ); + } + + + + + + + + + + + + + public class TestObject + { + public TestObject() + { + Id = Guid.NewGuid(); + } + + [PrimaryIndex] + public Guid Id { get; set; } + + public string Name { get; set; } + public string Name2 { get; set; } + + [SecondaryIndex] + public string Location { get; set; } + + public List Locations { get; set; } + public List Resources { get; set; } + } + } + + public class Location + { + public List Usages { get; set; } + } + + public class Resource + { + public List Locations { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq.Tests/project.json b/Source/RethinkDb.Driver.Linq.Tests/project.json new file mode 100644 index 0000000..138f851 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq.Tests/project.json @@ -0,0 +1,35 @@ +{ + "version": "1.0.0-*", + "description": "RethinkDb.Driver.Linq.Tests Class Library", + "authors": [ "jrote" ], + "tags": [ "" ], + "projectUrl": "", + "licenseUrl": "", + "commands": { + "test": "xunit.runner.dnx" + }, + "dependencies": { + "xunit": "2.1.0", + "xunit.runner.dnx": "2.1.0-rc1-build204", + + "RethinkDb.Driver.Linq": "", + "System.ComponentModel.Annotations": "4.0.11-beta-23516" + }, + "frameworks": { + "dnx451": { + "dependencies": { + "Moq": "4.2.1312.1622" + } + }, + "dnxcore50": { + "dependencies": { + "moq.netcore": "4.4.0-beta8", + "Microsoft.CSharp": "4.0.1-beta-23516", + "System.Collections": "4.0.11-beta-23516", + "System.Linq": "4.0.1-beta-23516", + "System.Runtime": "4.0.21-beta-23516", + "System.Threading": "4.0.11-beta-23516" + } + } + } +} diff --git a/Source/RethinkDb.Driver.Linq/Attributes/PrimaryIndexAttribute.cs b/Source/RethinkDb.Driver.Linq/Attributes/PrimaryIndexAttribute.cs new file mode 100644 index 0000000..f4a3a1c --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/Attributes/PrimaryIndexAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace RethinkDb.Driver.Linq.Attributes +{ + public class PrimaryIndexAttribute : Attribute + { + + } +} diff --git a/Source/RethinkDb.Driver.Linq/Attributes/SecondaryIndexAttribute.cs b/Source/RethinkDb.Driver.Linq/Attributes/SecondaryIndexAttribute.cs new file mode 100644 index 0000000..20a1b56 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/Attributes/SecondaryIndexAttribute.cs @@ -0,0 +1,8 @@ +using System; + +namespace RethinkDb.Driver.Linq.Attributes +{ + public class SecondaryIndexAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/LinqExtensions.cs b/Source/RethinkDb.Driver.Linq/LinqExtensions.cs new file mode 100644 index 0000000..dbd552d --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/LinqExtensions.cs @@ -0,0 +1,26 @@ +using Remotion.Linq; +using Remotion.Linq.Parsing.Structure; +using RethinkDb.Driver.Ast; +using RethinkDb.Driver.Net; + +namespace RethinkDb.Driver.Linq +{ + public static class LinqExtensions + { + public static RethinkQueryable AsQueryable( this Table term, IConnection conn ) + { + var executor = new RethinkQueryExecutor( term, conn ); + return new RethinkQueryable( + new DefaultQueryProvider( + typeof( RethinkQueryable<> ), + QueryParser.CreateDefault(), + executor ) + ); + } + + public static RethinkQueryable Table( this Db db, string tableName, IConnection conn ) + { + return db.Table( tableName ).AsQueryable( conn ); + } + } +} diff --git a/Source/RethinkDb.Driver.Linq/MemberNameResolver.cs b/Source/RethinkDb.Driver.Linq/MemberNameResolver.cs new file mode 100644 index 0000000..bd85fb3 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/MemberNameResolver.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Remotion.Linq.Clauses.Expressions; +using RethinkDb.Driver.Ast; +using RethinkDb.Driver.Linq.WhereClauseParsers.SubQueryVisitor; + +namespace RethinkDb.Driver.Linq +{ + public class MemberNameResolver + { + private readonly MemberExpression _expression; + + public MemberNameResolver( MemberExpression expression ) + { + _expression = expression; + } + + public ReqlExpr Resolve( ReqlExpr reqlExpr ) => ResolveMemberExpression( reqlExpr, _expression ); + + private static ReqlExpr ResolveMemberExpression( ReqlExpr reqlExpr, MemberExpression expression ) + { + if( expression.Expression.NodeType == ExpressionType.MemberAccess ) + reqlExpr = ResolveMemberExpression( reqlExpr, (MemberExpression)expression.Expression ); + if( expression.Expression.NodeType == ExpressionType.Extension && expression.Expression is SubQueryExpression ) + reqlExpr = ResolveExtensionExpression( reqlExpr, (SubQueryExpression)expression.Expression ); + return reqlExpr[expression.Member.Name]; + } + + private static ReqlExpr ResolveExtensionExpression( ReqlExpr reqlExpr, SubQueryExpression expression ) + { + var subQueryVisitors = new List + { + new AnySubQueryVisitor(), + new AllSubQueryVisitor(), + new FirstAndLastSubQueryVisitor() + }; + + var subQueryVisitor = subQueryVisitors.FirstOrDefault( x => x.CanVisit( expression.QueryModel ) ); + + if( subQueryVisitor == null ) + throw new NotSupportedException( "subqueries not allowed ." ); + + return subQueryVisitor.Visit( reqlExpr, expression.QueryModel ); + } + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/Properties/AssemblyInfo.cs b/Source/RethinkDb.Driver.Linq/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..15fee8c --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/Properties/AssemblyInfo.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("RethinkDb.Driver.Linq")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("RethinkDb.Driver.Linq")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9cd8edd7-99a4-441b-9eb5-ada7f94ac402")] diff --git a/Source/RethinkDb.Driver.Linq/RethinkDb.Driver.Linq.xproj b/Source/RethinkDb.Driver.Linq/RethinkDb.Driver.Linq.xproj new file mode 100644 index 0000000..a581435 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/RethinkDb.Driver.Linq.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 9cd8edd7-99a4-441b-9eb5-ada7f94ac402 + RethinkDb.Driver.Linq + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + diff --git a/Source/RethinkDb.Driver.Linq/RethinkDbGroup.cs b/Source/RethinkDb.Driver.Linq/RethinkDbGroup.cs new file mode 100644 index 0000000..490b450 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/RethinkDbGroup.cs @@ -0,0 +1,23 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace RethinkDb.Driver.Linq +{ + public class RethinkDbGroup : IGrouping + { + public IEnumerator GetEnumerator() + { + return Reduction.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public T Key { get; set; } + + public List Reduction { get; set; } + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/RethinkDbQueryModelVisitor.cs b/Source/RethinkDb.Driver.Linq/RethinkDbQueryModelVisitor.cs new file mode 100644 index 0000000..a02da59 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/RethinkDbQueryModelVisitor.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Remotion.Linq; +using Remotion.Linq.Clauses; +using Remotion.Linq.Clauses.Expressions; +using Remotion.Linq.Clauses.ResultOperators; +using RethinkDb.Driver.Ast; +using RethinkDb.Driver.Linq.Attributes; +using RethinkDb.Driver.Linq.WhereClauseParsers; +using ExpressionVisitor = RethinkDb.Driver.Linq.WhereClauseParsers.ExpressionVisitor; + +namespace RethinkDb.Driver.Linq +{ + public class RethinkDbQueryModelVisitor : QueryModelVisitorBase + { + private readonly Table _table; + + public ReqlAst Query => Stack.Peek(); + public Stack Stack = new Stack(); + + public RethinkDbQueryModelVisitor( Table table ) + { + _table = table; + } + + public override void VisitQueryModel( QueryModel queryModel ) + { + Stack.Push( _table ); + base.VisitQueryModel( queryModel ); + } + + public override void VisitMainFromClause( MainFromClause fromClause, QueryModel queryModel ) + { + var subQuery = fromClause.FromExpression as SubQueryExpression; + if( subQuery != null ) + { + var query = subQuery.QueryModel; + VisitBodyClauses( query.BodyClauses, query ); + base.VisitMainFromClause( fromClause, queryModel ); + } + + base.VisitMainFromClause( fromClause, queryModel ); + } + + public override void VisitWhereClause( WhereClause whereClause, QueryModel queryModel, int index ) + { + var expressions = SplitWhereClause( whereClause.Predicate ); + + foreach( var expression in expressions.ToList() ) + { + var whereClauseParsers = new List + { + new GroupItemsParser(), + new PrimaryIndexParser(), + new SecondaryIndexParser(), + new DefaultParser() + }; + + var reql = Stack.Pop(); + var matchingParser = whereClauseParsers.FirstOrDefault( x => x.IsAppropriate( reql, expression, queryModel.ResultTypeOverride ) ); + if( matchingParser != null ) + Stack.Push( matchingParser.Parse( reql, queryModel, expression ) ); + else + throw new NotSupportedException( "Unable to vist Where clause" ); + } + } + + private static IEnumerable SplitWhereClause( Expression predicate ) + { + if( predicate.NodeType != ExpressionType.AndAlso ) + { + yield return predicate; + yield break; + } + + var binaryExpression = predicate as BinaryExpression; + if( binaryExpression == null ) + yield break; + + + foreach( var expression in SplitWhereClause( binaryExpression.Left ) ) + yield return expression; + yield return binaryExpression.Right; + } + + private static ReqlExpr GetSelectReqlAst( Expression selector ) + { + var visitor = new SelectionProjector(); + visitor.Visit( selector ); + return visitor.Current; + } + + public override void VisitOrderByClause( OrderByClause orderByClause, QueryModel queryModel, int index ) + { + var expression = orderByClause.Orderings[0].Expression as MemberExpression; + if( expression == null ) + return; + + OrderBy reql; + var currentStack = Stack.Pop(); + if( orderByClause.Orderings[0].OrderingDirection == OrderingDirection.Asc ) + reql = currentStack.OrderBy( expression.Member.Name ); + else + reql = currentStack.OrderBy( RethinkDB.R.Desc( expression.Member.Name ) ); + + if( currentStack is Table && expression.Member.CustomAttributes.Any( x => x.AttributeType == typeof( PrimaryIndexAttribute ) || x.AttributeType == typeof( SecondaryIndexAttribute ) ) ) + reql = reql.OptArg( "index", expression.Member.Name ); + + Stack.Push( reql ); + } + + public override void VisitSelectClause( SelectClause selectClause, QueryModel queryModel ) + { + if( queryModel.ResultOperators.FirstOrDefault() is AverageResultOperator ) + { + var memberExpression = selectClause.Selector as MemberExpression; + var memberNameResolver = new MemberNameResolver( memberExpression ); + Stack.Push( Stack.Pop().Avg( x => memberNameResolver.Resolve( x ) ) ); + return; + } + + if( queryModel.ResultOperators.FirstOrDefault() is CountResultOperator ) + { + Stack.Push( Stack.Pop().Count( ) ); + return; + } + + var expr = GetSelectReqlAst( selectClause.Selector ); + if( !ReferenceEquals( expr, null ) ) + { + + } + + base.VisitSelectClause( selectClause, queryModel ); + } + + protected override void VisitBodyClauses( ObservableCollection bodyClauses, QueryModel queryModel ) + { + if( queryModel.ResultTypeOverride.GetTypeInfo().IsGenericTypeDefinition && queryModel.ResultTypeOverride.GetGenericArguments()[0].GetGenericTypeDefinition() != typeof( IGrouping<,> ) ) + { + base.VisitBodyClauses( bodyClauses, queryModel ); + return; + } + + var group = queryModel.ResultOperators.FirstOrDefault() as GroupResultOperator; + if( group == null ) + { + base.VisitBodyClauses( bodyClauses, queryModel ); + return; + } + + var groupReql = Stack.Pop().Group( ( (MemberExpression)group.KeySelector ).Member.Name ); + + var memberAccess = group.ElementSelector as MemberExpression; + Stack.Push( memberAccess != null ? groupReql.GetField( memberAccess.Member.Name ).Ungroup() : groupReql.Ungroup() ); + } + + public override void VisitResultOperator( ResultOperatorBase resultOperator, QueryModel queryModel, int index ) + { + if( resultOperator is AnyResultOperator ) + Stack.Push( Stack.Pop().Count() ); + else if( resultOperator is AllResultOperator ) + { + var allResultOperator = resultOperator as AllResultOperator; + Stack.Push( Stack.Pop().Filter( x => GetWhereReqlAst( x, allResultOperator.Predicate ).Not() ).Count() ); + } + else if( resultOperator is FirstResultOperator ) + Stack.Push( Stack.Pop().Nth( 0 ) ); + else if( resultOperator is LastResultOperator ) + Stack.Push( Stack.Pop().Nth( -1 ) ); + + base.VisitResultOperator( resultOperator, queryModel, index ); + } + + private static ReqlExpr GetWhereReqlAst( ReqlExpr reqlExpr, Expression predicate ) + { + var visitor = new ExpressionVisitor( reqlExpr, typeof( bool ) ); + visitor.Visit( predicate ); + return visitor.Current; + } + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/RethinkQueryExecutor.cs b/Source/RethinkDb.Driver.Linq/RethinkQueryExecutor.cs new file mode 100644 index 0000000..22eb3bd --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/RethinkQueryExecutor.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Remotion.Linq; +using Remotion.Linq.Clauses.ResultOperators; +using RethinkDb.Driver.Ast; +using RethinkDb.Driver.Net; + +namespace RethinkDb.Driver.Linq +{ + public class RethinkQueryExecutor : IQueryExecutor + { + private readonly Table _table; + private readonly IConnection _connection; + + public RethinkQueryExecutor( Table table, IConnection connection ) + { + _table = table; + _connection = connection; + } + + protected virtual void ProcessQuery( ReqlAst query ) + { + + } + + public T ExecuteScalar( QueryModel queryModel ) + { + var visitor = new RethinkDbQueryModelVisitor( _table ); + + visitor.VisitQueryModel( queryModel ); + + var query = visitor.Query; + ProcessQuery( query ); + + var result = query.Run( _connection ); + + if( queryModel.ResultOperators.FirstOrDefault() is AnyResultOperator ) + return result > 0; + if( queryModel.ResultOperators.FirstOrDefault() is AllResultOperator ) + return result == 0; + + return (T)result; + } + + + + public T ExecuteSingle( QueryModel queryModel, bool returnDefaultWhenEmpty ) + { + var visitor = new RethinkDbQueryModelVisitor( _table ); + + visitor.VisitQueryModel( queryModel ); + + var query = visitor.Query; + ProcessQuery( query ); + + try + { + return query.RunResult( _connection ); + } + catch( ReqlNonExistenceError ex ) + { + if( ShouldReturnDefault( queryModel ) ) + return default( T ); + throw new InvalidOperationException( ex.Message ); + } + } + + private static bool ShouldReturnDefault( QueryModel queryModel ) + { + var firstResultOperator = queryModel.ResultOperators.FirstOrDefault(); + return ( firstResultOperator as FirstResultOperator )?.ReturnDefaultWhenEmpty + ?? ( firstResultOperator as LastResultOperator )?.ReturnDefaultWhenEmpty + ?? false; + } + + public IEnumerable ExecuteCollection( QueryModel queryModel ) + { + var visitor = new RethinkDbQueryModelVisitor( _table ); + + visitor.VisitQueryModel( queryModel ); + + var query = visitor.Query; + ProcessQuery( query ); + + if( typeof( T ).GetTypeInfo().IsGenericType && typeof( T ).GetGenericTypeDefinition() == typeof( IGrouping<,> ) ) + return GetType().GetMethod( nameof( DeserializeGrouping ), BindingFlags.NonPublic | BindingFlags.Static ) + .MakeGenericMethod( + typeof( T ).GetGenericArguments()[0], + typeof( T ).GetGenericArguments()[1] ) + .Invoke( null, new object[] + { + query.Run( _connection ) as JArray + } ) as IEnumerable; + + if( query is Get ) + return new List + { + query.RunResult( _connection ) + }; + + return query.RunResult>( _connection ); + } + + private static List> DeserializeGrouping( JArray groups ) + { + var result = new List>(); + + foreach( var group in groups ) + { + var groupArray = (JObject)group; + result.Add( new RethinkDbGroup + { + Key = groupArray["group"].ToObject(), + Reduction = JsonConvert.DeserializeObject>( groupArray["reduction"].ToString() ) + } ); + } + + return result; + } + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/RethinkQueryable.cs b/Source/RethinkDb.Driver.Linq/RethinkQueryable.cs new file mode 100644 index 0000000..1e1eaf5 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/RethinkQueryable.cs @@ -0,0 +1,22 @@ +using System.Linq; +using System.Linq.Expressions; +using Remotion.Linq; +using Remotion.Linq.Parsing.Structure; + +namespace RethinkDb.Driver.Linq +{ + public class RethinkQueryable : QueryableBase + { + public RethinkQueryable( IQueryParser queryParser, IQueryExecutor executor ) : base( queryParser, executor ) + { + } + + public RethinkQueryable( IQueryProvider provider ) : base( provider ) + { + } + + public RethinkQueryable( IQueryProvider provider, Expression expression ) : base( provider, expression ) + { + } + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/SelectionProjector.cs b/Source/RethinkDb.Driver.Linq/SelectionProjector.cs new file mode 100644 index 0000000..27dbae9 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/SelectionProjector.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using Remotion.Linq.Clauses.Expressions; +using Remotion.Linq.Parsing; +using RethinkDb.Driver.Ast; + +namespace RethinkDb.Driver.Linq +{ + public class SelectionProjector : ThrowingExpressionVisitor + { + private readonly Stack _stack = new Stack(); + + public ReqlExpr Current => _stack.Count > 0 ? _stack.Peek() : null; + + protected override Exception CreateUnhandledItemException( T unhandledItem, string visitMethod ) + { + throw new NotImplementedException(); + } + + protected override Expression VisitQuerySourceReference( QuerySourceReferenceExpression expression ) + { + return expression; + } + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/BaseIndexParser.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/BaseIndexParser.cs new file mode 100644 index 0000000..6414ba5 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/BaseIndexParser.cs @@ -0,0 +1,40 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using Remotion.Linq; +using RethinkDb.Driver.Ast; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers +{ + public abstract class BaseIndexParser : IWhereClauseParser + { + public bool IsAppropriate( ReqlAst reql, Expression expression, Type resultType ) + { + var binaryExpression = expression as BinaryExpression; + + if( !( reql is Table ) || binaryExpression?.NodeType != ExpressionType.Equal ) + return false; + + var left = binaryExpression.Left as MemberExpression; + if( left != null ) + return IsIndex( left ); + + var right = binaryExpression.Right as MemberExpression; + + return right != null && IsIndex( right ); + } + + private static bool IsIndex( MemberExpression left ) + { + return left.Member.CustomAttributes.Any( x => x.AttributeType == typeof( T ) ); + } + + public abstract ReqlExpr Parse( ReqlExpr expression, QueryModel queryModel, Expression predicate ); + + protected static object GetValue( BinaryExpression binaryExpression ) + { + var left = binaryExpression.Left as ConstantExpression; + return left != null ? left.Value : ( (ConstantExpression)binaryExpression.Right ).Value; + } + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/DefaultParser.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/DefaultParser.cs new file mode 100644 index 0000000..ba1dc84 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/DefaultParser.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; +using Remotion.Linq; +using RethinkDb.Driver.Ast; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers +{ + public class DefaultParser : IWhereClauseParser + { + public bool IsAppropriate( ReqlAst reql, Expression expression, Type resultType ) => true; + + public ReqlExpr Parse( ReqlExpr expression, QueryModel queryModel, Expression predicate ) + { + return expression.Filter( reqlExpr => GetWhereReqlAst( reqlExpr, predicate, queryModel ) ); + } + + private static ReqlExpr GetWhereReqlAst( ReqlExpr reqlExpr, Expression predicate, QueryModel queryModel ) + { + var visitor = new ExpressionVisitor( reqlExpr, GetResultType( queryModel ) ); + visitor.Visit( predicate ); + return visitor.Current; + } + + private static Type GetResultType( QueryModel queryModel ) + { + if( !queryModel.ResultTypeOverride.GetTypeInfo().IsGenericType ) + return queryModel.ResultTypeOverride; + return queryModel.ResultTypeOverride.GetTypeInfo().GenericTypeArguments[0]; + } + } +} diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/ExprHelper.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/ExprHelper.cs new file mode 100644 index 0000000..ae469f4 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/ExprHelper.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq.Expressions; +using RethinkDb.Driver.Ast; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers +{ + public static class ExprHelper + { + public static ReqlExpr TranslateUnary( ExpressionType type, ReqlExpr term ) + { + switch( type ) + { + case ExpressionType.Not: + return term.Not(); + default: + throw new NotSupportedException( "Unary term not supported." ); + } + } + + public static ReqlExpr TranslateBinary( ExpressionType type, ReqlExpr left, ReqlExpr right ) + { + switch( type ) + { + case ExpressionType.Equal: + return left.Eq( right ); + case ExpressionType.NotEqual: + return left.Eq( right ).Not(); + case ExpressionType.LessThan: + return left.Lt( right ); + case ExpressionType.LessThanOrEqual: + return left.Le( right ); + case ExpressionType.GreaterThan: + return left.Gt( right ); + case ExpressionType.GreaterThanOrEqual: + return left.Ge( right ); + case ExpressionType.And: + case ExpressionType.AndAlso: + return left.And( right ); + case ExpressionType.Or: + case ExpressionType.OrElse: + return left.Or( right ); + case ExpressionType.Not: + throw new InvalidOperationException( "ExpresionType:Not cannot be called on a binary translation." ); + case ExpressionType.Add: + return left.Add( right ); + case ExpressionType.Subtract: + return left.Sub( right ); + case ExpressionType.Multiply: + return left.Mul( right ); + case ExpressionType.Divide: + return left.Div( right ); + case ExpressionType.Modulo: + return left.Mod( right ); + default: + throw new NotSupportedException( "Binary expression not supported." ); + } + } + + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/ExpressionVisitor.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/ExpressionVisitor.cs new file mode 100644 index 0000000..389bd79 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/ExpressionVisitor.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Remotion.Linq.Clauses.Expressions; +using Remotion.Linq.Parsing; +using RethinkDb.Driver.Ast; +using RethinkDb.Driver.Linq.WhereClauseParsers.SubQueryVisitor; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers +{ + public class ExpressionVisitor : ThrowingExpressionVisitor + { + private readonly ReqlExpr _reqlExpr; + private readonly Type _type; + private readonly Stack _stack = new Stack(); + + public ExpressionVisitor( ReqlExpr reqlExpr, Type type ) + { + _type = type; + _reqlExpr = reqlExpr; + } + + public ReqlExpr Current => _stack.Peek(); + + protected override Exception CreateUnhandledItemException( T unhandledItem, string visitMethod ) + { + string itemText = unhandledItem.ToString(); + var message = $"The expression '{itemText}' (type: {typeof( T )}) is not supported by RethinkDB LINQ provider."; + return new NotSupportedException( message ); + } + + private ReqlExpr VistSide( Expression expression ) + { + if( expression.NodeType == ExpressionType.Extension && expression is QuerySourceReferenceExpression ) + return _reqlExpr; + Visit( expression ); + return _stack.Pop(); + } + + protected override Expression VisitBinary( BinaryExpression expression ) + { + var left = VistSide( expression.Left ); + var right = VistSide( expression.Right ); + + var operation = ExprHelper.TranslateBinary( expression.NodeType, left, right ); + _stack.Push( operation ); + + return expression; + } + + protected override Expression VisitUnary( UnaryExpression expression ) + { + if( expression.NodeType == ExpressionType.Not ) + { + Visit( expression.Operand ); + _stack.Push( _stack.Pop().Not() ); + return null; + } + return base.VisitUnary( expression ); + } + + protected override Expression VisitConstant( ConstantExpression expression ) + { + var datum = Util.ToReqlExpr( expression.Value ); + _stack.Push( datum ); + return expression; + } + + protected override Expression VisitMember( MemberExpression expression ) + { + Visit( expression.Expression ); + + var fieldName = expression.Member.Name; + if( _type.GetTypeInfo().IsGenericType && _type.GetGenericTypeDefinition() == typeof( IGrouping<,> ) && fieldName == "Key" ) + _stack.Push( _reqlExpr["group"] ); + else + { + var memberNameResolver = new MemberNameResolver( expression ); + _stack.Push( memberNameResolver.Resolve( _reqlExpr ) ); + } + + return expression; + } + + protected override Expression VisitSubQuery( SubQueryExpression expression ) + { + var subQueryVisitors = new List + { + new AnySubQueryVisitor(), + new AllSubQueryVisitor(), + new FirstAndLastSubQueryVisitor(), + new CountSubQueryVisitor(), + new ContainsSubQueryVisitor() + }; + + var subQueryVisitor = subQueryVisitors.FirstOrDefault( x => x.CanVisit( expression.QueryModel ) ); + + if( subQueryVisitor == null ) throw new NotSupportedException( "subqueries not allowed ." ); + + _stack.Push( subQueryVisitor.Visit( _reqlExpr, expression.QueryModel ) ); + return null; + } + + protected override Expression VisitQuerySourceReference( QuerySourceReferenceExpression expression ) + { + return expression; + } + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/GroupItemsParser.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/GroupItemsParser.cs new file mode 100644 index 0000000..e8c23a9 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/GroupItemsParser.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Remotion.Linq; +using Remotion.Linq.Clauses; +using Remotion.Linq.Clauses.Expressions; +using Remotion.Linq.Clauses.ResultOperators; +using RethinkDb.Driver.Ast; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers +{ + public class GroupItemsParser : IWhereClauseParser + { + public bool IsAppropriate( ReqlAst reql, Expression expression, Type resultType ) + { + var subQueryExpression = expression as SubQueryExpression; + if( subQueryExpression == null ) + return false; + + var type = subQueryExpression.QueryModel.MainFromClause.FromExpression.Type; + + if( type.GetTypeInfo().IsGenericType + && type.GetGenericTypeDefinition() == typeof( IGrouping<,> ) ) + { + if( subQueryExpression.QueryModel.ResultOperators[0] is AnyResultOperator ) + return true; + throw new NotImplementedException( "This filter is not supported for GroupBy" ); + } + + return false; + } + + public ReqlExpr Parse( ReqlExpr expression, QueryModel queryModel, Expression predicate ) + { + return expression.Filter( x => x["reduction"].Contains( reqlExpr => GetWhereReqlAst( reqlExpr, predicate ) ) ); + } + + private static ReqlExpr GetWhereReqlAst( ReqlExpr reqlExpr, Expression predicate ) + { + var subQueryExpression = predicate as SubQueryExpression; + var where = subQueryExpression.QueryModel.BodyClauses[0] as WhereClause; + + var visitor = new ExpressionVisitor( reqlExpr, subQueryExpression.QueryModel.MainFromClause.ItemType ); + visitor.Visit( where.Predicate ); + return visitor.Current; + } + } +} diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/IWhereClauseParser.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/IWhereClauseParser.cs new file mode 100644 index 0000000..e3a96ec --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/IWhereClauseParser.cs @@ -0,0 +1,13 @@ +using System; +using System.Linq.Expressions; +using Remotion.Linq; +using RethinkDb.Driver.Ast; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers +{ + public interface IWhereClauseParser + { + bool IsAppropriate( ReqlAst reql, Expression expression, Type resultType ); + ReqlExpr Parse( ReqlExpr expression, QueryModel queryModel, Expression predicate ); + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/Poco.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/Poco.cs new file mode 100644 index 0000000..d8233e9 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/Poco.cs @@ -0,0 +1,46 @@ +using Newtonsoft.Json.Linq; +using RethinkDb.Driver.Ast; +using RethinkDb.Driver.Net; +using RethinkDb.Driver.Proto; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers +{ + internal class Poco : ReqlExpr + { + private readonly object _obj; + + public Poco( object obj ) : base( new TermType(), null, null ) + { + _obj = obj; + } + + + internal class PocoWriter : JTokenWriter + { + public override void WriteStartArray() + { + base.WriteStartArray(); + WriteValue( TermType.MAKE_ARRAY ); + base.WriteStartArray(); + } + + public override void WriteEndArray() + { + base.WriteEndArray(); + base.WriteEndArray(); + } + } + + + protected override object Build() + { + JToken token; + using( var writer = new PocoWriter() ) + { + Converter.Serializer.Serialize( writer, _obj ); + token = writer.Token; + } + return token; + } + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/PrimaryIndexParser.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/PrimaryIndexParser.cs new file mode 100644 index 0000000..fd394a7 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/PrimaryIndexParser.cs @@ -0,0 +1,18 @@ +using System.Linq.Expressions; +using Remotion.Linq; +using RethinkDb.Driver.Ast; +using RethinkDb.Driver.Linq.Attributes; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers +{ + public class PrimaryIndexParser : BaseIndexParser + { + public override ReqlExpr Parse( ReqlExpr expression, QueryModel queryModel, Expression predicate ) + { + var binaryExpression = (BinaryExpression)predicate; + var value = GetValue( binaryExpression ); + + return ( (Table)expression ).Get( value ); + } + } +} diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SecondaryIndexParser.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SecondaryIndexParser.cs new file mode 100644 index 0000000..9200a4d --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SecondaryIndexParser.cs @@ -0,0 +1,24 @@ +using System.Linq.Expressions; +using Remotion.Linq; +using RethinkDb.Driver.Ast; +using RethinkDb.Driver.Linq.Attributes; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers +{ + public class SecondaryIndexParser : BaseIndexParser + { + public override ReqlExpr Parse( ReqlExpr expression, QueryModel queryModel, Expression predicate ) + { + var binaryExpression = (BinaryExpression)predicate; + var value = GetValue( binaryExpression ); + + return ( (Table)expression ).GetAll( value ).OptArg( "index", GetIndexName( binaryExpression ) ); + } + + private static string GetIndexName( BinaryExpression binaryExpression ) + { + var left = binaryExpression.Left as MemberExpression; + return left?.Member.Name ?? ( (MemberExpression)binaryExpression.Right ).Member.Name; + } + } +} diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/AllSubQueryVisitor.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/AllSubQueryVisitor.cs new file mode 100644 index 0000000..bf4c348 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/AllSubQueryVisitor.cs @@ -0,0 +1,19 @@ +using System.Linq.Expressions; +using Remotion.Linq; +using Remotion.Linq.Clauses.ResultOperators; +using RethinkDb.Driver.Ast; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers.SubQueryVisitor +{ + public class AllSubQueryVisitor : BaseSubQueryVisitor + { + public override ReqlExpr Visit( ReqlExpr reqlExpr, QueryModel queryModel ) + { + var fromExpression = queryModel.MainFromClause.FromExpression as MemberExpression; + var memberNameResolver = new MemberNameResolver( fromExpression ); + reqlExpr = memberNameResolver.Resolve( reqlExpr ); + reqlExpr = reqlExpr.Filter( expr => GetWhereReqlAst( expr, ( (AllResultOperator)queryModel.ResultOperators[0] ).Predicate, queryModel ).Not() ); + return reqlExpr.Count().Eq( 0 ); + } + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/AnySubQueryVisitor.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/AnySubQueryVisitor.cs new file mode 100644 index 0000000..b312a7b --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/AnySubQueryVisitor.cs @@ -0,0 +1,11 @@ +using Remotion.Linq; +using Remotion.Linq.Clauses.ResultOperators; +using RethinkDb.Driver.Ast; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers.SubQueryVisitor +{ + public class AnySubQueryVisitor : BaseFilterableSubQueryVisitor + { + protected override ReqlExpr BuildReql( ReqlExpr reqlExpr, QueryModel queryModel ) => reqlExpr.Count().Gt( 0 ); + } +} diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/BaseFilterableSubQueryVisitor.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/BaseFilterableSubQueryVisitor.cs new file mode 100644 index 0000000..322de16 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/BaseFilterableSubQueryVisitor.cs @@ -0,0 +1,23 @@ +using System.Linq; +using System.Linq.Expressions; +using Remotion.Linq; +using Remotion.Linq.Clauses; +using RethinkDb.Driver.Ast; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers.SubQueryVisitor +{ + public abstract class BaseFilterableSubQueryVisitor : BaseSubQueryVisitor + { + public override ReqlExpr Visit( ReqlExpr reqlExpr, QueryModel queryModel ) + { + var fromExpression = queryModel.MainFromClause.FromExpression as MemberExpression; + var memberNameResolver = new MemberNameResolver( fromExpression ); + reqlExpr = memberNameResolver.Resolve( reqlExpr ); + if( queryModel.BodyClauses.Any() ) + reqlExpr = reqlExpr.Filter( expr => GetWhereReqlAst( expr, ( (WhereClause)queryModel.BodyClauses[0] ).Predicate, queryModel ) ); + return BuildReql( reqlExpr, queryModel ); + } + + protected abstract ReqlExpr BuildReql( ReqlExpr reqlExpr, QueryModel queryModel ); + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/BaseSubQueryVisitor.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/BaseSubQueryVisitor.cs new file mode 100644 index 0000000..b0511e9 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/BaseSubQueryVisitor.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Remotion.Linq; +using RethinkDb.Driver.Ast; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers.SubQueryVisitor +{ + public abstract class BaseSubQueryVisitor : ISubQueryVisitor + { + protected static ReqlExpr GetWhereReqlAst( ReqlExpr reqlExpr, Expression predicate, QueryModel queryModel ) + { + var visitor = new ExpressionVisitor( reqlExpr, GetResultType( queryModel ) ); + visitor.Visit( predicate ); + return visitor.Current; + } + + protected static Type GetResultType( QueryModel queryModel ) + { + if( !queryModel.ResultTypeOverride.GetTypeInfo().IsGenericType ) + return queryModel.ResultTypeOverride; + return queryModel.ResultTypeOverride.GetTypeInfo().GenericTypeArguments[0]; + } + + public virtual bool CanVisit( QueryModel queryModel ) + { + return queryModel.ResultOperators.Any( x => x is T ); + } + + public abstract ReqlExpr Visit( ReqlExpr reqlExpr, QueryModel queryModel ); + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/ContainsSubQueryVisitor.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/ContainsSubQueryVisitor.cs new file mode 100644 index 0000000..9e42d05 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/ContainsSubQueryVisitor.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using Remotion.Linq; +using Remotion.Linq.Clauses.ResultOperators; +using RethinkDb.Driver.Ast; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers.SubQueryVisitor +{ + public class ContainsSubQueryVisitor : BaseSubQueryVisitor + { + public override ReqlExpr Visit( ReqlExpr reqlExpr, QueryModel queryModel ) + { + var fromExpression = queryModel.MainFromClause.FromExpression as ConstantExpression; + var array = RethinkDB.R.Expr( fromExpression.Value as IEnumerable ); + + var resultOperator = queryModel.ResultOperators[0] as ContainsResultOperator; + + var memberNameResolver = new MemberNameResolver( (MemberExpression)resultOperator.Item ); + return array.Contains( memberNameResolver.Resolve( reqlExpr ) ); + } + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/CountSubQueryVisitor.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/CountSubQueryVisitor.cs new file mode 100644 index 0000000..7cc38a3 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/CountSubQueryVisitor.cs @@ -0,0 +1,11 @@ +using Remotion.Linq; +using Remotion.Linq.Clauses.ResultOperators; +using RethinkDb.Driver.Ast; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers.SubQueryVisitor +{ + public class CountSubQueryVisitor : BaseFilterableSubQueryVisitor + { + protected override ReqlExpr BuildReql( ReqlExpr reqlExpr, QueryModel queryModel ) => reqlExpr.Count(); + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/FirstSubQueryVisitor.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/FirstSubQueryVisitor.cs new file mode 100644 index 0000000..44a8884 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/FirstSubQueryVisitor.cs @@ -0,0 +1,24 @@ +using System.Linq; +using Remotion.Linq; +using Remotion.Linq.Clauses.ResultOperators; +using RethinkDb.Driver.Ast; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers.SubQueryVisitor +{ + public class FirstAndLastSubQueryVisitor : BaseFilterableSubQueryVisitor + { + public override bool CanVisit( QueryModel queryModel ) + { + return queryModel.ResultOperators.Any( x => x is FirstResultOperator || x is LastResultOperator ); + } + + protected override ReqlExpr BuildReql( ReqlExpr reqlExpr, QueryModel queryModel ) + { + reqlExpr = reqlExpr.Nth( queryModel.ResultOperators[0] is FirstResultOperator ? 0 : -1 ); + var resultOperator = (ChoiceResultOperatorBase)queryModel.ResultOperators[0]; + if( resultOperator.ReturnDefaultWhenEmpty ) + reqlExpr = reqlExpr.Default_( (object)null ); + return reqlExpr; + } + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/ISubQueryVisitor.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/ISubQueryVisitor.cs new file mode 100644 index 0000000..016cc33 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/SubQueryVisitor/ISubQueryVisitor.cs @@ -0,0 +1,11 @@ +using Remotion.Linq; +using RethinkDb.Driver.Ast; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers.SubQueryVisitor +{ + public interface ISubQueryVisitor + { + bool CanVisit( QueryModel queryModel ); + ReqlExpr Visit( ReqlExpr reqlExpr, QueryModel queryModel ); + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/WhereClauseParsers/Util.cs b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/Util.cs new file mode 100644 index 0000000..d2e7b11 --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/WhereClauseParsers/Util.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using RethinkDb.Driver.Ast; +using RethinkDb.Driver.Model; + +namespace RethinkDb.Driver.Linq.WhereClauseParsers +{ + internal static class Util + { + public static ReqlAst ToReqlAst( object val ) + { + return ToReqlAst( val, 100 ); + } + + public static ReqlExpr ToReqlExpr( object val ) + { + var converted = ToReqlAst( val ); + var reqlAst = converted as ReqlExpr; + if( !ReferenceEquals( reqlAst, null ) ) + { + return reqlAst; + } + throw new ReqlDriverError( $"Cannot convert {val} to ReqlExpr" ); + } + + private static ReqlAst ToReqlAst( object val, int remainingDepth ) + { + if( remainingDepth <= 0 ) + { + throw new ReqlDriverCompileError( "Recursion limit reached converting to ReqlAst" ); + } + var ast = val as ReqlAst; + if( !ReferenceEquals( ast, null ) ) + { + return ast; + } + + var token = val as JToken; + if( token != null ) + { + return new Poco( token ); + } + + var lst = val as IList; + if( lst != null ) + { + Arguments innerValues = new Arguments(); + foreach( object innerValue in lst ) + { + innerValues.Add( ToReqlAst( innerValue, remainingDepth - 1 ) ); + } + return new MakeArray( innerValues, null ); + } + + var dict = val as IDictionary; + if( dict != null ) + { + var obj = new Dictionary(); + foreach( var keyObj in dict.Keys ) + { + var key = keyObj as string; + if( key == null ) + { + throw new ReqlDriverCompileError( "Object keys can only be strings" ); + } + + obj[key] = ToReqlAst( dict[keyObj] ); + } + return MakeObj.FromMap( obj ); + } + + var del = val as Delegate; + if( del != null ) + { + return Func.FromLambda( del ); + } + + + if( val is DateTime ) + { + var dt = (DateTime)val; + var isoStr = dt.ToString( "o" ); + return Iso8601.FromString( isoStr ); + } + if( val is DateTimeOffset ) + { + var dt = (DateTimeOffset)val; + var isoStr = dt.ToString( "o" ); + return Iso8601.FromString( isoStr ); + } + + + var @int = val as int?; + if( @int != null ) + { + return new Datum( @int ); + } + + if( IsNumber( val ) ) + { + return new Datum( val ); + } + + var @bool = val as bool?; + if( @bool != null ) + { + return new Datum( @bool ); + } + + var str = val as string; + if( str != null ) + { + return new Datum( str ); + } + if( val == null ) + { + return new Datum( null ); + } + + return new Poco( val ); + } + + public static bool IsNumber( object value ) + { + return value is sbyte + || value is byte //maybe have char here? + || value is short + || value is ushort + || value is int + || value is uint + || value is long + || value is ulong + || value is float + || value is double + || value is decimal; + } + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver.Linq/project.json b/Source/RethinkDb.Driver.Linq/project.json new file mode 100644 index 0000000..5e9d85c --- /dev/null +++ b/Source/RethinkDb.Driver.Linq/project.json @@ -0,0 +1,24 @@ +{ + "version": "1.0.0-*", + "description": "RethinkDb.Driver.Linq Class Library", + "authors": [ "jrote" ], + "tags": [ "" ], + "projectUrl": "", + "licenseUrl": "", + "dependencies": { + "Remotion.Linq": "2.0.2", + "RethinkDb.Driver": "2.3.1-beta-1" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { + "dependencies": { + "Microsoft.CSharp": "4.0.1-beta-23516", + "System.Collections": "4.0.11-beta-23516", + "System.Linq": "4.0.1-beta-23516", + "System.Runtime": "4.0.21-beta-23516", + "System.Threading": "4.0.11-beta-23516" + } + } + } +} diff --git a/Source/RethinkDb.Driver.sln b/Source/RethinkDb.Driver.sln index 27b82cb..68388b9 100644 --- a/Source/RethinkDb.Driver.sln +++ b/Source/RethinkDb.Driver.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +VisualStudioVersion = 14.0.25123.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RethinkDb.Driver.Tests", "RethinkDb.Driver.Tests\RethinkDb.Driver.Tests.csproj", "{40037F4C-E867-462A-B3E6-AFED77582442}" EndProject @@ -32,6 +32,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RethinkDb.Driver.ReGrid.Tes EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Builder", "..\Builder\Builder.fsproj", "{2E878DDF-5585-48F2-85E0-748579278C9A}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "RethinkDb.Driver.Linq", "RethinkDb.Driver.Linq\RethinkDb.Driver.Linq.xproj", "{9CD8EDD7-99A4-441B-9EB5-ADA7F94AC402}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "RethinkDb.Driver.Linq.Tests", "RethinkDb.Driver.Linq.Tests\RethinkDb.Driver.Linq.Tests.xproj", "{836CD478-2DF3-4A8B-A57B-58D3FCB01A7D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -66,6 +70,14 @@ Global {2E878DDF-5585-48F2-85E0-748579278C9A}.Debug|Any CPU.Build.0 = Debug|Any CPU {2E878DDF-5585-48F2-85E0-748579278C9A}.Release|Any CPU.ActiveCfg = Release|Any CPU {2E878DDF-5585-48F2-85E0-748579278C9A}.Release|Any CPU.Build.0 = Release|Any CPU + {9CD8EDD7-99A4-441B-9EB5-ADA7F94AC402}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9CD8EDD7-99A4-441B-9EB5-ADA7F94AC402}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9CD8EDD7-99A4-441B-9EB5-ADA7F94AC402}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9CD8EDD7-99A4-441B-9EB5-ADA7F94AC402}.Release|Any CPU.Build.0 = Release|Any CPU + {836CD478-2DF3-4A8B-A57B-58D3FCB01A7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {836CD478-2DF3-4A8B-A57B-58D3FCB01A7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {836CD478-2DF3-4A8B-A57B-58D3FCB01A7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {836CD478-2DF3-4A8B-A57B-58D3FCB01A7D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE