diff --git a/Content.Tests/DMProject/Tests/Database/clear.dm b/Content.Tests/DMProject/Tests/Database/clear.dm new file mode 100644 index 0000000000..5790d9d641 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Database/clear.dm @@ -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") \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Database/error.dm b/Content.Tests/DMProject/Tests/Database/error.dm new file mode 100644 index 0000000000..c2c9de6822 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Database/error.dm @@ -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") \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Database/new_empty.dm b/Content.Tests/DMProject/Tests/Database/new_empty.dm new file mode 100644 index 0000000000..de23e24e05 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Database/new_empty.dm @@ -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) \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Database/no_entries.dm b/Content.Tests/DMProject/Tests/Database/no_entries.dm new file mode 100644 index 0000000000..0fb55d8501 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Database/no_entries.dm @@ -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") \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Database/open_and_open.dm b/Content.Tests/DMProject/Tests/Database/open_and_open.dm new file mode 100644 index 0000000000..b85ca66be4 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Database/open_and_open.dm @@ -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") \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Database/read.dm b/Content.Tests/DMProject/Tests/Database/read.dm new file mode 100644 index 0000000000..f32b93c605 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Database/read.dm @@ -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") diff --git a/Content.Tests/DMProject/Tests/Database/too_many_or_few_params.dm b/Content.Tests/DMProject/Tests/Database/too_many_or_few_params.dm new file mode 100644 index 0000000000..21ec3df5ed --- /dev/null +++ b/Content.Tests/DMProject/Tests/Database/too_many_or_few_params.dm @@ -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") diff --git a/DMCompiler/DMStandard/Types/Database.dm b/DMCompiler/DMStandard/Types/Database.dm new file mode 100644 index 0000000000..ed0fbc80e0 --- /dev/null +++ b/DMCompiler/DMStandard/Types/Database.dm @@ -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() diff --git a/DMCompiler/DMStandard/UnsortedAdditions.dm b/DMCompiler/DMStandard/UnsortedAdditions.dm index edda8f0612..8ad632c98c 100644 --- a/DMCompiler/DMStandard/UnsortedAdditions.dm +++ b/DMCompiler/DMStandard/UnsortedAdditions.dm @@ -43,30 +43,6 @@ proc/missile(Type, Start, End) /proc/walk_rand(Ref,Lag=0,Speed=0) 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() diff --git a/DMCompiler/DMStandard/_Standard.dm b/DMCompiler/DMStandard/_Standard.dm index 3df53f9234..73ae5874a5 100644 --- a/DMCompiler/DMStandard/_Standard.dm +++ b/DMCompiler/DMStandard/_Standard.dm @@ -112,6 +112,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" diff --git a/DMCompiler/DreamPath.cs b/DMCompiler/DreamPath.cs index 0338fcba87..2a3a832f32 100644 --- a/DMCompiler/DreamPath.cs +++ b/DMCompiler/DreamPath.cs @@ -21,6 +21,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"); diff --git a/OpenDreamRuntime/Objects/DreamObjectTree.cs b/OpenDreamRuntime/Objects/DreamObjectTree.cs index ce86bb4c33..b0a09d5827 100644 --- a/OpenDreamRuntime/Objects/DreamObjectTree.cs +++ b/OpenDreamRuntime/Objects/DreamObjectTree.cs @@ -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; } @@ -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)) @@ -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"); diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectDatabase.cs b/OpenDreamRuntime/Objects/Types/DreamObjectDatabase.cs new file mode 100644 index 0000000000..bd5c1bb692 --- /dev/null +++ b/OpenDreamRuntime/Objects/Types/DreamObjectDatabase.cs @@ -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(); + } + + /// + /// Establish the connection to our SQLite database + /// + /// The path to the SQLite file + 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(";", ""); + } + + /// + /// Attempts to get the current connection to the SQLite database, if it is open + /// + /// Variable to be populated with the connection. + /// Boolean of the success of the operation. + 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; + } + + /// + /// Closes the current SQLite connection, if it is established. + /// + public void Close() { + _connection?.Close(); + } +} diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectDatabaseQuery.cs b/OpenDreamRuntime/Objects/Types/DreamObjectDatabaseQuery.cs new file mode 100644 index 0000000000..a81adec2b8 --- /dev/null +++ b/OpenDreamRuntime/Objects/Types/DreamObjectDatabaseQuery.cs @@ -0,0 +1,245 @@ +using System.Text; +using Microsoft.Data.Sqlite; +using OpenDreamRuntime.Procs; + +namespace OpenDreamRuntime.Objects.Types; + +public sealed class DreamObjectDatabaseQuery(DreamObjectDefinition objectDefinition) : DreamObject(objectDefinition) { + private SqliteCommand? _command; + private SqliteDataReader? _reader; + + private string? _errorMessage; + private int? _errorCode; + + public override void Initialize(DreamProcArguments args) { + base.Initialize(args); + + if (!args.GetArgument(0).TryGetValueAsString(out var command)) { + return; + } + + SetupCommand(command, args.Values[1..]); + } + + protected override void HandleDeletion() { + CloseReader(); + base.HandleDeletion(); + } + + /// + /// Sets up the SQLiteCommand, setting up parameters when provided. + /// Supports strings and floats from DMcode. + /// + /// The command text of the SQLite command, with placeholders denoted by '?' + /// The values to be substituted into the command + public void SetupCommand(string command, ReadOnlySpan values) { + _command = new SqliteCommand(ParseCommandText(command)); + + for (var i = 0; i < values.Length; i++) { + var arg = values[i]; + + var type = arg.Type; + switch (type) { + case DreamValue.DreamValueType.String: + if (arg.TryGetValueAsString(out var stringValue)) { + _command.Parameters.AddWithValue($"@{i}", stringValue); + } + + break; + case DreamValue.DreamValueType.Float: + if (arg.TryGetValueAsFloat(out var floatValue)) { + _command.Parameters.AddWithValue($"@{i}", floatValue); + } + + break; + + case DreamValue.DreamValueType.DreamResource: + case DreamValue.DreamValueType.DreamObject: + case DreamValue.DreamValueType.DreamType: + case DreamValue.DreamValueType.DreamProc: + case DreamValue.DreamValueType.Appearance: + default: + // TODO: support saving BLOBS for icons, if we really want to + break; + } + } + } + + /// + /// Gets the names of all the columns in the current query + /// + /// A list of s containing the names of the columns in the query + public List GetAllColumns() { + if (_reader is null) { + return []; + } + + var names = new List(); + for (var i = 0; i < _reader.FieldCount; i++) { + names.Add(new DreamValue(_reader.GetName(i))); + } + + return names; + } + + /// + /// Gets the name of a single column in the current query + /// + /// The column ordinal value. + /// A of the name of the column. + public DreamValue GetColumn(int id) { + if (_reader is null) { + return DreamValue.Null; + } + + try { + var name = _reader.GetName(id); + return new DreamValue(name); + } catch (IndexOutOfRangeException exception) { + _errorCode = 1; + _errorMessage = exception.Message; + } + + return DreamValue.Null; + } + + public void ClearCommand() { + _command = null; + } + + public void CloseReader() { + _reader?.Close(); + } + + public int? GetErrorCode() { + return _errorCode; + } + + public string? GetErrorMessage() { + return _errorMessage; + } + + /// + /// Executes the currently held query against the SQLite database + /// + /// The that this query is being run against. + public void ExecuteCommand(DreamObjectDatabase database) { + if (!database.TryGetConnection(out var connection)) { + throw new DMCrashRuntime("Bad database"); + } + + if (_command == null) { + return; + } + + _command.Connection = connection; + + try { + _reader = _command.ExecuteReader(); + } catch (SqliteException exception) { + _errorCode = exception.SqliteErrorCode; + _errorMessage = exception.Message; + database.SetError(exception.SqliteErrorCode, exception.Message); + } + + ClearCommand(); + } + + public void NextRow() { + _reader?.Read(); + } + + /// + /// Attempts to fetch the value of a specific column. + /// + /// The ordinal column number + /// The out variable to be populated with the of the result. + /// + public bool TryGetColumn(int column, out DreamValue value) { + if (_reader is null) { + value = DreamValue.Null; + return false; + } + + try { + value = GetDreamValueFromDbObject(_reader.GetValue(column)); + return true; + } catch (Exception exception) { + _errorCode = 1; + _errorMessage = exception.Message; + } + + value = DreamValue.Null; + return false; + } + + public Dictionary? CurrentRowData() { + if (_reader is null) { + return null; + } + + var dict = new Dictionary(); + var totalColumns = _reader.FieldCount; + try { + for (var i = 0; i < totalColumns; i++) { + var name = _reader.GetName(i); + var value = _reader.GetValue(i); + + dict[name] = GetDreamValueFromDbObject(value); + } + } catch (InvalidOperationException exception) { + _errorCode = 1; + _errorMessage = exception.Message; + } + + return dict; + } + + public int RowsAffected() { + return _reader?.RecordsAffected ?? 0; + } + + /// + /// Converts a retrieved from the SQLite database to a containing the value. + /// + /// The from the database. + /// A containing the value. + /// Unsupported data type + private static DreamValue GetDreamValueFromDbObject(object value) { + return value switch { + float floatValue => new DreamValue(floatValue), + double doubleValue => new DreamValue(doubleValue), + long longValue => new DreamValue(longValue), + int intValue => new DreamValue(intValue), + string stringValue => new DreamValue(stringValue), + _ => throw new ArgumentOutOfRangeException(nameof(value)), + }; + } + + /// + /// Builds a new string, converting '?' characters to expressions we can bind to later + /// + /// The raw command text + /// A with the characters converted + private static string ParseCommandText(string text) { + var newString = new StringBuilder(); + + var paramsId = 0; + var inQuotes = false; + foreach (var character in text) { + switch (character) { + case '\'': + case '"': + inQuotes = !inQuotes; + break; + case '?' when !inQuotes: + newString.Append($"@{paramsId++}"); + continue; + } + + newString.Append(character); + } + + return newString.ToString(); + } +} diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs index 46fad26fb8..89a20bc2cd 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs @@ -143,13 +143,30 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) { objectTree.SetNativeProc(objectTree.Savefile, DreamProcNativeSavefile.NativeProc_ExportText); objectTree.SetNativeProc(objectTree.Savefile, DreamProcNativeSavefile.NativeProc_Flush); - + objectTree.SetNativeProc(objectTree.World, DreamProcNativeWorld.NativeProc_Export); objectTree.SetNativeProc(objectTree.World, DreamProcNativeWorld.NativeProc_GetConfig); objectTree.SetNativeProc(objectTree.World, DreamProcNativeWorld.NativeProc_Profile); objectTree.SetNativeProc(objectTree.World, DreamProcNativeWorld.NativeProc_SetConfig); objectTree.SetNativeProc(objectTree.World, DreamProcNativeWorld.NativeProc_ODHotReloadInterface); objectTree.SetNativeProc(objectTree.World, DreamProcNativeWorld.NativeProc_ODHotReloadResource); + + objectTree.SetNativeProc(objectTree.Database, DreamProcNativeDatabase.NativeProc_Close); + objectTree.SetNativeProc(objectTree.Database, DreamProcNativeDatabase.NativeProc_Error); + objectTree.SetNativeProc(objectTree.Database, DreamProcNativeDatabase.NativeProc_ErrorMsg); + objectTree.SetNativeProc(objectTree.Database, DreamProcNativeDatabase.NativeProc_Open); + + objectTree.SetNativeProc(objectTree.DatabaseQuery, DreamProcNativeDatabaseQuery.NativeProc_Add); + objectTree.SetNativeProc(objectTree.DatabaseQuery, DreamProcNativeDatabaseQuery.NativeProc_Clear); + objectTree.SetNativeProc(objectTree.DatabaseQuery, DreamProcNativeDatabaseQuery.NativeProc_Close); + objectTree.SetNativeProc(objectTree.DatabaseQuery, DreamProcNativeDatabaseQuery.NativeProc_Columns); + objectTree.SetNativeProc(objectTree.DatabaseQuery, DreamProcNativeDatabaseQuery.NativeProc_Error); + objectTree.SetNativeProc(objectTree.DatabaseQuery, DreamProcNativeDatabaseQuery.NativeProc_ErrorMsg); + objectTree.SetNativeProc(objectTree.DatabaseQuery, DreamProcNativeDatabaseQuery.NativeProc_Execute); + objectTree.SetNativeProc(objectTree.DatabaseQuery, DreamProcNativeDatabaseQuery.NativeProc_GetColumn); + objectTree.SetNativeProc(objectTree.DatabaseQuery, DreamProcNativeDatabaseQuery.NativeProc_GetRowData); + objectTree.SetNativeProc(objectTree.DatabaseQuery, DreamProcNativeDatabaseQuery.NativeProc_NextRow); + objectTree.SetNativeProc(objectTree.DatabaseQuery, DreamProcNativeDatabaseQuery.NativeProc_RowsAffected); SetOverridableNativeProc(objectTree, objectTree.World, DreamProcNativeWorld.NativeProc_Error); SetOverridableNativeProc(objectTree, objectTree.World, DreamProcNativeWorld.NativeProc_Reboot); diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeDatabase.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeDatabase.cs new file mode 100644 index 0000000000..5fab679a7b --- /dev/null +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeDatabase.cs @@ -0,0 +1,45 @@ +using OpenDreamRuntime.Objects; +using OpenDreamRuntime.Objects.Types; +using DreamValueTypeFlag = OpenDreamRuntime.DreamValue.DreamValueTypeFlag; + +namespace OpenDreamRuntime.Procs.Native; + +internal static class DreamProcNativeDatabase { + [DreamProc("Close")] + public static DreamValue NativeProc_Close(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var database = (DreamObjectDatabase)src!; + + database.Close(); + + return DreamValue.Null; + } + + [DreamProc("Error")] + public static DreamValue NativeProc_Error(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var database = (DreamObjectDatabase)src!; + + return new DreamValue(database.GetErrorCode() ?? 0); + } + + [DreamProc("ErrorMsg")] + public static DreamValue NativeProc_ErrorMsg(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var database = (DreamObjectDatabase)src!; + + var message = database.GetErrorMessage(); + return message == null ? DreamValue.Null : new DreamValue(message); + } + + [DreamProc("Open")] + [DreamProcParameter("filename", Type = DreamValueTypeFlag.String)] + public static DreamValue NativeProc_Open(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var database = (DreamObjectDatabase)src!; + + DreamValue fileValue = bundle.GetArgument(0, "filename"); + + if (!fileValue.TryGetValueAsString(out var filename)) return DreamValue.Null; + + if (!database.Open(filename)) throw new DMCrashRuntime("Could not open database."); + + return DreamValue.Null; + } +} diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeDatabaseQuery.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeDatabaseQuery.cs new file mode 100644 index 0000000000..85dba06989 --- /dev/null +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeDatabaseQuery.cs @@ -0,0 +1,140 @@ +using OpenDreamRuntime.Objects; +using OpenDreamRuntime.Objects.Types; +using DreamValueTypeFlag = OpenDreamRuntime.DreamValue.DreamValueTypeFlag; + +namespace OpenDreamRuntime.Procs.Native; + +internal static class DreamProcNativeDatabaseQuery { + [DreamProc("Add")] + [DreamProcParameter("text", Type = DreamValueTypeFlag.String)] + [DreamProcParameter("item1")] + public static DreamValue NativeProc_Add(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var query = (DreamObjectDatabaseQuery)src!; + + query.ClearCommand(); + + if (!bundle.GetArgument(0, "text").TryGetValueAsString(out var command)) { + return DreamValue.Null; + } + + query.SetupCommand(command, bundle.Arguments[1..]); + + return DreamValue.Null; + } + + [DreamProc("Clear")] + public static DreamValue NativeProc_Clear(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var query = (DreamObjectDatabaseQuery)src!; + + query.ClearCommand(); + + return DreamValue.Null; + } + + [DreamProc("Close")] + public static DreamValue NativeProc_Close(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var query = (DreamObjectDatabaseQuery)src!; + + query.CloseReader(); + + return DreamValue.Null; + } + + [DreamProc("Columns")] + [DreamProcParameter("column", Type = DreamValueTypeFlag.Float)] + public static DreamValue NativeProc_Columns(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var query = (DreamObjectDatabaseQuery)src!; + + if (bundle.GetArgument(0, "column").TryGetValueAsInteger(out var column)) { + return query.GetColumn(column); + } + + var list = bundle.ObjectTree.CreateList(); + + foreach (var value in query.GetAllColumns()) { + list.AddValue(value); + } + + return new DreamValue(list); + } + + [DreamProc("Error")] + public static DreamValue NativeProc_Error(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var query = (DreamObjectDatabaseQuery)src!; + + var code = query.GetErrorCode(); + return code.HasValue ? new DreamValue(code.Value) : DreamValue.Null; + } + + [DreamProc("ErrorMsg")] + public static DreamValue NativeProc_ErrorMsg(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var query = (DreamObjectDatabaseQuery)src!; + + var message = query.GetErrorMessage(); + + return message == null ? DreamValue.Null : new DreamValue(message); + } + + [DreamProc("Execute")] + [DreamProcParameter("database", Type = DreamValueTypeFlag.DreamObject)] + public static DreamValue NativeProc_Execute(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var query = (DreamObjectDatabaseQuery)src!; + + if (!bundle.GetArgument(0, "database").TryGetValueAsDreamObject(out DreamObjectDatabase? database)) + return DreamValue.Null; + + query.ExecuteCommand(database); + + return DreamValue.Null; + } + + [DreamProc("RowsAffected")] + public static DreamValue NativeProc_RowsAffected(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var query = (DreamObjectDatabaseQuery)src!; + + return new DreamValue(query.RowsAffected()); + } + + [DreamProc("NextRow")] + public static DreamValue NativeProc_NextRow(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var query = (DreamObjectDatabaseQuery)src!; + + query.NextRow(); + + return DreamValue.Null; + } + + [DreamProc("GetColumn")] + [DreamProcParameter("column", Type = DreamValueTypeFlag.Float)] + public static DreamValue NativeProc_GetColumn(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var query = (DreamObjectDatabaseQuery)src!; + + if (!bundle.GetArgument(0, "column").TryGetValueAsInteger(out var column)) { + return DreamValue.Null; + } + + if (!query.TryGetColumn(column, out var value)) { + return DreamValue.Null; + } + + return value; + } + + [DreamProc("GetRowData")] + public static DreamValue NativeProc_GetRowData(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var query = (DreamObjectDatabaseQuery)src!; + + var data = query.CurrentRowData(); + + if (data == null) { + return DreamValue.Null; + } + + var list = bundle.ObjectTree.CreateList(); + foreach (var entry in data) { + list.SetValue(new DreamValue(entry.Key), entry.Value); + } + + return new DreamValue(list); + } +}