Skip to content

Commit

Permalink
implement /database and /database/query (#1802)
Browse files Browse the repository at this point in the history
Co-authored-by: wixoa <[email protected]>
  • Loading branch information
hry-gh and wixoaGit authored Aug 19, 2024
1 parent 9b12503 commit 97e8841
Show file tree
Hide file tree
Showing 17 changed files with 721 additions and 25 deletions.
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")
23 changes: 23 additions & 0 deletions DMCompiler/DMStandard/Types/Database.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/database
parent_type = /datum
proc/Close()
proc/Error()
proc/ErrorMsg()
New(filename)
proc/Open(filename)

/database/query
var/_binobj as opendream_unimplemented
proc/Add(text, ...)
proc/Clear()
Close()
proc/Columns(column)
Error()
ErrorMsg()
proc/Execute(database)
proc/GetColumn(column)
proc/GetRowData()
New(text, ...)
proc/NextRow()
proc/Reset()
proc/RowsAffected()
24 changes: 0 additions & 24 deletions DMCompiler/DMStandard/UnsortedAdditions.dm
Original file line number Diff line number Diff line change
Expand Up @@ -41,30 +41,6 @@ proc/missile(Type, Start, End)
/proc/splittext_char(Text,Start=1,End=0,Insert="")
set opendream_unimplemented = TRUE

/database
parent_type = /datum
proc/Close()
proc/Error()
proc/ErrorMsg()
New(filename)
proc/Open(filename)

/database/query
var/_binobj as opendream_unimplemented
proc/Add(text, ...)
proc/Clear()
Close()
proc/Columns(column)
Error()
ErrorMsg()
proc/Execute(database)
proc/GetColumn(column)
proc/GetRowData()
New(text, ...)
proc/NextRow()
proc/Reset()
proc/RowsAffected()

/proc/_dm_db_new_con()
set opendream_unimplemented = TRUE
/proc/_dm_db_connect()
Expand Down
1 change: 1 addition & 0 deletions DMCompiler/DMStandard/_Standard.dm
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ proc/winset(player, control_id, params)

#include "Defines.dm"
#include "Types\Client.dm"
#include "Types\Database.dm"
#include "Types\Datum.dm"
#include "Types\Exception.dm"
#include "Types\Filter.dm"
Expand Down
2 changes: 2 additions & 0 deletions DMCompiler/DreamPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public struct DreamPath {
public static readonly DreamPath World = new DreamPath("/world");
public static readonly DreamPath Client = new DreamPath("/client");
public static readonly DreamPath Datum = new DreamPath("/datum");
public static readonly DreamPath Database = new DreamPath("/database");
public static readonly DreamPath DatabaseQuery = new DreamPath("/database/query");
public static readonly DreamPath Matrix = new DreamPath("/matrix");
public static readonly DreamPath Atom = new DreamPath("/atom");
public static readonly DreamPath Area = new DreamPath("/area");
Expand Down
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 sealed class DreamObjectTree {
public TreeEntry Matrix { get; private set; }
public TreeEntry Exception { get; private set; }
public TreeEntry Savefile { get; private set; }
public TreeEntry Database { get; private set; }
public TreeEntry DatabaseQuery { get; private set; }
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 @@ public DreamObject CreateObject(TreeEntry type) {
return CreateList();
if (type == Savefile)
return new DreamObjectSavefile(Savefile.ObjectDefinition);
if (type.ObjectDefinition.IsSubtypeOf(DatabaseQuery))
return new DreamObjectDatabaseQuery(type.ObjectDefinition);
if (type.ObjectDefinition.IsSubtypeOf(Database))
return new DreamObjectDatabase(type.ObjectDefinition);
if (type.ObjectDefinition.IsSubtypeOf(Matrix))
return new DreamObjectMatrix(type.ObjectDefinition);
if (type.ObjectDefinition.IsSubtypeOf(Sound))
Expand Down Expand Up @@ -275,6 +281,8 @@ private void LoadTypesFromJson(DreamTypeJson[] types, ProcDefinitionJson[]? proc
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
95 changes: 95 additions & 0 deletions OpenDreamRuntime/Objects/Types/DreamObjectDatabase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System.Data;
using System.IO;
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;
}

if(Open(filename)) return;

throw new DMCrashRuntime("Unable to open database.");
}

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 bool Open(string filename) {
if (_connection?.State == ConnectionState.Open) {
Close();
}

filename = SanitizeFilename(filename);

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

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

private static string SanitizeFilename(string filename) {
foreach (var character in Path.GetInvalidFileNameChars()) {
filename = filename.Replace(character.ToString(), "");
}

return filename.Replace("=", "").Replace(";", "");
}

/// <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

0 comments on commit 97e8841

Please sign in to comment.