How to exclusively lock Oracle DB Table using C#? - c#

Actually I want to apply lock on a table so that no other process can perform DML(Insert/Update/Delete but can perform Select) or lock that table while 'process execution'.
LOCK TABLE table-name IN EXCLUSIVE MODE
how should i write that in C#?
// ..... process execution .......
How to Release lock? I guess by either commiting or roll back.
Any suggestions?

Lock will be relased on commit or rollback
OracleConnection conn= new OracleConnection("Data Source=datasrc;User=USER;Password=passwd");
conn.Open();
OracleTransaction tr = conn.BeginTransaction();
OracleCommand cmd = new OracleCommand("LOCK TABLE TABLE_NAME IN EXCLUSIVE MODE",conn,tr);
cmd.ExecuteNonQuery();
Console.ReadLine();
tr.Commit();
conn.Close();

Why do you need to lock a table?
May be it's enough to specify isolation level using transaction scope object?
For example
TransactionOptions TransOpt = New TransactionOptions();
TransOpt.IsolationLevel = System.Transactions.IsolationLevel.Serializable;
using(TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, TransOptions))
{
...
}
Please, check your requirements.

There's work around for this issue by exploiting another strategy. Neither solving it through:
EXCLUSIVE LOCKING (Because I realized if there's another transaction updated my table while my process was underway, that could not update table due to the Lock but as soon as my process completes,means Lock now released, and if another transaction is still on hold then that another transaction will immediately update my table. And that would not bear fruits for me.)Nor solving it through:
IsolationLevel.Serializable (As the serializable transaction isolation level is not supported with distributed transactions.)
Therefore, on every entry in my table I identify if there is any transaction(whether distributed or local) blocking my table. If there is one, i identify that session and forcefully kill that session. Which perfectly suits my scenario:
Database db = DataRepository.GetDatabase();
int result, session_id = 0;
string kill_session, serial = null;
string chk_lock = "SELECT l.session_id,v.serial# ,"
+"object_name FROM dba_objects o, gv$locked_object l, "
+"v$session v WHERE o.object_id = l.object_id and "
+"l.SESSION_ID=v.sid";
DbDataReader rdr_blkAccount;
try{
//MY PROCESS RUNS HERE...
}
catch(Exception excep)
{
//...
}
finally
{
rdr_blkAccount = db.ExecuteReader(chk_lock);
while (rdr_blkAccount.Read())
{
if (rdr_blkAccount[2].ToString().ToUpper() == "ACCOUNT")
{
session_id = Convert.ToInt32(rdr_blkAccount[0]);
serial = session_id.ToString() + ','
+ Convert.ToInt32(rdr_blkAccount[1]).ToString();
kill_session = "alter system kill session '" + serial + "'";
result = db.ExecuteNonQuery(kill_session);
logger.Log( LogLevel.Warning
, string.Format("Session_id '{0}' has been forcefully killed"
, serial));
}
}
rdr_blkAccount.Close();
}

Related

Cannot see changes in database without running manually the commit command

I'm trying to save the data I'm inserting into a local Firebird database.
I've tried running an sql command, in the C# code, containg commit; after inserting the data but it doesn't seem to work. The informations are sent but the database isn't saving them.
This is the code I'm using for inserting the data.
FbConnectionStringBuilder csb = new FbConnectionStringBuilder
{
DataSource = "localhost",
Port = 3050,
Database = #"D:\db\DBUTENTI.FDB",
UserID = "SYSDBA",
Password = "masterkey",
ServerType = FbServerType.Default
};
using (FbConnection myConn = new FbConnection(csb.ToString()))
{
if (myConn.State == ConnectionState.Closed)
{
try
{
myConn.Open();
Console.WriteLine("CONNECTION OPENED");
string Id = txt_Id.Text;
string Utente = txt_User.Text;
string Password = txt_Password.Text;
FbCommand cmd = new FbCommand("insert into utenti(id,utente,password)values(#id, #utente, #password)", myConn);
cmd.Parameters.AddWithValue("id", Id);
cmd.Parameters.AddWithValue("utente", Utente);
cmd.Parameters.AddWithValue("password", Password);
cmd.ExecuteNonQuery();
myConn.Close();
Console.WriteLine("CONNECTION CLOSED");
}
catch (Exception exc)
{
Console.WriteLine(exc.Message);
}
}
}
The code runs without any errors/exceptions, but I have to manually commit in the ISQL Tool to see the changes.
Thanks to anyone who is willing to help.
If your workaround (solution) is to manually commit in ISQL, the problem is that you had an active transaction in ISQL (and one is started as soon as you start ISQL). This transaction cannot see changes from transactions committed after the transaction in ISQL started (ie: the changes in your program).
ISQL by default starts transactions with the SNAPSHOT isolation level (which is somewhat equivalent to the SQL standard REPEATABLE READ). If you want ISQL to be able to see changes made by your program, you either need to relax its isolation level to READ COMMITTED, or - as you already found out - you need to explicitly commit (so a new transaction is used).
For example to switch ISQL to use READ COMMITTED, you can use statement:
set transaction read committed record_version;
This will only change the transaction setting for the current session.
For details, see
Firebird 2.5 Language Reference, Transaction Statements
ISQL, Transaction Handling

SQLServer. Unable to make ALLOW_SNAPSHOT_ISOLATION work in c# code (it works on Management Studio)

Recently I had to resolve lock problems such as Transaction was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
After reading several articles and analyzing on the context of my system I ended up accepting the most usual solution:
ALTER DATABASE MyDb SET READ_COMMITTED_SNAPSHOT ON;
ALTER DATABASE MyDb SET ALLOW_SNAPSHOT_ISOLATION ON;
I want the ALLOW_SNAPSHOT_ISOLATION because that isolation makes sense on my system.
I successfully implemented the flow described in the section "Allow Snapshot Isolation" from https://www.databasejournal.com/features/mssql/article.php/3566746/Controlling-Transactions-and-Locks-Part-5-SQL-2005-Snapshots.htm on two sessions in SQL Server Management Studio.
Pseudo code:
SET TRANSACTION ISOLATION LEVEL SNAPSHOT
BEGIN TRAN 1
select value from MyTable where ID=1 --reads original value
--On another session:
BEGIN TRAN 2
update mytable set value=2 where ID=1
COMMIT TRAN2
-- back to session 1
select value from MyTable where ID=1 --still reads original value
The above example works as expected. This tells me that the database is configured correctly.
My problem is on the C# code. While I was able to prevent the lock situations (READ_COMMITTED_SNAPSHOT is working) I was unable to replicate the "Allow Snapshot Isolation" behavior on my c# code. I tried with TransactionScope and without it. My goal is to have it working with TransactionScope.
My test on C# is to start a long transaction: Read value from my table, wait for 20 seconds, read the value again and print both values. When the code is sleeping for the 20 seconds I go to SQL Server Management Studio and update the value to the new value. After 20 seconds it is shown the original value and the new value. I was expecting both original values because of ALLOW_SNAPSHOT_ISOLATION and SET TRANSACTION ISOLATION LEVEL SNAPSHOT
With transaction scope (I am using Dapper):
static TransactionScope CreateTransactionScope()
{
var transactionOptions = new TransactionOptions();
transactionOptions.Timeout = TransactionManager.MaximumTimeout;
transactionOptions.IsolationLevel = IsolationLevel.Snapshot; //also tried IsolationLevel.ReadCommitted
return new TransactionScope(TransactionScopeOption.RequiresNew, transactionOptions);
}
...
using (var transactionScope = CreateTransactionScope())
{
T ret;
using (var connection = new SqlConnection(_connectionString))
{
//connection.Execute("SET TRANSACTION ISOLATION LEVEL SNAPSHOT"); this makes no difference
ret = TestWithTransactionScope(connection);
}
transactionScope.Complete();
return ret;
}
...
public object TestWithTransactionScope(IDbConnection c)
{
var sql = "select value from MyTable where ID=1";
var firstRead = c.Query<string>(sql).Single();
System.Threading.Thread.Sleep(25000);
var secondRead = c.Query<string>(sql).Single();
return string.Format("firstRead: {0}, secondRead: {1}", firstRead, secondRead);
}
Without TransactionScope:
...
using (var connection = new SqlConnection("..."))
{
connection.Open();
connection.Execute("SET TRANSACTION ISOLATION LEVEL SNAPSHOT");
using (var transaction = connection.BeginTransaction())
{
try
{
var ret = TestWithTransactionScope(connection, transaction);
transaction.Commit();
return ret;
}
catch
{
transaction.Rollback();
throw;
}
}
}
...
public object TestWithTransactionScope(IDbConnection c, SqlTransaction t)
{
var sql = "select value from MyTable where ID=1";
var firstRead = c.Query<string>(q, null, t).Single();
System.Threading.Thread.Sleep(25000);
var secondRead = c.Query<string>(q, null, t).Single();
return string.Format("firstRead: {0}, secondRead: {1}", firstRead, secondRead);
}
Any ideas?
I am using .Net 4.5, Dapper 1.50.2 and SQL Server 2014
UPDATE 1
I was able to use the Snapshot isolation on the non TransactionScope version:
using (var transaction = connection.BeginTransaction(IsolationLevel.Snapshot))
But I still need it to work on the TransactionScope version.

Getting timeout errors with SqlTransaction on same table

public TransImport()
{
ConnString = ConfigurationManager.ConnectionStrings["Connection"].ConnectionString;
SqlConnection conn_new;
SqlCommand command_serial_new;
SqlConnection conn;
SqlCommand command_serial;
SqlTransaction InsertUpdateSerialNumbers;
conn = new SqlConnection(ConnString);
command_serial = conn.CreateCommand();
conn_new = new SqlConnection(ConnString);
command_serial_new = conn_new.CreateCommand();
command_serial_new.CommandText = "SELECT 1 FROM YSL00 WHERE SERLNMBR = #slnr";
var p = new SqlParameter("#slnr", SqlDbType.NVarChar, 50);
command_serial_new.Parameters.Add(p);
//Here you will start reading flat file to get serialnumber.
InsertUpdateSerialNumbers = conn.BeginTransaction();
while (!headerFileReader.EndOfStream)
{
headerRow = headerFileReader.ReadLine();
if (CheckSerialNumber(headerFields[0].Trim()))
DisplayMessage("Good serialnumber"); //this function is not copied here.
}
InsertUpdateSerialNumbers.Commit();
}
private Boolean CheckSerialNumber(string SerialNumber)
{
command_serial_new.Parameters["#slnr"].Value = SerialNumber;
try
{
var itExists = Convert.ToInt32(command_serial_new.ExecuteScalar()) > 0;
if (!itExists)
{
command_serial.Transaction = InsertUpdateSerialNumbers;
command_serial.CommandText = "INSERT INTO YSL00([Manifest_Number],[PONUMBER],[ITEMNMBR],[SERLNMBR]"
+ "VALUES ('" + Manifest + "','" + PONr + "','" + itemNumber + "','" + serialNr + "')";
var insertStatus = command_serial.ExecuteNonQuery();
return true;
}
}
catch (Exception ex)
{
LogException(ex, "Error in CheckSerialNumber =>"+ command_serial_new.CommandText.ToString());
}
return false;
}
I get error "Timeout expired. The timeout period elapsed prior to completion of the operation or server is not responding".
The CheckSerialNumber function also does an insert to YSL00 (the same table where I had executescalar. See code above).
As I mentioned earlier there are 1000s of line in a flat file that I read and update YSL000 table.
Note that I have two separate sqlcommands and also two separate connections to handle this. Reason is with sqltransaction it doesn't let me to query on the same table. I think timeout may be happening because of this?
Thanks for reading. Please suggest
Update 1: Since I have not pasted entire code, I want to mention that dispose is done using below code in the program.
if (conn != null)
{
conn.Close();
conn.Dispose();
}
if (conn_new != null)
{
conn_new.Close();
conn_new.Dispose();
}
you can increase the time out of your SqlConnection object.
you can do this with your ConnString:
string connStr = "Data Source=(local);Initial Catalog=AdventureWorks;Integrated
Security=SSPI;Connection Timeout=300";
I think default isolation level - read commited - is preventing your 'CheckSerialNumber' method from being effective. Command_serial_new will not take into consideration rows inserted in your loop - this might lead to some troubles. To be honest I would also look for some deadlock. Perhaps command_serial_new is actually completely blocked by the other transaction.
To start off:
Set command_serial_new query as:
SELECT 1 FROM YSL00 WITH (NOLOCK) WHERE SERLNMBR = #slnr
Think about using lower isolation level to query inserted rows as well (set it to read uncommited).
Close your connections and transactions.
Use just one SqlConnection - you don't need two of them.
Many of the objects you are using implement IDisposable, and you should be wrapping them with using statements. Without these using statements, .NET won't necessarily get rid of your objects until an undetermined time when the garbage collector runs, and could block subsequent queries if it's still holding a transaction open somewhere.
So for example, you'll need to wrap your connections with using statements:
using (conn_new = new SqlConnection(ConnString)) {
...
If I am not mistaken you need to merge the file content with the table content.
For this purpose I would recommend you
Copy the file content in to a temporary table (see temporary tables and BulkInsert)
Use command MERGE (http://msdn.microsoft.com/en-us/library/bb510625.aspx) to merge the temporary table content with the original table

Unable to restore SQL database, exclusive access could not be obtained (single user mode)

I am writing a simple database backup and restore routine for an application. I can backup my database without issues, however when I restore is I am unable to gain exclusive access to my database.
I am trying all the combinations of fixes on SO, putting in single user mode, taking it offline then placing it back only with no success.
I can successfully restore the database within studio manager (express)
This method is the only connection to the SQL server at the time, so I don't understand why I can't perform the restore.
Appreciate the help to point out where the issue may be.
internal void RestoreDatabase(string databaseFile)
{
//get database details
var databaseConfiguration = new DatabaseConfiguration().GetDatabaseConfiguration();
try
{
//construct server connection string
var connection = databaseConfiguration.IsSqlAuthentication
? new ServerConnection(databaseConfiguration.ServerInstance,
databaseConfiguration.SqlUsername,
databaseConfiguration.SqlPassword)
: new ServerConnection(databaseConfiguration.ServerInstance);
//set database to single user and kick everyone off
using (
var sqlconnection =
new SqlConnection(new DatabaseConfiguration().MakeConnectionString(databaseConfiguration)))
{
sqlconnection.Open();
using (
var sqlcommand = new SqlCommand("ALTER DATABASE " + databaseConfiguration.DatabaseName + " SET Single_User WITH Rollback IMMEDIATE",
sqlconnection))
{
sqlcommand.ExecuteNonQuery();
}
using (
var sqlcommand = new SqlCommand("ALTER DATABASE " + databaseConfiguration.DatabaseName + " SET OFFLINE",
sqlconnection))
{
sqlcommand.ExecuteNonQuery();
}
using (
var sqlcommand = new SqlCommand("ALTER DATABASE " + databaseConfiguration.DatabaseName + " SET ONLINE",
sqlconnection))
{
sqlcommand.ExecuteNonQuery();
}
sqlconnection.Close();
}
//setup server connection and restore
var server = new Server(connection);
var restore = new Restore();
restore.Database = databaseConfiguration.DatabaseName;
restore.Action = RestoreActionType.Database;
restore.Devices.AddDevice(databaseFile, DeviceType.File);
restore.ReplaceDatabase = true;
restore.Complete += Restore_Complete;
restore.SqlRestore(server);
}
catch (Exception ex)
{
//my bad
restoreDatabaseServerError(ex.InnerException.Message, EventArgs.Empty);
}
finally
{
//set database to multi user
using (
var sqlconnection =
new SqlConnection(new DatabaseConfiguration().MakeConnectionString(databaseConfiguration)))
{
sqlconnection.Open();
using (
var sqlcommand = new SqlCommand("ALTER DATABASE " + databaseConfiguration.DatabaseName + " SET Multi_User",
sqlconnection))
{
sqlcommand.ExecuteNonQuery();
sqlcommand.Dispose();
}
sqlconnection.Close();
}
}
}
If anybody is connected to your database, SQL Server cannot drop it, so you have to disconnect existing connections, as you have tried. The problem with single_user is, that it still allows a single user to connect. As you yourself cannot be connected to the database when dropping it you have to get out of there. That opens up that slot for someone else to connect and in turn prevent you from dropping it.
There are a few SQL Server processes that are particularly good at connecting to a database in that split second. Replication is one example. (You shouldn't really drop a database that is published anyway, bat that is another story.)
So what can we do about this? The only 100% safe way is to prevent users from connecting to the database. The only practical way is to switch the database offline and then drop it. However, that has the nasty side effect, that SQL Server does not delete the files of that database, so you have to do that manually.
Another option is to just be fast enough. In your example you bring the database back online before you drop it. That is a fairly resource intensive process that gives an "intruder" lots of time to connect.
The solution I have been using with success looks like this:
ALTER DATABASE MyDb SET RESTRICTED_USER WITH ROLLBACK IMMEDIATE;
USE MyDb;
ALTER DATABASE MyDb SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
USE tempdb;
DROP DATABASE MyDb;
This first sets the database to restricted user and connects to it. Then, while still connected it sets the database to single user. Afterwards the context is switched to tempdb and the drop is executed immediately thereafter. Important here is, to send these commands as one batch to SQL Server to minimize the time between the USE tempdb; and the DROP. Setting the database to restricted user in the beginning catches some rare edge cases, so leave it in even though it does not make sense at first glance.
While this still leaves a theoretical gap for someone else to get in, I have never seen it fail.
After the database is dropped you can run your restore as normal.
Good luck.
Your restore needs to take place on the same connection you set the DB server to single user mode.
In summary for the changes below, I moved the end of the using to below your restore code, and moved the close for the SQL connection to after the restore so it uses the same connection. Also removed set offline and online since they aren't needed. Can't test at the moment, so let me know if it works.
//set database to single user and kick everyone off
using (var sqlconnection = new SqlConnection(new DatabaseConfiguration().MakeConnectionString(databaseConfiguration)))
{
sqlconnection.Open();
using (var sqlcommand = new SqlCommand("ALTER DATABASE " + databaseConfiguration.DatabaseName + " SET Single_User WITH Rollback IMMEDIATE",sqlconnection))
{
sqlcommand.ExecuteNonQuery();
}
//setup server connection and restore
var server = new Server(sqlconnection);
var restore = new Restore();
restore.Database = databaseConfiguration.DatabaseName;
restore.Action = RestoreActionType.Database;
restore.Devices.AddDevice(databaseFile, DeviceType.File);
restore.ReplaceDatabase = true;
restore.Complete += Restore_Complete;
restore.SqlRestore(server);
sqlconnection.Close();
}

Rollback transaction to savepoint on failing ALTER TABLE ... ADD CONSTRAINT

Is there any way to add a check constraint in a transaction and in case of failure rolling back to a previous savepoint (instead of rolling back the entire transaction)?
In my case, when an ALTER TABLE ... ADD CONSTRAINT command fails, the transaction cannot be rolled back to the savepoint (the attempt to do so throws an InvalidOperationException).
Overview to demonstrate the crucial point:
SqlTransaction transaction = connection.BeginTransaction();
// ... execute SQL commands on the transaction ...
// Create savepoint
transaction.Save("mySavepoint");
try
{
// This will fail...
SqlCommand boom = new SqlCommand(
"ALTER TABLE table WITH CHECK ADD CONSTRAINT ...",
connection,
transaction);
boom.ExecuteNonQuery();
}
catch
{
// ...and should be rolled back to the savepoint, but can't.
try
{
transaction.Rollback("mySavepoint");
}
catch (InvalidOperationException)
{
// Instead, an InvalidOperationException is thrown.
// The transaction is unusable and can only be rolled back entirely.
transaction.Rollback();
}
}
And here's ready-to-run demo code to test (you need a datase named "test"):
public class Demo
{
private const string _connectionString = "Data Source=(local);Integrated security=true;Initial Catalog=test;";
private const string _savepoint = "save";
private static readonly string _tableName = DateTime.Now.ToString("hhmmss");
private static readonly string _constraintName = "CK" + DateTime.Now.ToString("hhmmss");
private static readonly string _createTable = "CREATE TABLE [dbo].[" + _tableName + "] ([one] [int] NULL,[two] [int] NULL) ON [PRIMARY]";
private static readonly string _insert1 = "INSERT INTO [" + _tableName + "] VALUES (1,1)";
private static readonly string _addConstraint = "ALTER TABLE [dbo].[" + _tableName + "] WITH CHECK ADD CONSTRAINT [" + _constraintName + "] CHECK (([one]>(1)))";
private static readonly string _insert2 = "INSERT INTO [" + _tableName + "] VALUES (2,2)";
public static void Main(string[] args)
{
// Example code! Please ignore missing using statements.
SqlConnection connection = new SqlConnection(_connectionString);
connection.Open();
SqlTransaction transaction = connection.BeginTransaction();
SqlCommand createTable = new SqlCommand(_createTable, connection, transaction);
createTable.ExecuteNonQuery();
// Create savepoint
transaction.Save(_savepoint);
SqlCommand insert1 = new SqlCommand(_insert1, connection, transaction);
insert1.ExecuteNonQuery();
try
{
// This will fail...
SqlCommand boom = new SqlCommand(_addConstraint, connection, transaction);
boom.ExecuteNonQuery();
}
catch
{
// ...and should be rolled back to the savepoint, but can't
transaction.Rollback(_savepoint);
}
SqlCommand insert2 = new SqlCommand(_insert2, connection, transaction);
insert2.ExecuteNonQuery();
transaction.Commit();
connection.Close();
}
}
I get the same behaviour when I tried in TSQL.
BEGIN TRAN
CREATE TABLE foo (col int)
INSERT INTO foo values (1)
SAVE TRANSACTION ProcedureSave;
BEGIN TRY
ALTER TABLE foo WITH CHECK ADD CONSTRAINT ck CHECK (col= 2)
END TRY
BEGIN CATCH
SELECT XACT_STATE() AS XACT_STATE
/*Returns -1, transaction is uncommittable. Next line will fail*/
ROLLBACK TRANSACTION ProcedureSave
/*Msg 3931, Level 16, State 1: The current transaction cannot be committed and
cannot be rolled back to a savepoint. Roll back the entire transaction.*/
END CATCH
GO
SELECT ##TRANCOUNT AS [##TRANCOUNT] /*Zero the transaction was rolled back*/
I didn't find any information in the docs that states which errors would lead to the transaction becoming doomed in this way. I think no such documentation exists from this connect item comment.
The answer is, the error handling is
case-by-case. It depends on not only
the serverity, but also the error type
and context. Unfortunately, there is
no published list of error handling
behavior for different errors. In
general, only servere errors should
kill the connection and extremely ones
shutdown server. But when it comes to
statement abort vs transaction abort,
it is hard to summarize the rules --
i.e. it is case-by-case.
I don't think you can intermingle save point usage in scripts and in C#. I perform the following SQL:
BEGIN TRANSACTION
INSERT INTO Foos (Fooname)
VALUES ('Bar1')
SAVE TRANSACTION MySavePoint;
INSERT INTO Foos (FooName)
VALUES ('Bar2')
ROLLBACK TRANSACTION MySavePoint
COMMIT TRANSACTION
This will work in SQL, and will work with the following code:
using (SqlConnection conn = new SqlConnection("connectionString"))
{
conn.Open();
using (SqlTransaction trans = conn.BeginTransaction())
using (SqlCommand comm = new SqlCommand("The Above SQL", conn, trans))
{
comm.ExecuteNonQuery();
trans.Commit();
}
}
If you attempt to trans.Rollback("MySavePoint"); it will fail because the trans object is not in control of the save point - it doesn't know about it.
If you split the SQL out into the two independent inserts and use the following code:
using (SqlConnection conn = new SqlConnection("connectionString"))
{
conn.Open();
using (SqlTransaction trans = conn.BeginTransaction())
using (SqlCommand comm1 = new SqlCommand("INSERT INTO Foos(fooName) VALUES('Bar1')", conn, trans))
using (SqlCommand comm2 = new SqlCommand("INSERT INTO Foos(fooName) VALUES('Bar2')", conn, trans))
{
comm1.ExecuteNonQuery();
trans.Save("MySavePoint");
comm2.ExecuteNonQuery();
trans.Rollback("MySavePoint");
trans.Commit();
}
}
It will work as you expect.
Just a note, always dispose of objects that implement IDisposable - preferably in a using statement.
Further reading:
http://www.davidhayden.com/blog/dave/archive/2005/10/15/2517.aspx
Update: after faffing with this for a while using your sample code, it looks like due to the error coming from SQL, the transaction is being rolled back and becomes unusable. As has been stated in another answer, it appears as though in conjunction with SQL, the transaction is being forcefully rolled back regardless of savepoints due to certain errors. The only recourse for this is to re-order the commands run against the database and not rely on savepoints, or at least not rely on that action being in a savepoint.

Categories

Resources