Skip to content
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

implement /database and /database/query #1802

Merged
merged 11 commits into from
Aug 19, 2024
19 changes: 19 additions & 0 deletions Content.Tests/DMProject/Tests/Database/clear.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/proc/RunTest()
var/database/db = new("clear.db")

var/database/query/query = new("invalid command text")
query.Clear()

// Add with no parameters does nothing
query.Add()

// Execute without a command does nothing
query.Execute()

// and shouldn't report an error
ASSERT(!query.Error())

del(query)
del(db)

fdel("clear.db")
13 changes: 13 additions & 0 deletions Content.Tests/DMProject/Tests/Database/error.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/proc/RunTest()
var/database/db = new("database.db")

var/database/query/query = new("I am the greatest SQL query writer of all time.")
query.Execute(db)

ASSERT(query.Error() == 1)
ASSERT(length(query.ErrorMsg()) > 1)

ASSERT(db.Error() == 1)
ASSERT(length(db.ErrorMsg()) > 1)

fdel("database.db")
7 changes: 7 additions & 0 deletions Content.Tests/DMProject/Tests/Database/new_empty.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// RUNTIME ERROR

/proc/RunTest()
var/database/db = new()
var/database/query/query = new("CREATE TABLE foobar (id int)")

query.Execute(db)
15 changes: 15 additions & 0 deletions Content.Tests/DMProject/Tests/Database/no_entries.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/proc/RunTest()
var/database/db = new("noentries.db")

var/database/query/query = new("CREATE TABLE foobar (id int)")
query.Execute(db)

query.Add("SELECT * FROM foobar")
query.Execute(db)
query.NextRow()

query.GetRowData()

ASSERT(query.Error() && query.ErrorMsg())

fdel("noentries.db")
22 changes: 22 additions & 0 deletions Content.Tests/DMProject/Tests/Database/open_and_open.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/proc/RunTest()
var/database/db = new("foo.db")

var/database/query/query = new("CREATE TABLE foo (id int)")
query.Execute(db)

query.Add("INSERT INTO foo VALUES (1)")
query.Execute(db)

ASSERT(query.RowsAffected() == 1)

db.Open("bar.db")
query.Add("CREATE TABLE bar (id int)")
query.Execute(db)

query.Add("INSERT INTO bar VALUES (1)")
query.Execute(db)

ASSERT(query.RowsAffected() == 1)

fdel("foo.db")
fdel("bar.db")
54 changes: 54 additions & 0 deletions Content.Tests/DMProject/Tests/Database/read.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/proc/RunTest()
var/database/db = new("database.db")

var/database/query/query = new("CREATE TABLE test (id int, name string, points float)")
query.Execute(db)

query.Add("INSERT INTO test VALUES (?, ?, ?)", 1, "foo", 1.5)
query.Execute(db)

ASSERT(query.RowsAffected() == 1)

query.Add("SELECT * FROM test WHERE id = ?", 1)
query.Execute(db)
query.NextRow()

var/list/assoc = query.GetRowData()
ASSERT(length(assoc) == 3)

ASSERT(assoc["id"] == 1)
ASSERT(assoc["name"] == "foo")
ASSERT(assoc["points"] == 1.5)

ASSERT(query.GetColumn(0) == 1)
ASSERT(query.GetColumn(1) == "foo")
ASSERT(query.GetColumn(2) == 1.5)

var/list/columns = query.Columns()
ASSERT(columns[1] == "id")
ASSERT(columns[2] == "name")
ASSERT(columns[3] == "points")

ASSERT(query.Columns(0) == "id")
ASSERT(query.Columns(1) == "name")
ASSERT(query.Columns(2) == "points")

ASSERT(!query.Columns(10))

ASSERT(query.Error() && query.ErrorMsg())

query.Close()
db.Close()

db.Open("database.db")

query.Add("SELECT * FROM test WHERE id = ?", 1)
query.Execute(db)
query.NextRow()

ASSERT(query.GetColumn(0) == 1)

ASSERT(!query.GetColumn(10))
ASSERT(query.Error() && query.ErrorMsg())

fdel("database.db")
14 changes: 14 additions & 0 deletions Content.Tests/DMProject/Tests/Database/too_many_or_few_params.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/proc/RunTest()
var/database/db = new("params.db")

