From 6e702cbafb807c9ae72dab924a63e346095dfe1b Mon Sep 17 00:00:00 2001 From: Aivars Date: Tue, 16 Jul 2024 14:52:52 +0300 Subject: [PATCH 1/6] added AppendStoredProcedureCall --- .../Update/Internal/FbUpdateSqlGenerator.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Update/Internal/FbUpdateSqlGenerator.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Update/Internal/FbUpdateSqlGenerator.cs index d3b36d05..ec2fe580 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Update/Internal/FbUpdateSqlGenerator.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Update/Internal/FbUpdateSqlGenerator.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using System.Text; using FirebirdSql.EntityFrameworkCore.Firebird.Storage.Internal; @@ -252,4 +253,68 @@ void AppendSqlLiteral(StringBuilder commandStringBuilder, object value, IPropert mapping ??= Dependencies.TypeMappingSource.GetMappingForValue(value); commandStringBuilder.Append(mapping.GenerateProviderValueSqlLiteral(value)); } + + public override ResultSetMapping AppendStoredProcedureCall( + StringBuilder commandStringBuilder, + IReadOnlyModificationCommand command, + int commandPosition, + out bool requiresTransaction) + { + var storedProcedure = command.StoreStoredProcedure; + var resultSetMapping = ResultSetMapping.NoResults; + + foreach (var resultColumn in storedProcedure.ResultColumns) + { + resultSetMapping = ResultSetMapping.LastInResultSet; + if (resultColumn == command.RowsAffectedColumn) + { + resultSetMapping |= ResultSetMapping.ResultSetWithRowsAffectedOnly; + } + else + { + resultSetMapping = ResultSetMapping.LastInResultSet; + break; + } + } + + commandStringBuilder.Append("EXECUTE PROCEDURE "); + SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, storedProcedure.Name); + + if (storedProcedure.Parameters.Any()) + { + commandStringBuilder.Append(" "); + var first = true; + + for (var i = 0; i < command.ColumnModifications.Count; i++) + { + var columnModification = command.ColumnModifications[i]; + if (columnModification.Column is not IStoreStoredProcedureParameter parameter) + { + continue; + } + + if (parameter.Direction.HasFlag(ParameterDirection.Output)) + { + continue; + } + + if (first) + { + first = false; + } + else + { + commandStringBuilder.Append(", "); + } + SqlGenerationHelper.GenerateParameterNamePlaceholder( + commandStringBuilder, columnModification.UseOriginalValueParameter + ? columnModification.OriginalParameterName! + : columnModification.ParameterName!); + } + } + + commandStringBuilder.AppendLine(SqlGenerationHelper.StatementTerminator); + requiresTransaction = true; + return resultSetMapping; + } } From 7a80edeb58a015947c84fbf61c472c40745d1f71 Mon Sep 17 00:00:00 2001 From: Aivars Date: Wed, 28 Aug 2024 15:42:50 +0300 Subject: [PATCH 2/6] call sp as selectable; tests --- .../EndToEndUsingSP/DeleteTestsUsingSP.cs | 150 ++++++++ .../EndToEndUsingSP/InsertTestsUsingSP.cs | 321 ++++++++++++++++ .../EndToEndUsingSP/UpdateTestsUsingSP.cs | 352 ++++++++++++++++++ .../Update/Internal/FbUpdateSqlGenerator.cs | 7 +- 4 files changed, 828 insertions(+), 2 deletions(-) create mode 100644 src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/DeleteTestsUsingSP.cs create mode 100644 src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/InsertTestsUsingSP.cs create mode 100644 src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/UpdateTestsUsingSP.cs diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/DeleteTestsUsingSP.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/DeleteTestsUsingSP.cs new file mode 100644 index 00000000..637698e0 --- /dev/null +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/DeleteTestsUsingSP.cs @@ -0,0 +1,150 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; + +namespace FirebirdSql.EntityFrameworkCore.Firebird.Tests.EndToEnd; + +public class DeleteTestsUsingSP : EntityFrameworkCoreTestsBase +{ + class DeleteContext : FbTestDbContext + { + public DeleteContext(string connectionString) + : base(connectionString) + { } + + protected override void OnTestModelCreating(ModelBuilder modelBuilder) + { + base.OnTestModelCreating(modelBuilder); + + var insertEntityConf = modelBuilder.Entity(); + insertEntityConf.Property(x => x.Id).HasColumnName("ID"); + insertEntityConf.Property(x => x.Name).HasColumnName("NAME"); + insertEntityConf.ToTable("TEST_DELETE_USP"); + modelBuilder.Entity().DeleteUsingStoredProcedure("SP_TEST_DELETE", + storedProcedureBuilder => + { + storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); + storedProcedureBuilder.HasRowsAffectedResultColumn(); + }); + } + } + class DeleteEntity + { + public int Id { get; set; } + public string Name { get; set; } + } + [Test] + public async Task Delete() + { + await using (var db = await GetDbContext()) + { + await db.Database.ExecuteSqlRawAsync("create table test_delete_usp (id int primary key, name varchar(20))"); + await db.Database.ExecuteSqlRawAsync("insert into test_delete_usp values (65, 'test')"); + await db.Database.ExecuteSqlRawAsync("insert into test_delete_usp values (66, 'test')"); + await db.Database.ExecuteSqlRawAsync("insert into test_delete_usp values (67, 'test')"); + var sp = """ + create or alter procedure sp_test_delete ( + pid integer) + returns (rowcount integer) + as + begin + delete from test_delete_usp where id = :pid; + rowcount = row_count; + suspend; + end + """; + await db.Database.ExecuteSqlRawAsync(sp); + var entity = new DeleteEntity() { Id = 66 }; + var entry = db.Attach(entity); + entry.State = EntityState.Deleted; + await db.SaveChangesAsync(); + var values = await db.Set() + .FromSqlRaw("select * from test_delete_usp") + .AsNoTracking() + .OrderBy(x => x.Id) + .ToListAsync(); + Assert.AreEqual(2, values.Count()); + Assert.AreEqual(65, values[0].Id); + Assert.AreEqual(67, values[1].Id); + } + } + + class ConcurrencyDeleteContext : FbTestDbContext + { + public ConcurrencyDeleteContext(string connectionString) + : base(connectionString) + { } + + protected override void OnTestModelCreating(ModelBuilder modelBuilder) + { + base.OnTestModelCreating(modelBuilder); + + var insertEntityConf = modelBuilder.Entity(); + insertEntityConf.Property(x => x.Id).HasColumnName("ID"); + insertEntityConf.Property(x => x.Name).HasColumnName("NAME"); + insertEntityConf.Property(x => x.Stamp).HasColumnName("STAMP") + .IsRowVersion(); + insertEntityConf.ToTable("TEST_DELETE_CONCURRENCY_USP"); + modelBuilder.Entity().DeleteUsingStoredProcedure("SP_TEST_DELETE_CONCURRENCY", + storedProcedureBuilder => + { + storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); + storedProcedureBuilder.HasOriginalValueParameter(x => x.Stamp); + storedProcedureBuilder.HasRowsAffectedResultColumn(); + }); + } + } + class ConcurrencyDeleteEntity + { + public int Id { get; set; } + public string Name { get; set; } + public DateTime Stamp { get; set; } + } + [Test] + public async Task ConcurrencyDelete() + { + await using (var db = await GetDbContext()) + { + await db.Database.ExecuteSqlRawAsync("create table test_delete_concurrency_usp (id int primary key, name varchar(20), stamp timestamp)"); + await db.Database.ExecuteSqlRawAsync("insert into test_delete_concurrency_usp values (65, 'test', current_timestamp)"); + await db.Database.ExecuteSqlRawAsync("insert into test_delete_concurrency_usp values (66, 'test', current_timestamp)"); + await db.Database.ExecuteSqlRawAsync("insert into test_delete_concurrency_usp values (67, 'test', current_timestamp)"); + var sp = """ + create or alter procedure sp_test_delete_concurrency ( + pid integer, + pstamp timestamp) + returns (rowcount integer) + as + begin + delete from test_delete_concurrency_usp where id = :pid and stamp = :pstamp; + rowcount = row_count; + if (rowcount > 0) then suspend; + end + """; + await db.Database.ExecuteSqlRawAsync(sp); + var entity = new ConcurrencyDeleteEntity() { Id = 66, Stamp = new DateTime(1970, 1, 1) }; + var entry = db.Attach(entity); + entry.State = EntityState.Deleted; + Assert.ThrowsAsync(() => db.SaveChangesAsync()); + } + } +} diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/InsertTestsUsingSP.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/InsertTestsUsingSP.cs new file mode 100644 index 00000000..4293fd60 --- /dev/null +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/InsertTestsUsingSP.cs @@ -0,0 +1,321 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; + +namespace FirebirdSql.EntityFrameworkCore.Firebird.Tests.EndToEnd; + +public class InsertTestsUsingSP : EntityFrameworkCoreTestsBase +{ + class InsertContext : FbTestDbContext + { + public InsertContext(string connectionString) + : base(connectionString) + { } + + protected override void OnTestModelCreating(ModelBuilder modelBuilder) + { + base.OnTestModelCreating(modelBuilder); + + var insertEntityConf = modelBuilder.Entity(); + insertEntityConf.Property(x => x.Id).HasColumnName("ID"); + insertEntityConf.Property(x => x.Name).HasColumnName("NAME"); + insertEntityConf.ToTable("TEST_INSERT_USP"); + modelBuilder.Entity().InsertUsingStoredProcedure("SP_TEST_INSERT", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(x => x.Id); + storedProcedureBuilder.HasParameter(x => x.Name); + storedProcedureBuilder.HasResultColumn(x => x.Id); + }); + } + } + class InsertEntity + { + public int Id { get; set; } + public string Name { get; set; } + } + [Test] + public async Task Insert() + { + await using (var db = await GetDbContext()) + { + await db.Database.ExecuteSqlRawAsync("create table test_insert_usp (id int primary key, name varchar(20))"); + var sp = """ + create or alter procedure sp_test_insert ( + pid integer, + pname varchar(20)) + returns (rid integer) + as + begin + insert into test_insert_usp (id, name) values (:pid, :pname) + returning id into :rid; + suspend; + end + """; + await db.Database.ExecuteSqlRawAsync(sp); + var entity = new InsertEntity() { Id = -6, Name = "foobar" }; + await db.AddAsync(entity); + await db.SaveChangesAsync(); + Assert.AreEqual(-6, entity.Id); + } + } + + class IdentityInsertContext : FbTestDbContext + { + public IdentityInsertContext(string connectionString) + : base(connectionString) + { } + + protected override void OnTestModelCreating(ModelBuilder modelBuilder) + { + base.OnTestModelCreating(modelBuilder); + + var insertEntityConf = modelBuilder.Entity(); + insertEntityConf.Property(x => x.Id).HasColumnName("ID") + .UseIdentityColumn(); + insertEntityConf.Property(x => x.Name).HasColumnName("NAME"); + insertEntityConf.ToTable("TEST_INSERT_IDENTITY_USP"); + modelBuilder.Entity().InsertUsingStoredProcedure("SP_TEST_INSERT_IDENTITY", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(x => x.Name); + storedProcedureBuilder.HasResultColumn(x => x.Id); + }); + } + } + class IdentityInsertEntity + { + public int Id { get; set; } + public string Name { get; set; } + } + [Test] + public async Task IdentityInsert() + { + if (!EnsureServerVersionAtLeast(new Version(3, 0, 0, 0))) + return; + + var id = ServerVersion >= new Version(4, 0, 0, 0) ? 26 : 27; + + await using (var db = await GetDbContext()) + { + await db.Database.ExecuteSqlRawAsync("create table test_insert_identity_usp (id int generated by default as identity (start with 26) primary key, name varchar(20))"); + var sp = """ + create or alter procedure sp_test_insert_identity ( + pname varchar(20)) + returns (rid integer) + as + begin + insert into test_insert_identity_usp (name) values (:pname) + returning id into :rid; + suspend; + end + """; + await db.Database.ExecuteSqlRawAsync(sp); + var entity = new IdentityInsertEntity() { Name = "foobar" }; + await db.AddAsync(entity); + await db.SaveChangesAsync(); + Assert.AreEqual(id, entity.Id); + } + } + + class SequenceInsertContext : FbTestDbContext + { + public SequenceInsertContext(string connectionString) + : base(connectionString) + { } + + protected override void OnTestModelCreating(ModelBuilder modelBuilder) + { + base.OnTestModelCreating(modelBuilder); + + var insertEntityConf = modelBuilder.Entity(); + insertEntityConf.Property(x => x.Id).HasColumnName("ID") + .UseSequenceTrigger(); + insertEntityConf.Property(x => x.Name).HasColumnName("NAME"); + insertEntityConf.ToTable("TEST_INSERT_SEQUENCE_USP"); + modelBuilder.Entity().InsertUsingStoredProcedure("SP_TEST_INSERT_SEQUENCE", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(x => x.Name); + storedProcedureBuilder.HasResultColumn(x => x.Id); + }); + } + } + class SequenceInsertEntity + { + public int Id { get; set; } + public string Name { get; set; } + } + [Test] + public async Task SequenceInsert() + { + var id = ServerVersion >= new Version(4, 0, 0, 0) ? 30 : 31; + + await using (var db = await GetDbContext()) + { + await db.Database.ExecuteSqlRawAsync("create table test_insert_sequence_usp (id int primary key, name varchar(20))"); + await db.Database.ExecuteSqlRawAsync("create sequence seq_test_insert_sequence_usp"); + await db.Database.ExecuteSqlRawAsync("alter sequence seq_test_insert_sequence_usp restart with 30"); + await db.Database.ExecuteSqlRawAsync("create trigger test_insert_sequence_id_usp before insert on test_insert_sequence_usp as begin if (new.id is null) then begin new.id = next value for seq_test_insert_sequence_usp; end end"); + var sp = """ + create or alter procedure sp_test_insert_sequence ( + pname varchar(20)) + returns (rid integer) + as + begin + insert into test_insert_sequence_usp (name) values (:pname) + returning id into :rid; + suspend; + end + """; + await db.Database.ExecuteSqlRawAsync(sp); + var entity = new SequenceInsertEntity() { Name = "foobar" }; + await db.AddAsync(entity); + await db.SaveChangesAsync(); + Assert.AreEqual(id, entity.Id); + } + } + + class DefaultValuesInsertContext : FbTestDbContext + { + public DefaultValuesInsertContext(string connectionString) + : base(connectionString) + { } + + protected override void OnTestModelCreating(ModelBuilder modelBuilder) + { + base.OnTestModelCreating(modelBuilder); + + var insertEntityConf = modelBuilder.Entity(); + insertEntityConf.Property(x => x.Id).HasColumnName("ID") + .ValueGeneratedOnAdd(); + insertEntityConf.Property(x => x.Name).HasColumnName("NAME") + .ValueGeneratedOnAdd(); + insertEntityConf.ToTable("TEST_INSERT_DEVAULTVALUES_USP"); + modelBuilder.Entity().InsertUsingStoredProcedure("SP_TEST_INSERT_DEFAULTVALUES", + storedProcedureBuilder => + { + storedProcedureBuilder.HasResultColumn(x => x.Id); + storedProcedureBuilder.HasResultColumn(x => x.Name); + }); + } + } + class DefaultValuesInsertEntity + { + public int Id { get; set; } + public string Name { get; set; } + } + [Test] + public async Task DefaultValuesInsert() + { + if (!EnsureServerVersionAtLeast(new Version(3, 0, 0, 0))) + return; + + var id = ServerVersion >= new Version(4, 0, 0, 0) ? 26 : 27; + + await using (var db = await GetDbContext()) + { + await db.Database.ExecuteSqlRawAsync("create table test_insert_devaultvalues_usp (id int generated by default as identity (start with 26) primary key, name generated always as (id || 'foobar'))"); + var sp = """ + create or alter procedure sp_test_insert_defaultvalues + returns (rid integer, rname varchar(20)) + as + begin + insert into test_insert_devaultvalues_usp default values + returning id, name into :rid, :rname; + suspend; + end + """; + await db.Database.ExecuteSqlRawAsync(sp); + var entity = new DefaultValuesInsertEntity() { }; + await db.AddAsync(entity); + await db.SaveChangesAsync(); + Assert.AreEqual(id, entity.Id); + Assert.AreEqual($"{id}foobar", entity.Name); + } + } + + class TwoComputedInsertContext : FbTestDbContext + { + public TwoComputedInsertContext(string connectionString) + : base(connectionString) + { } + + protected override void OnTestModelCreating(ModelBuilder modelBuilder) + { + base.OnTestModelCreating(modelBuilder); + + var insertEntityConf = modelBuilder.Entity(); + insertEntityConf.Property(x => x.Id).HasColumnName("ID") + .UseIdentityColumn(); + insertEntityConf.Property(x => x.Name).HasColumnName("NAME"); + insertEntityConf.Property(x => x.Computed1).HasColumnName("COMPUTED1") + .ValueGeneratedOnAddOrUpdate(); + insertEntityConf.Property(x => x.Computed2).HasColumnName("COMPUTED2") + .ValueGeneratedOnAddOrUpdate(); + insertEntityConf.ToTable("TEST_INSERT_2COMPUTED_USP"); + modelBuilder.Entity().InsertUsingStoredProcedure("SP_TEST_INSERT_2COMPUTED", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(x => x.Name); + storedProcedureBuilder.HasResultColumn(x => x.Id); + storedProcedureBuilder.HasResultColumn(x => x.Computed1); + storedProcedureBuilder.HasResultColumn(x => x.Computed2); + }); + } + } + class TwoComputedInsertEntity + { + public int Id { get; set; } + public string Name { get; set; } + public string Computed1 { get; set; } + public string Computed2 { get; set; } + } + [Test] + public async Task TwoComputedInsert() + { + if (!EnsureServerVersionAtLeast(new Version(3, 0, 0, 0))) + return; + + await using (var db = await GetDbContext()) + { + await db.Database.ExecuteSqlRawAsync("create table test_insert_2computed_usp (id int generated by default as identity (start with 26) primary key, name varchar(20), computed1 generated always as ('1' || name), computed2 generated always as ('2' || name))"); + var sp = """ + create or alter procedure sp_test_insert_2computed ( + pname varchar(20)) + returns (rid integer, rcomputed1 varchar(25), rcomputed2 varchar(25)) + as + begin + insert into test_insert_2computed_usp (name) values (:pname) + returning id, computed1, computed2 + into :rid, :rcomputed1, :rcomputed2; + suspend; + end + """; + await db.Database.ExecuteSqlRawAsync(sp); + var entity = new TwoComputedInsertEntity() { Name = "foobar" }; + await db.AddAsync(entity); + await db.SaveChangesAsync(); + Assert.AreEqual("1foobar", entity.Computed1); + Assert.AreEqual("2foobar", entity.Computed2); + } + } +} diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/UpdateTestsUsingSP.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/UpdateTestsUsingSP.cs new file mode 100644 index 00000000..c3045e00 --- /dev/null +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/UpdateTestsUsingSP.cs @@ -0,0 +1,352 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; + +namespace FirebirdSql.EntityFrameworkCore.Firebird.Tests.EndToEnd; + +public class UpdateTestsUsingSP : EntityFrameworkCoreTestsBase +{ + class UpdateContext : FbTestDbContext + { + public UpdateContext(string connectionString) + : base(connectionString) + { } + + protected override void OnTestModelCreating(ModelBuilder modelBuilder) + { + base.OnTestModelCreating(modelBuilder); + + var insertEntityConf = modelBuilder.Entity(); + insertEntityConf.Property(x => x.Id).HasColumnName("ID"); + insertEntityConf.Property(x => x.Foo).HasColumnName("FOO"); + insertEntityConf.Property(x => x.Bar).HasColumnName("BAR"); + insertEntityConf.ToTable("TEST_UPDATE_USP"); + modelBuilder.Entity().UpdateUsingStoredProcedure("SP_TEST_UPDATE", + storedProcedureBuilder => + { + storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); + storedProcedureBuilder.HasParameter(x => x.Foo); + storedProcedureBuilder.HasParameter(x => x.Bar); + storedProcedureBuilder.HasRowsAffectedResultColumn(); + }); + } + } + class UpdateEntity + { + public int Id { get; set; } + public string Foo { get; set; } + public string Bar { get; set; } + } + [Test] + public async Task Update() + { + await using (var db = await GetDbContext()) + { + await db.Database.ExecuteSqlRawAsync("create table test_update_usp (id int primary key, foo varchar(20), bar varchar(20))"); + await db.Database.ExecuteSqlRawAsync("update or insert into test_update_usp values (66, 'foo', 'bar')"); + var sp = """ + create procedure sp_test_update ( + pid integer, + pfoo varchar(20), + pbar varchar(20)) + returns (rowcount integer) + as + begin + update test_update_usp set foo = :pfoo, bar = :pbar + where id = :pid; + rowcount = row_count; + suspend; + end + """; + await db.Database.ExecuteSqlRawAsync(sp); + var entity = new UpdateEntity() { Id = 66, Foo = "test", Bar = "test" }; + var entry = db.Attach(entity); + entry.Property(x => x.Foo).IsModified = true; + await db.SaveChangesAsync(); + var value = await db.Set() + .FromSqlRaw("select * from test_update_usp where id = 66") + .AsNoTracking() + .FirstAsync(); + Assert.AreEqual("test", value.Foo); + Assert.AreEqual("test", value.Bar); + //Assert.AreNotEqual("test", value.Bar); //this has to fail + } + } + + class ComputedUpdateContext : FbTestDbContext + { + public ComputedUpdateContext(string connectionString) + : base(connectionString) + { } + + protected override void OnTestModelCreating(ModelBuilder modelBuilder) + { + base.OnTestModelCreating(modelBuilder); + + var insertEntityConf = modelBuilder.Entity(); + insertEntityConf.Property(x => x.Id).HasColumnName("ID"); + insertEntityConf.Property(x => x.Foo).HasColumnName("FOO"); + insertEntityConf.Property(x => x.Bar).HasColumnName("BAR"); + insertEntityConf.Property(x => x.Computed).HasColumnName("COMPUTED") + .ValueGeneratedOnAddOrUpdate(); + insertEntityConf.ToTable("TEST_UPDATE_COMPUTED_USP"); + modelBuilder.Entity().UpdateUsingStoredProcedure("SP_TEST_UPDATE_COMPUTED", + storedProcedureBuilder => + { + storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); + storedProcedureBuilder.HasParameter(x => x.Foo); + storedProcedureBuilder.HasParameter(x => x.Bar); + storedProcedureBuilder.HasResultColumn(x => x.Computed); + }); + } + } + class ComputedUpdateEntity + { + public int Id { get; set; } + public string Foo { get; set; } + public string Bar { get; set; } + public string Computed { get; set; } + } + [Test] + public async Task ComputedUpdate() + { + await using (var db = await GetDbContext()) + { + await db.Database.ExecuteSqlRawAsync("create table test_update_computed_usp (id int primary key, foo varchar(20), bar varchar(20), computed generated always as (foo || bar))"); + await db.Database.ExecuteSqlRawAsync("update or insert into test_update_computed_usp values (66, 'foo', 'bar')"); + var sp = """ + create procedure sp_test_update_computed ( + pid integer, + pfoo varchar(20), + pbar varchar(20)) + returns (rcomputed varchar(50)) + as + begin + update test_update_computed_usp set foo = :pfoo, bar = :pbar + where id = :pid + returning computed into :rcomputed; + if (row_count > 0) then suspend; + end + """; + await db.Database.ExecuteSqlRawAsync(sp); + var entity = new ComputedUpdateEntity() { Id = 66, Foo = "test", Bar = "bar" }; + var entry = db.Attach(entity); + entry.Property(x => x.Foo).IsModified = true; + await db.SaveChangesAsync(); + Assert.AreEqual("testbar", entity.Computed); + } + } + + class ConcurrencyUpdateContext : FbTestDbContext + { + public ConcurrencyUpdateContext(string connectionString) + : base(connectionString) + { } + + protected override void OnTestModelCreating(ModelBuilder modelBuilder) + { + base.OnTestModelCreating(modelBuilder); + + var insertEntityConf = modelBuilder.Entity(); + insertEntityConf.Property(x => x.Id).HasColumnName("ID"); + insertEntityConf.Property(x => x.Foo).HasColumnName("FOO"); + insertEntityConf.Property(x => x.Stamp).HasColumnName("STAMP") + .IsRowVersion(); + insertEntityConf.ToTable("TEST_UPDATE_CONCURRENCY_USP"); + modelBuilder.Entity().UpdateUsingStoredProcedure("SP_TEST_UPDATE_CONCURRENCY", + storedProcedureBuilder => + { + storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); + storedProcedureBuilder.HasParameter(x => x.Foo); + storedProcedureBuilder.HasOriginalValueParameter(x => x.Stamp); + storedProcedureBuilder.HasResultColumn(x => x.Stamp); + }); + } + } + class ConcurrencyUpdateEntity + { + public int Id { get; set; } + public string Foo { get; set; } + public DateTime Stamp { get; set; } + } + [Test] + public async Task ConcurrencyUpdate() + { + await using (var db = await GetDbContext()) + { + await db.Database.ExecuteSqlRawAsync("create table test_update_concurrency_usp (id int primary key, foo varchar(20), stamp timestamp)"); + await db.Database.ExecuteSqlRawAsync("update or insert into test_update_concurrency_usp values (66, 'foo', current_timestamp)"); + var sp = """ + create procedure sp_test_update_concurrency ( + pid integer, + pfoo varchar(20), + pstamp timestamp) + returns (rstamp timestamp) + as + begin + update test_update_concurrency_usp set foo = :pfoo, stamp = current_timestamp + where id = :pid and stamp = :pstamp + returning stamp into :rstamp; + if (row_count > 0) then suspend; + end + """; + await db.Database.ExecuteSqlRawAsync(sp); + var entity = new ConcurrencyUpdateEntity() { Id = 66, Foo = "test", Stamp = new DateTime(1970, 1, 1) }; + var entry = db.Attach(entity); + entry.Property(x => x.Foo).IsModified = true; + Assert.ThrowsAsync(() => db.SaveChangesAsync()); + } + } + + class ConcurrencyUpdateNoGeneratedContext : FbTestDbContext + { + public ConcurrencyUpdateNoGeneratedContext(string connectionString) + : base(connectionString) + { } + + protected override void OnTestModelCreating(ModelBuilder modelBuilder) + { + base.OnTestModelCreating(modelBuilder); + + var insertEntityConf = modelBuilder.Entity(); + insertEntityConf.Property(x => x.Id).HasColumnName("ID"); + insertEntityConf.Property(x => x.Foo).HasColumnName("FOO"); + insertEntityConf.Property(x => x.Stamp).HasColumnName("STAMP") + .IsConcurrencyToken(); + insertEntityConf.ToTable("TEST_UPDATE_CONCURRENCY_NG_USP"); + modelBuilder.Entity().UpdateUsingStoredProcedure("SP_TEST_UPDATE_CONCURRENCY_NG", + storedProcedureBuilder => + { + storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); + storedProcedureBuilder.HasParameter(x => x.Foo); + storedProcedureBuilder.HasOriginalValueParameter(x => x.Stamp); + storedProcedureBuilder.HasRowsAffectedResultColumn(); + }); + } + } + class ConcurrencyUpdateNoGeneratedEntity + { + public int Id { get; set; } + public string Foo { get; set; } + public DateTime Stamp { get; set; } + } + [Test] + public async Task ConcurrencyUpdateNoGenerated() + { + await using (var db = await GetDbContext()) + { + await db.Database.ExecuteSqlRawAsync("create table test_update_concurrency_ng_usp (id int primary key, foo varchar(20), stamp timestamp)"); + await db.Database.ExecuteSqlRawAsync("update or insert into test_update_concurrency_ng_usp values (66, 'foo', current_timestamp)"); + var sp = """ + create procedure sp_test_update_concurrency_ng ( + pid integer, + pfoo varchar(20), + pstamp timestamp) + returns (rowcount integer) + as + begin + update test_update_concurrency_ng_usp set foo = :pfoo, stamp = current_timestamp + where id = :pid and stamp = :pstamp; + rowcount = row_count; + if (rowcount > 0) then suspend; + end + """; + await db.Database.ExecuteSqlRawAsync(sp); + var entity = new ConcurrencyUpdateNoGeneratedEntity() { Id = 66, Foo = "test", Stamp = new DateTime(1970, 1, 1) }; + var entry = db.Attach(entity); + entry.Property(x => x.Foo).IsModified = true; + entry.Property(x => x.Stamp).IsModified = true; + Assert.ThrowsAsync(() => db.SaveChangesAsync()); + } + } + + class TwoComputedUpdateContext : FbTestDbContext + { + public TwoComputedUpdateContext(string connectionString) + : base(connectionString) + { } + + protected override void OnTestModelCreating(ModelBuilder modelBuilder) + { + base.OnTestModelCreating(modelBuilder); + + var insertEntityConf = modelBuilder.Entity(); + insertEntityConf.Property(x => x.Id).HasColumnName("ID"); + insertEntityConf.Property(x => x.Foo).HasColumnName("FOO"); + insertEntityConf.Property(x => x.Bar).HasColumnName("BAR"); + insertEntityConf.Property(x => x.Computed1).HasColumnName("COMPUTED1") + .ValueGeneratedOnAddOrUpdate(); + insertEntityConf.Property(x => x.Computed2).HasColumnName("COMPUTED2") + .ValueGeneratedOnAddOrUpdate(); + insertEntityConf.ToTable("TEST_UPDATE_2COMPUTED_USP"); + modelBuilder.Entity().UpdateUsingStoredProcedure("SP_TEST_UPDATE_2COMPUTED", + storedProcedureBuilder => + { + storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); + storedProcedureBuilder.HasParameter(x => x.Foo); + storedProcedureBuilder.HasParameter(x => x.Bar); + storedProcedureBuilder.HasResultColumn(x => x.Computed1); + storedProcedureBuilder.HasResultColumn(x => x.Computed2); + }); + } + } + class TwoComputedUpdateEntity + { + public int Id { get; set; } + public string Foo { get; set; } + public string Bar { get; set; } + public string Computed1 { get; set; } + public string Computed2 { get; set; } + } + [Test] + public async Task TwoComputedUpdate() + { + await using (var db = await GetDbContext()) + { + await db.Database.ExecuteSqlRawAsync("create table test_update_2computed_usp (id int primary key, foo varchar(20), bar varchar(20), computed1 generated always as (foo || bar), computed2 generated always as (bar || bar))"); + await db.Database.ExecuteSqlRawAsync("update or insert into test_update_2computed_usp values (66, 'foo', 'bar')"); + var sp = """ + create procedure sp_test_update_2computed ( + pid integer, + pfoo varchar(20), + pbar varchar(20)) + returns ( + rcomputed1 varchar(50), + rcomputed2 varchar(50)) + as + begin + update test_update_2computed_usp set foo = :pfoo, bar = :pbar + where id = :pid + returning computed1, computed2 into :rcomputed1, :rcomputed2; + if (row_count > 0) then suspend; + end + """; + await db.Database.ExecuteSqlRawAsync(sp); + var entity = new TwoComputedUpdateEntity() { Id = 66, Foo = "test", Bar = "bar" }; + var entry = db.Attach(entity); + entry.Property(x => x.Foo).IsModified = true; + await db.SaveChangesAsync(); + Assert.AreEqual("testbar", entity.Computed1); + Assert.AreEqual("barbar", entity.Computed2); + } + } +} diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Update/Internal/FbUpdateSqlGenerator.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Update/Internal/FbUpdateSqlGenerator.cs index ec2fe580..2fa97667 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Update/Internal/FbUpdateSqlGenerator.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Update/Internal/FbUpdateSqlGenerator.cs @@ -277,12 +277,13 @@ public override ResultSetMapping AppendStoredProcedureCall( } } - commandStringBuilder.Append("EXECUTE PROCEDURE "); + commandStringBuilder.Append("SELECT * FROM "); SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, storedProcedure.Name); if (storedProcedure.Parameters.Any()) { - commandStringBuilder.Append(" "); + commandStringBuilder.Append("("); + var first = true; for (var i = 0; i < command.ColumnModifications.Count; i++) @@ -311,6 +312,8 @@ public override ResultSetMapping AppendStoredProcedureCall( ? columnModification.OriginalParameterName! : columnModification.ParameterName!); } + + commandStringBuilder.Append(")"); } commandStringBuilder.AppendLine(SqlGenerationHelper.StatementTerminator); From b8e4a3fae760004cf655afeaf8be7527c8620cd6 Mon Sep 17 00:00:00 2001 From: Aivars Date: Fri, 30 Aug 2024 19:00:06 +0300 Subject: [PATCH 3/6] fixes --- .../EndToEndUsingSP/DeleteTestsUsingSP.cs | 25 +++--- .../EndToEndUsingSP/InsertTestsUsingSP.cs | 56 ++++++------ .../EndToEndUsingSP/UpdateTestsUsingSP.cs | 85 ++++++++++--------- 3 files changed, 85 insertions(+), 81 deletions(-) diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/DeleteTestsUsingSP.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/DeleteTestsUsingSP.cs index 637698e0..a1b45697 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/DeleteTestsUsingSP.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/DeleteTestsUsingSP.cs @@ -40,11 +40,11 @@ protected override void OnTestModelCreating(ModelBuilder modelBuilder) insertEntityConf.Property(x => x.Name).HasColumnName("NAME"); insertEntityConf.ToTable("TEST_DELETE_USP"); modelBuilder.Entity().DeleteUsingStoredProcedure("SP_TEST_DELETE", - storedProcedureBuilder => - { - storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); - storedProcedureBuilder.HasRowsAffectedResultColumn(); - }); + storedProcedureBuilder => + { + storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); + storedProcedureBuilder.HasRowsAffectedResultColumn(); + }); } } class DeleteEntity @@ -105,12 +105,12 @@ protected override void OnTestModelCreating(ModelBuilder modelBuilder) .IsRowVersion(); insertEntityConf.ToTable("TEST_DELETE_CONCURRENCY_USP"); modelBuilder.Entity().DeleteUsingStoredProcedure("SP_TEST_DELETE_CONCURRENCY", - storedProcedureBuilder => - { - storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); - storedProcedureBuilder.HasOriginalValueParameter(x => x.Stamp); - storedProcedureBuilder.HasRowsAffectedResultColumn(); - }); + storedProcedureBuilder => + { + storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); + storedProcedureBuilder.HasOriginalValueParameter(x => x.Stamp); + storedProcedureBuilder.HasRowsAffectedResultColumn(); + }); } } class ConcurrencyDeleteEntity @@ -137,7 +137,8 @@ create or alter procedure sp_test_delete_concurrency ( begin delete from test_delete_concurrency_usp where id = :pid and stamp = :pstamp; rowcount = row_count; - if (rowcount > 0) then suspend; + if (rowcount > 0) then + suspend; end """; await db.Database.ExecuteSqlRawAsync(sp); diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/InsertTestsUsingSP.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/InsertTestsUsingSP.cs index 4293fd60..29654372 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/InsertTestsUsingSP.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/InsertTestsUsingSP.cs @@ -39,12 +39,12 @@ protected override void OnTestModelCreating(ModelBuilder modelBuilder) insertEntityConf.Property(x => x.Name).HasColumnName("NAME"); insertEntityConf.ToTable("TEST_INSERT_USP"); modelBuilder.Entity().InsertUsingStoredProcedure("SP_TEST_INSERT", - storedProcedureBuilder => - { - storedProcedureBuilder.HasParameter(x => x.Id); - storedProcedureBuilder.HasParameter(x => x.Name); - storedProcedureBuilder.HasResultColumn(x => x.Id); - }); + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(x => x.Id); + storedProcedureBuilder.HasParameter(x => x.Name); + storedProcedureBuilder.HasResultColumn(x => x.Id); + }); } } class InsertEntity @@ -94,11 +94,11 @@ protected override void OnTestModelCreating(ModelBuilder modelBuilder) insertEntityConf.Property(x => x.Name).HasColumnName("NAME"); insertEntityConf.ToTable("TEST_INSERT_IDENTITY_USP"); modelBuilder.Entity().InsertUsingStoredProcedure("SP_TEST_INSERT_IDENTITY", - storedProcedureBuilder => - { - storedProcedureBuilder.HasParameter(x => x.Name); - storedProcedureBuilder.HasResultColumn(x => x.Id); - }); + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(x => x.Name); + storedProcedureBuilder.HasResultColumn(x => x.Id); + }); } } class IdentityInsertEntity @@ -152,11 +152,11 @@ protected override void OnTestModelCreating(ModelBuilder modelBuilder) insertEntityConf.Property(x => x.Name).HasColumnName("NAME"); insertEntityConf.ToTable("TEST_INSERT_SEQUENCE_USP"); modelBuilder.Entity().InsertUsingStoredProcedure("SP_TEST_INSERT_SEQUENCE", - storedProcedureBuilder => - { - storedProcedureBuilder.HasParameter(x => x.Name); - storedProcedureBuilder.HasResultColumn(x => x.Id); - }); + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(x => x.Name); + storedProcedureBuilder.HasResultColumn(x => x.Id); + }); } } class SequenceInsertEntity @@ -211,11 +211,11 @@ protected override void OnTestModelCreating(ModelBuilder modelBuilder) .ValueGeneratedOnAdd(); insertEntityConf.ToTable("TEST_INSERT_DEVAULTVALUES_USP"); modelBuilder.Entity().InsertUsingStoredProcedure("SP_TEST_INSERT_DEFAULTVALUES", - storedProcedureBuilder => - { - storedProcedureBuilder.HasResultColumn(x => x.Id); - storedProcedureBuilder.HasResultColumn(x => x.Name); - }); + storedProcedureBuilder => + { + storedProcedureBuilder.HasResultColumn(x => x.Id); + storedProcedureBuilder.HasResultColumn(x => x.Name); + }); } } class DefaultValuesInsertEntity @@ -273,13 +273,13 @@ protected override void OnTestModelCreating(ModelBuilder modelBuilder) .ValueGeneratedOnAddOrUpdate(); insertEntityConf.ToTable("TEST_INSERT_2COMPUTED_USP"); modelBuilder.Entity().InsertUsingStoredProcedure("SP_TEST_INSERT_2COMPUTED", - storedProcedureBuilder => - { - storedProcedureBuilder.HasParameter(x => x.Name); - storedProcedureBuilder.HasResultColumn(x => x.Id); - storedProcedureBuilder.HasResultColumn(x => x.Computed1); - storedProcedureBuilder.HasResultColumn(x => x.Computed2); - }); + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(x => x.Name); + storedProcedureBuilder.HasResultColumn(x => x.Id); + storedProcedureBuilder.HasResultColumn(x => x.Computed1); + storedProcedureBuilder.HasResultColumn(x => x.Computed2); + }); } } class TwoComputedInsertEntity diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/UpdateTestsUsingSP.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/UpdateTestsUsingSP.cs index c3045e00..660d67bb 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/UpdateTestsUsingSP.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/UpdateTestsUsingSP.cs @@ -41,13 +41,13 @@ protected override void OnTestModelCreating(ModelBuilder modelBuilder) insertEntityConf.Property(x => x.Bar).HasColumnName("BAR"); insertEntityConf.ToTable("TEST_UPDATE_USP"); modelBuilder.Entity().UpdateUsingStoredProcedure("SP_TEST_UPDATE", - storedProcedureBuilder => - { - storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); - storedProcedureBuilder.HasParameter(x => x.Foo); - storedProcedureBuilder.HasParameter(x => x.Bar); - storedProcedureBuilder.HasRowsAffectedResultColumn(); - }); + storedProcedureBuilder => + { + storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); + storedProcedureBuilder.HasParameter(x => x.Foo); + storedProcedureBuilder.HasParameter(x => x.Bar); + storedProcedureBuilder.HasRowsAffectedResultColumn(); + }); } } class UpdateEntity @@ -88,7 +88,6 @@ pbar varchar(20)) .FirstAsync(); Assert.AreEqual("test", value.Foo); Assert.AreEqual("test", value.Bar); - //Assert.AreNotEqual("test", value.Bar); //this has to fail } } @@ -110,13 +109,13 @@ protected override void OnTestModelCreating(ModelBuilder modelBuilder) .ValueGeneratedOnAddOrUpdate(); insertEntityConf.ToTable("TEST_UPDATE_COMPUTED_USP"); modelBuilder.Entity().UpdateUsingStoredProcedure("SP_TEST_UPDATE_COMPUTED", - storedProcedureBuilder => - { - storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); - storedProcedureBuilder.HasParameter(x => x.Foo); - storedProcedureBuilder.HasParameter(x => x.Bar); - storedProcedureBuilder.HasResultColumn(x => x.Computed); - }); + storedProcedureBuilder => + { + storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); + storedProcedureBuilder.HasParameter(x => x.Foo); + storedProcedureBuilder.HasParameter(x => x.Bar); + storedProcedureBuilder.HasResultColumn(x => x.Computed); + }); } } class ComputedUpdateEntity @@ -144,7 +143,8 @@ pbar varchar(20)) update test_update_computed_usp set foo = :pfoo, bar = :pbar where id = :pid returning computed into :rcomputed; - if (row_count > 0) then suspend; + if (row_count > 0) then + suspend; end """; await db.Database.ExecuteSqlRawAsync(sp); @@ -173,13 +173,13 @@ protected override void OnTestModelCreating(ModelBuilder modelBuilder) .IsRowVersion(); insertEntityConf.ToTable("TEST_UPDATE_CONCURRENCY_USP"); modelBuilder.Entity().UpdateUsingStoredProcedure("SP_TEST_UPDATE_CONCURRENCY", - storedProcedureBuilder => - { - storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); - storedProcedureBuilder.HasParameter(x => x.Foo); - storedProcedureBuilder.HasOriginalValueParameter(x => x.Stamp); - storedProcedureBuilder.HasResultColumn(x => x.Stamp); - }); + storedProcedureBuilder => + { + storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); + storedProcedureBuilder.HasParameter(x => x.Foo); + storedProcedureBuilder.HasOriginalValueParameter(x => x.Stamp); + storedProcedureBuilder.HasResultColumn(x => x.Stamp); + }); } } class ConcurrencyUpdateEntity @@ -206,7 +206,8 @@ pfoo varchar(20), update test_update_concurrency_usp set foo = :pfoo, stamp = current_timestamp where id = :pid and stamp = :pstamp returning stamp into :rstamp; - if (row_count > 0) then suspend; + if (row_count > 0) then + suspend; end """; await db.Database.ExecuteSqlRawAsync(sp); @@ -234,13 +235,13 @@ protected override void OnTestModelCreating(ModelBuilder modelBuilder) .IsConcurrencyToken(); insertEntityConf.ToTable("TEST_UPDATE_CONCURRENCY_NG_USP"); modelBuilder.Entity().UpdateUsingStoredProcedure("SP_TEST_UPDATE_CONCURRENCY_NG", - storedProcedureBuilder => - { - storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); - storedProcedureBuilder.HasParameter(x => x.Foo); - storedProcedureBuilder.HasOriginalValueParameter(x => x.Stamp); - storedProcedureBuilder.HasRowsAffectedResultColumn(); - }); + storedProcedureBuilder => + { + storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); + storedProcedureBuilder.HasParameter(x => x.Foo); + storedProcedureBuilder.HasOriginalValueParameter(x => x.Stamp); + storedProcedureBuilder.HasRowsAffectedResultColumn(); + }); } } class ConcurrencyUpdateNoGeneratedEntity @@ -267,7 +268,8 @@ pfoo varchar(20), update test_update_concurrency_ng_usp set foo = :pfoo, stamp = current_timestamp where id = :pid and stamp = :pstamp; rowcount = row_count; - if (rowcount > 0) then suspend; + if (rowcount > 0) then + suspend; end """; await db.Database.ExecuteSqlRawAsync(sp); @@ -299,14 +301,14 @@ protected override void OnTestModelCreating(ModelBuilder modelBuilder) .ValueGeneratedOnAddOrUpdate(); insertEntityConf.ToTable("TEST_UPDATE_2COMPUTED_USP"); modelBuilder.Entity().UpdateUsingStoredProcedure("SP_TEST_UPDATE_2COMPUTED", - storedProcedureBuilder => - { - storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); - storedProcedureBuilder.HasParameter(x => x.Foo); - storedProcedureBuilder.HasParameter(x => x.Bar); - storedProcedureBuilder.HasResultColumn(x => x.Computed1); - storedProcedureBuilder.HasResultColumn(x => x.Computed2); - }); + storedProcedureBuilder => + { + storedProcedureBuilder.HasOriginalValueParameter(x => x.Id); + storedProcedureBuilder.HasParameter(x => x.Foo); + storedProcedureBuilder.HasParameter(x => x.Bar); + storedProcedureBuilder.HasResultColumn(x => x.Computed1); + storedProcedureBuilder.HasResultColumn(x => x.Computed2); + }); } } class TwoComputedUpdateEntity @@ -337,7 +339,8 @@ rcomputed2 varchar(50)) update test_update_2computed_usp set foo = :pfoo, bar = :pbar where id = :pid returning computed1, computed2 into :rcomputed1, :rcomputed2; - if (row_count > 0) then suspend; + if (row_count > 0) then + suspend; end """; await db.Database.ExecuteSqlRawAsync(sp); From 53fccb6719267c52aa37b88656299e2f18a33a1a Mon Sep 17 00:00:00 2001 From: Aivars Date: Sun, 22 Sep 2024 16:42:10 +0300 Subject: [PATCH 4/6] added NUMERIC type to typa mapping source --- .../Storage/Internal/FbTypeMappingSource.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbTypeMappingSource.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbTypeMappingSource.cs index 9e41a9ef..d1a2d70b 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbTypeMappingSource.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbTypeMappingSource.cs @@ -47,6 +47,7 @@ public class FbTypeMappingSource : RelationalTypeMappingSource readonly FloatTypeMapping _float = new FloatTypeMapping("FLOAT"); readonly DoubleTypeMapping _double = new DoubleTypeMapping("DOUBLE PRECISION"); readonly DecimalTypeMapping _decimal = new DecimalTypeMapping($"DECIMAL({DefaultDecimalPrecision},{DefaultDecimalScale})"); + readonly DecimalTypeMapping _numeric = new DecimalTypeMapping($"NUMERIC({DefaultDecimalPrecision},{DefaultDecimalScale})"); readonly FbDateTimeTypeMapping _timestamp = new FbDateTimeTypeMapping("TIMESTAMP", FbDbType.TimeStamp); readonly FbDateTimeTypeMapping _date = new FbDateTimeTypeMapping("DATE", FbDbType.Date); @@ -77,6 +78,7 @@ public FbTypeMappingSource(TypeMappingSourceDependencies dependencies, Relationa { "FLOAT", _float }, { "DOUBLE PRECISION", _double }, { "DECIMAL", _decimal }, + { "NUMERIC", _numeric }, { "TIMESTAMP", _timestamp }, { "DATE", _date }, { "TIME", _timeSpan }, From c921270048001c9e5ddc78ed2260d574c509b023 Mon Sep 17 00:00:00 2001 From: Aivars Date: Wed, 25 Sep 2024 13:15:10 +0300 Subject: [PATCH 5/6] Revert "added NUMERIC type to typa mapping source" This reverts commit 53fccb6719267c52aa37b88656299e2f18a33a1a. --- .../Storage/Internal/FbTypeMappingSource.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbTypeMappingSource.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbTypeMappingSource.cs index d1a2d70b..9e41a9ef 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbTypeMappingSource.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbTypeMappingSource.cs @@ -47,7 +47,6 @@ public class FbTypeMappingSource : RelationalTypeMappingSource readonly FloatTypeMapping _float = new FloatTypeMapping("FLOAT"); readonly DoubleTypeMapping _double = new DoubleTypeMapping("DOUBLE PRECISION"); readonly DecimalTypeMapping _decimal = new DecimalTypeMapping($"DECIMAL({DefaultDecimalPrecision},{DefaultDecimalScale})"); - readonly DecimalTypeMapping _numeric = new DecimalTypeMapping($"NUMERIC({DefaultDecimalPrecision},{DefaultDecimalScale})"); readonly FbDateTimeTypeMapping _timestamp = new FbDateTimeTypeMapping("TIMESTAMP", FbDbType.TimeStamp); readonly FbDateTimeTypeMapping _date = new FbDateTimeTypeMapping("DATE", FbDbType.Date); @@ -78,7 +77,6 @@ public FbTypeMappingSource(TypeMappingSourceDependencies dependencies, Relationa { "FLOAT", _float }, { "DOUBLE PRECISION", _double }, { "DECIMAL", _decimal }, - { "NUMERIC", _numeric }, { "TIMESTAMP", _timestamp }, { "DATE", _date }, { "TIME", _timeSpan }, From 1b5afa3628da3d2952717107d684419419e29852 Mon Sep 17 00:00:00 2001 From: Aivars Date: Wed, 25 Sep 2024 18:42:23 +0300 Subject: [PATCH 6/6] updated AppendStoredProcedureCall and tests --- .../EndToEndUsingSP/DeleteTestsUsingSP.cs | 10 ++- .../EndToEndUsingSP/InsertTestsUsingSP.cs | 90 +++++++++++++++---- .../EndToEndUsingSP/UpdateTestsUsingSP.cs | 32 ++++--- .../Update/Internal/FbUpdateSqlGenerator.cs | 36 +++++++- 4 files changed, 131 insertions(+), 37 deletions(-) diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/DeleteTestsUsingSP.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/DeleteTestsUsingSP.cs index a1b45697..27f98b07 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/DeleteTestsUsingSP.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/DeleteTestsUsingSP.cs @@ -62,12 +62,13 @@ public async Task Delete() await db.Database.ExecuteSqlRawAsync("insert into test_delete_usp values (66, 'test')"); await db.Database.ExecuteSqlRawAsync("insert into test_delete_usp values (67, 'test')"); var sp = """ - create or alter procedure sp_test_delete ( + create procedure sp_test_delete ( pid integer) returns (rowcount integer) as begin - delete from test_delete_usp where id = :pid; + delete from test_delete_usp + where id = :pid; rowcount = row_count; suspend; end @@ -129,13 +130,14 @@ public async Task ConcurrencyDelete() await db.Database.ExecuteSqlRawAsync("insert into test_delete_concurrency_usp values (66, 'test', current_timestamp)"); await db.Database.ExecuteSqlRawAsync("insert into test_delete_concurrency_usp values (67, 'test', current_timestamp)"); var sp = """ - create or alter procedure sp_test_delete_concurrency ( + create procedure sp_test_delete_concurrency ( pid integer, pstamp timestamp) returns (rowcount integer) as begin - delete from test_delete_concurrency_usp where id = :pid and stamp = :pstamp; + delete from test_delete_concurrency_usp + where id = :pid and stamp = :pstamp; rowcount = row_count; if (rowcount > 0) then suspend; diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/InsertTestsUsingSP.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/InsertTestsUsingSP.cs index 29654372..7817d659 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/InsertTestsUsingSP.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/InsertTestsUsingSP.cs @@ -59,14 +59,15 @@ public async Task Insert() { await db.Database.ExecuteSqlRawAsync("create table test_insert_usp (id int primary key, name varchar(20))"); var sp = """ - create or alter procedure sp_test_insert ( + create procedure sp_test_insert ( pid integer, pname varchar(20)) - returns (rid integer) + returns (id integer) as begin - insert into test_insert_usp (id, name) values (:pid, :pname) - returning id into :rid; + insert into test_insert_usp (id, name) + values (:pid, :pname) + returning id into :id; suspend; end """; @@ -78,6 +79,53 @@ insert into test_insert_usp (id, name) values (:pid, :pname) } } + class InsertContextWithoutReturns : FbTestDbContext + { + public InsertContextWithoutReturns(string connectionString) + : base(connectionString) + { } + + protected override void OnTestModelCreating(ModelBuilder modelBuilder) + { + base.OnTestModelCreating(modelBuilder); + + var insertEntityConf = modelBuilder.Entity(); + insertEntityConf.Property(x => x.Id).HasColumnName("ID").ValueGeneratedNever(); + insertEntityConf.Property(x => x.Name).HasColumnName("NAME"); + insertEntityConf.ToTable("TEST_INSERT_NORETURNS_USP"); + modelBuilder.Entity().InsertUsingStoredProcedure("SP_TEST_INSERT_NORETURNS", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(x => x.Id); + storedProcedureBuilder.HasParameter(x => x.Name); + }); + } + } + + [Test] + public async Task InsertWithoutReturns() + { + await using (var db = await GetDbContext()) + { + await db.Database.ExecuteSqlRawAsync("create table test_insert_noreturns_usp (id int primary key, name varchar(20))"); + var sp = """ + create procedure sp_test_insert_noreturns ( + pid integer, + pname varchar(20)) + as + begin + insert into test_insert_noreturns_usp (id, name) + values (:pid, :pname); + end + """; + await db.Database.ExecuteSqlRawAsync(sp); + var entity = new InsertEntity() { Id = -6, Name = "foobar" }; + await db.AddAsync(entity); + await db.SaveChangesAsync(); + Assert.AreEqual(-6, entity.Id); + } + } + class IdentityInsertContext : FbTestDbContext { public IdentityInsertContext(string connectionString) @@ -118,13 +166,14 @@ public async Task IdentityInsert() { await db.Database.ExecuteSqlRawAsync("create table test_insert_identity_usp (id int generated by default as identity (start with 26) primary key, name varchar(20))"); var sp = """ - create or alter procedure sp_test_insert_identity ( + create procedure sp_test_insert_identity ( pname varchar(20)) - returns (rid integer) + returns (id integer) as begin - insert into test_insert_identity_usp (name) values (:pname) - returning id into :rid; + insert into test_insert_identity_usp (name) + values (:pname) + returning id into :id; suspend; end """; @@ -176,13 +225,14 @@ public async Task SequenceInsert() await db.Database.ExecuteSqlRawAsync("alter sequence seq_test_insert_sequence_usp restart with 30"); await db.Database.ExecuteSqlRawAsync("create trigger test_insert_sequence_id_usp before insert on test_insert_sequence_usp as begin if (new.id is null) then begin new.id = next value for seq_test_insert_sequence_usp; end end"); var sp = """ - create or alter procedure sp_test_insert_sequence ( + create procedure sp_test_insert_sequence ( pname varchar(20)) - returns (rid integer) + returns (id integer) as begin - insert into test_insert_sequence_usp (name) values (:pname) - returning id into :rid; + insert into test_insert_sequence_usp (name) + values (:pname) + returning id into :id; suspend; end """; @@ -235,12 +285,13 @@ public async Task DefaultValuesInsert() { await db.Database.ExecuteSqlRawAsync("create table test_insert_devaultvalues_usp (id int generated by default as identity (start with 26) primary key, name generated always as (id || 'foobar'))"); var sp = """ - create or alter procedure sp_test_insert_defaultvalues - returns (rid integer, rname varchar(20)) + create procedure sp_test_insert_defaultvalues + returns (id integer, name varchar(20)) as begin insert into test_insert_devaultvalues_usp default values - returning id, name into :rid, :rname; + returning id, name + into :id, :name; suspend; end """; @@ -299,14 +350,15 @@ public async Task TwoComputedInsert() { await db.Database.ExecuteSqlRawAsync("create table test_insert_2computed_usp (id int generated by default as identity (start with 26) primary key, name varchar(20), computed1 generated always as ('1' || name), computed2 generated always as ('2' || name))"); var sp = """ - create or alter procedure sp_test_insert_2computed ( + create procedure sp_test_insert_2computed ( pname varchar(20)) - returns (rid integer, rcomputed1 varchar(25), rcomputed2 varchar(25)) + returns (id integer, computed1 varchar(25), computed2 varchar(25)) as begin - insert into test_insert_2computed_usp (name) values (:pname) + insert into test_insert_2computed_usp (name) + values (:pname) returning id, computed1, computed2 - into :rid, :rcomputed1, :rcomputed2; + into :id, :computed1, :computed2; suspend; end """; diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/UpdateTestsUsingSP.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/UpdateTestsUsingSP.cs index 660d67bb..2abd853d 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/UpdateTestsUsingSP.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/UpdateTestsUsingSP.cs @@ -71,7 +71,8 @@ pbar varchar(20)) returns (rowcount integer) as begin - update test_update_usp set foo = :pfoo, bar = :pbar + update test_update_usp + set foo = :pfoo, bar = :pbar where id = :pid; rowcount = row_count; suspend; @@ -137,12 +138,14 @@ create procedure sp_test_update_computed ( pid integer, pfoo varchar(20), pbar varchar(20)) - returns (rcomputed varchar(50)) + returns (computed varchar(50)) as begin - update test_update_computed_usp set foo = :pfoo, bar = :pbar + update test_update_computed_usp + set foo = :pfoo, bar = :pbar where id = :pid - returning computed into :rcomputed; + returning computed + into :computed; if (row_count > 0) then suspend; end @@ -200,12 +203,14 @@ create procedure sp_test_update_concurrency ( pid integer, pfoo varchar(20), pstamp timestamp) - returns (rstamp timestamp) + returns (stamp timestamp) as begin - update test_update_concurrency_usp set foo = :pfoo, stamp = current_timestamp + update test_update_concurrency_usp + set foo = :pfoo, stamp = current_timestamp where id = :pid and stamp = :pstamp - returning stamp into :rstamp; + returning stamp + into :stamp; if (row_count > 0) then suspend; end @@ -265,7 +270,8 @@ pfoo varchar(20), returns (rowcount integer) as begin - update test_update_concurrency_ng_usp set foo = :pfoo, stamp = current_timestamp + update test_update_concurrency_ng_usp + set foo = :pfoo, stamp = current_timestamp where id = :pid and stamp = :pstamp; rowcount = row_count; if (rowcount > 0) then @@ -332,13 +338,15 @@ create procedure sp_test_update_2computed ( pfoo varchar(20), pbar varchar(20)) returns ( - rcomputed1 varchar(50), - rcomputed2 varchar(50)) + computed1 varchar(50), + computed2 varchar(50)) as begin - update test_update_2computed_usp set foo = :pfoo, bar = :pbar + update test_update_2computed_usp + set foo = :pfoo, bar = :pbar where id = :pid - returning computed1, computed2 into :rcomputed1, :rcomputed2; + returning computed1, computed2 + into :computed1, :computed2; if (row_count > 0) then suspend; end diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Update/Internal/FbUpdateSqlGenerator.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Update/Internal/FbUpdateSqlGenerator.cs index 2fa97667..edcc5046 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Update/Internal/FbUpdateSqlGenerator.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Update/Internal/FbUpdateSqlGenerator.cs @@ -277,7 +277,39 @@ public override ResultSetMapping AppendStoredProcedureCall( } } - commandStringBuilder.Append("SELECT * FROM "); + if (resultSetMapping == ResultSetMapping.NoResults) + { + commandStringBuilder.Append("EXECUTE PROCEDURE "); + } + else + { + commandStringBuilder.Append("SELECT "); + + var first = true; + + foreach (var resultColumn in storedProcedure.ResultColumns) + { + if (first) + { + first = false; + } + else + { + commandStringBuilder.Append(", "); + } + + if (resultColumn == command.RowsAffectedColumn || resultColumn.Name == "RowsAffected") + { + SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, "ROWCOUNT"); + } + else + { + SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, resultColumn.Name); + } + } + commandStringBuilder.Append(" FROM "); + } + SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, storedProcedure.Name); if (storedProcedure.Parameters.Any()) @@ -296,7 +328,7 @@ public override ResultSetMapping AppendStoredProcedureCall( if (parameter.Direction.HasFlag(ParameterDirection.Output)) { - continue; + throw new InvalidOperationException("Output parameters are not supported in stored procedures"); } if (first)