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..27f98b07 --- /dev/null +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/DeleteTestsUsingSP.cs @@ -0,0 +1,153 @@ +/* + * 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 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 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..7817d659 --- /dev/null +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/InsertTestsUsingSP.cs @@ -0,0 +1,373 @@ +/* + * 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 procedure sp_test_insert ( + pid integer, + pname varchar(20)) + returns (id integer) + as + begin + insert into test_insert_usp (id, name) + values (:pid, :pname) + returning id into :id; + 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 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) + : 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 procedure sp_test_insert_identity ( + pname varchar(20)) + returns (id integer) + as + begin + insert into test_insert_identity_usp (name) + values (:pname) + returning id into :id; + 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 procedure sp_test_insert_sequence ( + pname varchar(20)) + returns (id integer) + as + begin + insert into test_insert_sequence_usp (name) + values (:pname) + returning id into :id; + 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 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 :id, :name; + 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 procedure sp_test_insert_2computed ( + pname varchar(20)) + returns (id integer, computed1 varchar(25), computed2 varchar(25)) + as + begin + insert into test_insert_2computed_usp (name) + values (:pname) + returning id, computed1, computed2 + into :id, :computed1, :computed2; + 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..2abd853d --- /dev/null +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/EndToEndUsingSP/UpdateTestsUsingSP.cs @@ -0,0 +1,363 @@ +/* + * 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); + } + } + + 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 (computed varchar(50)) + as + begin + update test_update_computed_usp + set foo = :pfoo, bar = :pbar + where id = :pid + returning computed + into :computed; + 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 (stamp timestamp) + as + begin + update test_update_concurrency_usp + set foo = :pfoo, stamp = current_timestamp + where id = :pid and stamp = :pstamp + returning stamp + into :stamp; + 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 ( + computed1 varchar(50), + computed2 varchar(50)) + as + begin + update test_update_2computed_usp + set foo = :pfoo, bar = :pbar + where id = :pid + returning computed1, computed2 + into :computed1, :computed2; + 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 d3b36d05..edcc5046 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,103 @@ 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; + } + } + + 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()) + { + 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)) + { + throw new InvalidOperationException("Output parameters are not supported in stored procedures"); + } + + if (first) + { + first = false; + } + else + { + commandStringBuilder.Append(", "); + } + SqlGenerationHelper.GenerateParameterNamePlaceholder( + commandStringBuilder, columnModification.UseOriginalValueParameter + ? columnModification.OriginalParameterName! + : columnModification.ParameterName!); + } + + commandStringBuilder.Append(")"); + } + + commandStringBuilder.AppendLine(SqlGenerationHelper.StatementTerminator); + requiresTransaction = true; + return resultSetMapping; + } }