var/database/query/query = new("CREATE TABLE test (id int)", 1, "foo", 1.5)
query.Execute(db)

ASSERT(!query.Error()) // no error for too many parameters

query.Add("INSERT INTO test VALUES (?, ?, ?)", 1)
query.Execute(db)

ASSERT(query.Error()) // but there is an error for too few parameters

fdel("params.db")
8 changes: 8 additions & 0 deletions OpenDreamRuntime/Objects/DreamObjectTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
public TreeEntry Matrix { get; private set; }
public TreeEntry Exception { get; private set; }
public TreeEntry Savefile { get; private set; }
public TreeEntry Database { get; private set; }

Check warning

Code scanning / InspectCode

Non-nullable member is uninitialized. Warning

Non-nullable property 'Database' is uninitialized. Consider declaring the property as nullable.
public TreeEntry DatabaseQuery { get; private set; }

Check warning

Code scanning / InspectCode

Non-nullable member is uninitialized. Warning

Non-nullable property 'DatabaseQuery' is uninitialized. Consider declaring the property as nullable.
public TreeEntry Regex { get; private set; }
public TreeEntry Filter { get; private set; }
public TreeEntry Icon { get; private set; }
Expand Down Expand Up @@ -139,6 +141,10 @@
return CreateList();
if (type == Savefile)
return new DreamObjectSavefile(Savefile.ObjectDefinition);
if (type == Database)
return new DreamObjectDatabase(Database.ObjectDefinition);
if (type == DatabaseQuery)
return new DreamObjectDatabaseQuery(DatabaseQuery.ObjectDefinition);
if (type.ObjectDefinition.IsSubtypeOf(Matrix))
return new DreamObjectMatrix(type.ObjectDefinition);
if (type.ObjectDefinition.IsSubtypeOf(Sound))
Expand Down Expand Up @@ -275,6 +281,8 @@
Matrix = GetTreeEntry("/matrix");
Exception = GetTreeEntry("/exception");
Savefile = GetTreeEntry("/savefile");
Database = GetTreeEntry("/database");
DatabaseQuery = GetTreeEntry("/database/query");
Regex = GetTreeEntry("/regex");
Filter = GetTreeEntry("/dm_filter");
Icon = GetTreeEntry("/icon");
Expand Down
82 changes: 82 additions & 0 deletions OpenDreamRuntime/Objects/Types/DreamObjectDatabase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Data;
using Microsoft.Data.Sqlite;
using OpenDreamRuntime.Procs;

namespace OpenDreamRuntime.Objects.Types;

public sealed class DreamObjectDatabase(DreamObjectDefinition objectDefinition) : DreamObject(objectDefinition) {
private SqliteConnection? _connection;

private string? _errorMessage;
private int? _errorCode;

public override void Initialize(DreamProcArguments args) {
base.Initialize(args);

if (!args.GetArgument(0).TryGetValueAsString(out var filename)) {
return;
}

Open(filename);
}

protected override void HandleDeletion() {
Close();
base.HandleDeletion();
}

/// <summary>
/// Establish the connection to our SQLite database
/// </summary>
/// <param name="filename">The path to the SQLite file</param>
public void Open(string filename) {

if (_connection?.State == ConnectionState.Open) {
Close();
}

_connection = new SqliteConnection($"Data Source={filename};Mode=ReadWriteCreate");

try {
_connection.Open();
} catch (SqliteException exception) {
Logger.GetSawmill("opendream.db").Error($"Failed to open database {filename} - {exception}");
}
}

/// <summary>
/// Attempts to get the current connection to the SQLite database, if it is open
/// </summary>
/// <param name="connection">Variable to be populated with the connection.</param>
/// <returns>Boolean of the success of the operation.</returns>
public bool TryGetConnection(out SqliteConnection? connection) {
if (_connection?.State == ConnectionState.Open) {
connection = _connection;
return true;
}

connection = null;
return false;
}

public void SetError(int code, string message) {
_errorCode = code;
_errorMessage = message;
}

public int? GetErrorCode() {
return _errorCode;
}

public string? GetErrorMessage() {
return _errorMessage;
}

/// <summary>
/// Closes the current SQLite connection, if it is established.
/// </summary>
public void Close() {
_connection?.Close();
}

}
Loading
Loading