diff --git a/src/EntityFramework6.Npgsql/SqlGenerators/SqlBaseGenerator.cs b/src/EntityFramework6.Npgsql/SqlGenerators/SqlBaseGenerator.cs index 0cf543c..723def2 100644 --- a/src/EntityFramework6.Npgsql/SqlGenerators/SqlBaseGenerator.cs +++ b/src/EntityFramework6.Npgsql/SqlGenerators/SqlBaseGenerator.cs @@ -399,6 +399,15 @@ PendingProjectsNode VisitInputWithBinding(DbExpression expression, string bindin break; } + case DbExpressionKind.Function: + { + var function = (DbFunctionExpression)expression; + var input = new InputExpression( + VisitFunction(function.Function, function.Arguments, function.ResultType), bindingName); + + n = new PendingProjectsNode(bindingName, input); + break; + } default: throw new NotImplementedException(); } diff --git a/src/EntityFramework6.Npgsql/SqlGenerators/VisitedExpression.cs b/src/EntityFramework6.Npgsql/SqlGenerators/VisitedExpression.cs index 04ff22f..d1e94b2 100644 --- a/src/EntityFramework6.Npgsql/SqlGenerators/VisitedExpression.cs +++ b/src/EntityFramework6.Npgsql/SqlGenerators/VisitedExpression.cs @@ -526,7 +526,7 @@ internal override void WriteSql(StringBuilder sqlText) } else { - var wrap = !(_from is LiteralExpression || _from is ScanExpression); + var wrap = !(_from is LiteralExpression || _from is ScanExpression || _from is FunctionExpression); if (wrap) sqlText.Append("("); _from.WriteSql(sqlText); diff --git a/test/EntityFramework6.Npgsql.Tests/EntityFrameworkBasicTests.cs b/test/EntityFramework6.Npgsql.Tests/EntityFrameworkBasicTests.cs index 2ff87fb..dc39dc0 100644 --- a/test/EntityFramework6.Npgsql.Tests/EntityFrameworkBasicTests.cs +++ b/test/EntityFramework6.Npgsql.Tests/EntityFrameworkBasicTests.cs @@ -719,5 +719,43 @@ public void Test_issue_27_select_ef_generated_literals_from_inner_select() Assert.That(administrator.HasBlog, Is.True); } } + + [Test] + public void TestTableValuedStoredFunctions() + { + using (var context = new BloggingContext(ConnectionString)) + { + context.Database.Log = Console.Out.WriteLine; + + // Add some data and query it back using Stored Function + context.Blogs.Add(new Blog + { + Name = "Some blog1 name", + Posts = new List() + }); + context.Blogs.Add(new Blog + { + Name = "Some blog2 name", + Posts = new List() + }); + context.SaveChanges(); + + // Query back + var query = from b in context.GetBlogsByName("blog1") + select b; + var list = query.ToList(); + + Assert.AreEqual(1, list.Count); + Assert.AreEqual("Some blog1 name", list[0].Name); + + // Query with projection + var query2 = from b in context.GetBlogsByName("blog1") + select new { b.Name, Something = 1 }; + var list2 = query2.ToList(); + Assert.AreEqual(1, list2.Count); + Assert.AreEqual("Some blog1 name", list2[0].Name); + Assert.AreEqual(1, list2[0].Something); + } + } } } diff --git a/test/EntityFramework6.Npgsql.Tests/Support/EntityFrameworkTestBase.cs b/test/EntityFramework6.Npgsql.Tests/Support/EntityFrameworkTestBase.cs index 1fcc3ad..e26dbc6 100644 --- a/test/EntityFramework6.Npgsql.Tests/Support/EntityFrameworkTestBase.cs +++ b/test/EntityFramework6.Npgsql.Tests/Support/EntityFrameworkTestBase.cs @@ -30,6 +30,7 @@ using System.Linq; using System.Text; using System.ComponentModel.DataAnnotations.Schema; +using System.Data.Entity.Core.Mapping; using System.Data.Entity.Core.Metadata.Edm; using System.Data.Entity.Core.Objects; using System.Data.Entity.Infrastructure; @@ -58,6 +59,7 @@ public abstract class EntityFrameworkTestBase : TestBase createSequenceConn.ExecuteNonQuery("alter table \"dbo\".\"Posts\" alter column \"VarbitColumn\" type varbit using null"); createSequenceConn.ExecuteNonQuery("CREATE OR REPLACE FUNCTION \"dbo\".\"StoredAddFunction\"(integer, integer) RETURNS integer AS $$ SELECT $1 + $2; $$ LANGUAGE SQL;"); createSequenceConn.ExecuteNonQuery("CREATE OR REPLACE FUNCTION \"dbo\".\"StoredEchoFunction\"(integer) RETURNS integer AS $$ SELECT $1; $$ LANGUAGE SQL;"); + createSequenceConn.ExecuteNonQuery("CREATE OR REPLACE FUNCTION \"dbo\".\"GetBlogsByName\"(text) RETURNS TABLE(\"BlogId\" int, \"Name\" text, \"IntComputedValue\" int) as $$ select \"BlogId\", \"Name\", \"IntComputedValue\" from \"dbo\".\"Blogs\" where \"Name\" ilike '%' || $1 || '%' $$ LANGUAGE SQL;"); } } @@ -144,6 +146,15 @@ public static int StoredEchoFunction(int value) throw new NotSupportedException(); } + [DbFunction("BloggingContext", "GetBlogsByName")] + public IQueryable GetBlogsByName(string name) + { + ObjectParameter nameParameter = new ObjectParameter("Name", name); + + return ((IObjectContextAdapter)this).ObjectContext.CreateQuery( + $"[GetBlogsByName](@Name)", nameParameter); + } + private static DbCompiledModel CreateModel(NpgsqlConnection connection) { var dbModelBuilder = new DbModelBuilder(DbModelBuilderVersion.Latest); @@ -213,6 +224,87 @@ private static DbCompiledModel CreateModel(NpgsqlConnection connection) null); dbModel.StoreModel.AddItem(echoFunc); + var stringStoreType = dbModel.ProviderManifest.GetStoreTypes().First(x => x.ClrEquivalentType == typeof(string)); + var modelBlogStoreType = dbModel.StoreModel.EntityTypes.First(x => x.Name == typeof(Blog).Name); + var rowType = RowType.Create( + modelBlogStoreType.Properties.Select(x => + { + var clone = EdmProperty.Create(x.Name, x.TypeUsage); + clone.CollectionKind = x.CollectionKind; + clone.ConcurrencyMode = x.ConcurrencyMode; + clone.IsFixedLength = x.IsFixedLength; + clone.IsMaxLength = x.IsMaxLength; + clone.IsUnicode = x.IsUnicode; + clone.MaxLength = x.MaxLength; + clone.Precision = x.Precision; + clone.Scale = x.Scale; + clone.StoreGeneratedPattern = x.StoreGeneratedPattern; + clone.SetMetadataProperties(x + .MetadataProperties + .Where(metadataProerty => !clone + .MetadataProperties + .Any(cloneMetadataProperty => cloneMetadataProperty.Name.Equals(metadataProerty.Name)))); + return clone; + }), + null); + + var getBlogsFunc = EdmFunction.Create( + "StoredGetBlogsFunction", + "BloggingContext", + DataSpace.SSpace, + new EdmFunctionPayload + { + ParameterTypeSemantics = ParameterTypeSemantics.AllowImplicitConversion, + Schema = "dbo", + IsComposable = true, + IsNiladic = false, + IsBuiltIn = false, + IsAggregate = false, + StoreFunctionName = "GetBlogsByName", + ReturnParameters = new[] + { + FunctionParameter.Create("ReturnType1", rowType.GetCollectionType(), ParameterMode.ReturnValue) + }, + Parameters = new[] + { + FunctionParameter.Create("Name", stringStoreType, ParameterMode.In) + } + }, + null); + dbModel.StoreModel.AddItem(getBlogsFunc); + + var stringPrimitiveType = PrimitiveType.GetEdmPrimitiveTypes().First(x => x.ClrEquivalentType == typeof(string)); + var modelBlogConceptualType = dbModel.ConceptualModel.EntityTypes.First(x => x.Name == typeof(Blog).Name); + EdmFunction getBlogsFuncModel = EdmFunction.Create( + "GetBlogsByName", + dbModel.ConceptualModel.Container.Name, + DataSpace.CSpace, + new EdmFunctionPayload + { + IsFunctionImport = true, + IsComposable = true, + Parameters = new[] + { + FunctionParameter.Create("Name", stringPrimitiveType, ParameterMode.In) + }, + ReturnParameters = new[] + { + FunctionParameter.Create("ReturnType1", modelBlogConceptualType.GetCollectionType(), ParameterMode.ReturnValue) + }, + EntitySets = new[] + { + dbModel.ConceptualModel.Container.EntitySets.First(x => x.ElementType == modelBlogConceptualType) + } + }, + null); + dbModel.ConceptualModel.Container.AddFunctionImport(getBlogsFuncModel); + + dbModel.ConceptualToStoreMapping.AddFunctionImportMapping(new FunctionImportMappingComposable( + getBlogsFuncModel, + getBlogsFunc, + new FunctionImportResultMapping(), + dbModel.ConceptualToStoreMapping)); + var compiledModel = dbModel.Compile(); return compiledModel; }