Skip to content

Added AppendStoredProcedureCall #1185

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 ([email protected])

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<DeleteEntity>();
insertEntityConf.Property(x => x.Id).HasColumnName("ID");
insertEntityConf.Property(x => x.Name).HasColumnName("NAME");
insertEntityConf.ToTable("TEST_DELETE_USP");
modelBuilder.Entity<DeleteEntity>().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<DeleteContext>())
{
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<DeleteEntity>()
.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<ConcurrencyDeleteEntity>();
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<ConcurrencyDeleteEntity>().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<ConcurrencyDeleteContext>())
{
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<DbUpdateConcurrencyException>(() => db.SaveChangesAsync());
}
}
}
Original file line number Diff line number Diff line change
@@ -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<InsertEntity>();
insertEntityConf.Property(x => x.Id).HasColumnName("ID");
insertEntityConf.Property(x => x.Name).HasColumnName("NAME");
insertEntityConf.ToTable("TEST_INSERT_USP");
modelBuilder.Entity<InsertEntity>().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<InsertContext>())
{
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<InsertEntity>();
insertEntityConf.Property(x => x.Id).HasColumnName("ID").ValueGeneratedNever();
insertEntityConf.Property(x => x.Name).HasColumnName("NAME");
insertEntityConf.ToTable("TEST_INSERT_NORETURNS_USP");
modelBuilder.Entity<InsertEntity>().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<InsertContextWithoutReturns>())
{
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<IdentityInsertEntity>();
insertEntityConf.Property(x => x.Id).HasColumnName("ID")
.UseIdentityColumn();
insertEntityConf.Property(x => x.Name).HasColumnName("NAME");
insertEntityConf.ToTable("TEST_INSERT_IDENTITY_USP");
modelBuilder.Entity<IdentityInsertEntity>().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<IdentityInsertContext>())
{
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<SequenceInsertEntity>();
insertEntityConf.Property(x => x.Id).HasColumnName("ID")
.UseSequenceTrigger();
insertEntityConf.Property(x => x.Name).HasColumnName("NAME");
insertEntityConf.ToTable("TEST_INSERT_SEQUENCE_USP");
modelBuilder.Entity<SequenceInsertEntity>().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<SequenceInsertContext>())
{
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<DefaultValuesInsertEntity>();
insertEntityConf.Property(x => x.Id).HasColumnName("ID")
.ValueGeneratedOnAdd();
insertEntityConf.Property(x => x.Name).HasColumnName("NAME")
.ValueGeneratedOnAdd();
insertEntityConf.ToTable("TEST_INSERT_DEVAULTVALUES_USP");
modelBuilder.Entity<DefaultValuesInsertEntity>().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<DefaultValuesInsertContext>())
{
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<TwoComputedInsertEntity>();
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<TwoComputedInsertEntity>().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<TwoComputedInsertContext>())
{
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<UpdateEntity>();
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<UpdateEntity>().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<UpdateContext>())
{
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<UpdateEntity>()
.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<ComputedUpdateEntity>();
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<ComputedUpdateEntity>().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<ComputedUpdateContext>())
{
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<ConcurrencyUpdateEntity>();
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<ConcurrencyUpdateEntity>().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<ConcurrencyUpdateContext>())
{
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<DbUpdateConcurrencyException>(() => db.SaveChangesAsync());
}
}

class ConcurrencyUpdateNoGeneratedContext : FbTestDbContext
{
public ConcurrencyUpdateNoGeneratedContext(string connectionString)
: base(connectionString)
{ }

protected override void OnTestModelCreating(ModelBuilder modelBuilder)
{
base.OnTestModelCreating(modelBuilder);

var insertEntityConf = modelBuilder.Entity<ConcurrencyUpdateNoGeneratedEntity>();
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<ConcurrencyUpdateNoGeneratedEntity>().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<ConcurrencyUpdateNoGeneratedContext>())
{
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<DbUpdateConcurrencyException>(() => db.SaveChangesAsync());
}
}

class TwoComputedUpdateContext : FbTestDbContext
{
public TwoComputedUpdateContext(string connectionString)
: base(connectionString)
{ }

protected override void OnTestModelCreating(ModelBuilder modelBuilder)
{
base.OnTestModelCreating(modelBuilder);

var insertEntityConf = modelBuilder.Entity<TwoComputedUpdateEntity>();
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<TwoComputedUpdateEntity>().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<TwoComputedUpdateContext>())
{
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}