Merge pull request #26 from alliedmodders/sql-txn

Add an API for off-thread SQL transactions. (bug 3775, r=kyle)
This commit is contained in:
David Anderson
2014-06-06 20:51:30 -07:00
6 changed files with 611 additions and 26 deletions
+66
View File
@@ -694,3 +694,69 @@ native SQL_TConnect(SQLTCallback:callback, const String:name[]="default", any:da
* @error Invalid database Handle.
*/
native SQL_TQuery(Handle:database, SQLTCallback:callback, const String:query[], any:data=0, DBPriority:prio=DBPrio_Normal);
/**
* Creates a new transaction object. A transaction object is a list of queries
* that can be sent to the database thread and executed as a single transaction.
*
* @return A transaction handle.
*/
native Handle:SQL_CreateTransaction();
/**
* Adds a query to a transaction object.
*
* @param txn A transaction handle.
* @param query Query string.
* @param data Extra data value to pass to the final callback.
* @return The index of the query in the transaction's query list.
* @error Invalid transaction handle.
*/
native SQL_AddQuery(Handle:txn, const String:query[], any:data=0);
/**
* Callback for a successful transaction.
*
* @param db Database handle.
* @param data Data value passed to SQL_ExecuteTransaction().
* @param numQueries Number of queries executed in the transaction.
* @param results An array of Query handle results, one for each of numQueries. They are closed automatically.
* @param queryData An array of each data value passed to SQL_AddQuery().
* @noreturn
*/
functag public SQLTxnSuccess(Handle:db, any:data, numQueries, Handle:results[], any:queryData[]);
/**
* Callback for a failed transaction.
*
* @param db Database handle.
* @param data Data value passed to SQL_ExecuteTransaction().
* @param numQueries Number of queries executed in the transaction.
* @param error Error message.
* @param failIndex Index of the query that failed, or -1 if something else.
* @param queryData An array of each data value passed to SQL_AddQuery().
* @noreturn
*/
functag public SQLTxnFailure(Handle:db, any:data, numQueries, const String:error[], failIndex, any:queryData[]);
/**
* Sends a transaction to the database thread. The transaction handle is
* automatically closed. When the transaction completes, the optional
* callback is invoked.
*
* @param db A database handle.
* @param txn A transaction handle.
* @param onSuccess An optional callback to receive a successful transaction.
* @param onError An optional callback to receive an error message.
* @param data An optional value to pass to callbacks.
* @param prio Priority queue to use.
* @noreturn
* @error An invalid handle.
*/
native SQL_ExecuteTransaction(
Handle:db,
Handle:txn,
SQLTxnSuccess:onSuccess=SQLTxnSuccess:-1,
SQLTxnFailure:onError=SQLTxnFailure:-1,
any:data=0,
DBPriority:priority=DBPrio_Normal);
+72
View File
@@ -0,0 +1,72 @@
/**
* vim: set ts=4 sw=4 tw=99 noet :
* =============================================================================
* SourceMod (C)2004-2014 AlliedModders LLC. All rights reserved.
* =============================================================================
*
* This file is part of the SourceMod/SourcePawn SDK.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, version 3.0, as published by the
* Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*
* As a special exception, AlliedModders LLC gives you permission to link the
* code of this program (as well as its derivative works) to "Half-Life 2," the
* "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
* by the Valve Corporation. You must obey the GNU General Public License in
* all respects for all other code used. Additionally, AlliedModders LLC grants
* this exception to all derivative works. AlliedModders LLC defines further
* exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
* or <http://www.sourcemod.net/license.php>.
*
* Version: $Id$
*/
static TestNumber = 0;
static String:TestContext[255];
SetTestContext(const String:context[])
{
strcopy(TestContext, sizeof(TestContext), context);
}
AssertEq(const String:text[], cell1, cell2)
{
TestNumber++;
if (cell1 == cell2) {
PrintToServer("[%d] %s: %s == %d OK", TestNumber, TestContext, text, cell2);
} else {
PrintToServer("[%d] %s FAIL: %s should be %d, got %d", TestNumber, TestContext, text, cell2, cell1);
ThrowError("test %d (%s in %s) failed", TestNumber, text, TestContext);
}
}
AssertFalse(const String:text[], bool:value)
{
TestNumber++;
if (!value) {
PrintToServer("[%d] %s: %s == false OK", TestNumber, TestContext, text, value);
} else {
PrintToServer("[%d] %s FAIL: %s should be false, got true", TestNumber, TestContext, text);
ThrowError("test %d (%s in %s) failed", TestNumber, text, TestContext);
}
}
AssertTrue(const String:text[], bool:value)
{
TestNumber++;
if (value) {
PrintToServer("[%d] %s: %s == true OK", TestNumber, TestContext, text, value);
} else {
PrintToServer("[%d] %s FAIL: %s should be true, got false", TestNumber, TestContext, text);
ThrowError("test %d (%s in %s) failed", TestNumber, text, TestContext);
}
}
+118
View File
@@ -1,4 +1,5 @@
#include <sourcemod>
#include <testing>
public Plugin:myinfo =
{
@@ -16,6 +17,12 @@ public OnPluginStart()
RegServerCmd("sql_test_thread1", Command_TestSql3)
RegServerCmd("sql_test_thread2", Command_TestSql4)
RegServerCmd("sql_test_thread3", Command_TestSql5)
RegServerCmd("sql_test_txn", Command_TestTxn)
new Handle:hibernate = FindConVar("sv_hibernate_when_empty");
if (hibernate != INVALID_HANDLE) {
ServerCommand("sv_hibernate_when_empty 0");
}
}
PrintQueryData(Handle:query)
@@ -198,3 +205,114 @@ public Action:Command_TestSql5(args)
return Plugin_Handled;
}
FastQuery(Handle:db, const String:query[])
{
new String:error[256];
if (!SQL_FastQuery(db, query)) {
SQL_GetError(db, error, sizeof(error));
ThrowError("ERROR: %s", error);
}
}
public Txn_Test1_OnSuccess(Handle:db, any:data, numQueries, Handle:results[], any:queryData[])
{
SetTestContext("Transaction Test 1");
AssertEq("data", data, 1000);
AssertEq("numQueries", numQueries, 3);
AssertEq("queryData[0]", queryData[0], 50);
AssertEq("queryData[1]", queryData[1], 60);
AssertEq("queryData[2]", queryData[2], 70);
AssertFalse("HasResultSet(0)", SQL_HasResultSet(results[0]));
AssertFalse("HasResultSet(1)", SQL_HasResultSet(results[1]));
AssertTrue("HasResultSet(2)", SQL_HasResultSet(results[2]));
AssertTrue("FetchRow(2)", SQL_FetchRow(results[2]));
AssertEq("FetchInt(2, 0)", SQL_FetchInt(results[2], 0), 5);
AssertFalse("FetchRow(2)", SQL_FetchRow(results[2]));
}
public Txn_Test1_OnFailure(Handle:db, any:data, numQueries, const String:error[], failIndex, any:queryData[])
{
ThrowError("Transaction test 1 failed: %s (failIndex=%d)", error, failIndex);
}
public Txn_Test2_OnSuccess(Handle:db, any:data, numQueries, Handle:results[], any:queryData[])
{
ThrowError("Transaction test 2 failed: should have failed");
}
public Txn_Test2_OnFailure(Handle:db, any:data, numQueries, const String:error[], failIndex, any:queryData[])
{
SetTestContext("Transaction Test 2");
AssertEq("data", data, 1000);
AssertEq("numQueries", numQueries, 3);
AssertEq("queryData[0]", queryData[0], 50);
AssertEq("queryData[1]", queryData[1], 60);
AssertEq("queryData[2]", queryData[2], 70);
AssertEq("failIndex", failIndex, 1);
}
public Txn_Test3_OnSuccess(Handle:db, any:data, numQueries, Handle:results[], any:queryData[])
{
SetTestContext("Transaction Test 3");
AssertEq("data", data, 0);
AssertEq("numQueries", numQueries, 1);
AssertEq("queryData[0]", queryData[0], 0);
AssertTrue("HasResultSet(0)", SQL_HasResultSet(results[0]));
AssertTrue("FetchRow(0)", SQL_FetchRow(results[0]));
AssertEq("FetchInt(0, 0)", SQL_FetchInt(results[0], 0), 5);
}
public Action:Command_TestTxn(args)
{
new String:error[256];
new Handle:db = SQL_Connect("storage-local", false, error, sizeof(error));
if (db == INVALID_HANDLE) {
ThrowError("ERROR: %s", error);
return Plugin_Handled;
}
FastQuery(db, "DROP TABLE IF EXISTS egg");
FastQuery(db, "CREATE TABLE egg(id int primary key)");
FastQuery(db, "INSERT INTO egg (id) VALUES (1)");
FastQuery(db, "INSERT INTO egg (id) VALUES (2)");
FastQuery(db, "INSERT INTO egg (id) VALUES (3)");
SetTestContext("CreateTransaction");
new Handle:txn = SQL_CreateTransaction();
AssertEq("AddQuery", SQL_AddQuery(txn, "INSERT INTO egg (id) VALUES (4)", 50), 0);
AssertEq("AddQuery", SQL_AddQuery(txn, "INSERT INTO egg (id) VALUES (5)", 60), 1);
AssertEq("AddQuery", SQL_AddQuery(txn, "SELECT COUNT(id) FROM egg", 70), 2);
SQL_ExecuteTransaction(
db,
txn,
Txn_Test1_OnSuccess,
Txn_Test1_OnFailure,
1000
);
txn = SQL_CreateTransaction();
AssertEq("AddQuery", SQL_AddQuery(txn, "INSERT INTO egg (id) VALUES (6)", 50), 0);
AssertEq("AddQuery", SQL_AddQuery(txn, "INSERT INTO egg (id) VALUES (6)", 60), 1);
AssertEq("AddQuery", SQL_AddQuery(txn, "SELECT COUNT(id) FROM egg", 70), 2);
SQL_ExecuteTransaction(
db,
txn,
Txn_Test2_OnSuccess,
Txn_Test2_OnFailure,
1000
);
// Make sure the transaction was rolled back - COUNT should be 5.
txn = SQL_CreateTransaction();
AssertEq("CloneHandle", _:CloneHandle(txn), _:INVALID_HANDLE);
SQL_AddQuery(txn, "SELECT COUNT(id) FROM egg");
SQL_ExecuteTransaction(
db,
txn,
Txn_Test3_OnSuccess
);
CloseHandle(db);
return Plugin_Handled;
}