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);
+ }
+}