using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SQLite;
namespace VRCX
{
[Table("data")]
public class WorldData
{
[PrimaryKey, AutoIncrement]
[Column("id")]
public int Id { get; set; }
[Column("world_id"), NotNull]
public string WorldId { get; set; }
[Column("key"), NotNull]
public string Key { get; set; }
[Column("value"), NotNull]
public string Value { get; set; }
[Column("value_size"), NotNull]
public int ValueSize { get; set; }
[Column("last_accessed")]
public DateTimeOffset LastAccessed { get; set; }
[Column("last_modified")]
public DateTimeOffset LastModified { get; set; }
}
[Table("worlds")]
public class World
{
[PrimaryKey, AutoIncrement]
[Column("id")]
public int Id { get; set; }
[Column("world_id"), NotNull]
public string WorldId { get; set; }
[Column("connection_key"), NotNull]
public string ConnectionKey { get; set; }
[Column("total_data_size"), NotNull]
public int TotalDataSize { get; set; }
[Column("allow_external_read")]
public bool AllowExternalRead { get; set; }
}
internal class WorldDatabase
{
private static SQLiteConnection sqlite;
private readonly static string dbInitQuery = @"
CREATE TABLE IF NOT EXISTS worlds (
id INTEGER PRIMARY KEY AUTOINCREMENT,
world_id TEXT NOT NULL UNIQUE,
connection_key TEXT NOT NULL,
total_data_size INTEGER DEFAULT 0,
allow_external_read INTEGER DEFAULT 0
);
\
CREATE TABLE IF NOT EXISTS data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
world_id TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
value_size INTEGER NOT NULL DEFAULT 0,
last_accessed INTEGER DEFAULT (strftime('%s', 'now')),
last_modified INTEGER DEFAULT (strftime('%s', 'now')),
FOREIGN KEY (world_id) REFERENCES worlds(world_id) ON DELETE CASCADE,
UNIQUE (world_id, key)
);
\
CREATE TRIGGER IF NOT EXISTS data_update_trigger
AFTER UPDATE ON data
FOR EACH ROW
BEGIN
UPDATE data SET last_modified = (strftime('%s', 'now')) WHERE id = OLD.id;
END;
\
CREATE TRIGGER IF NOT EXISTS data_insert_trigger
AFTER INSERT ON data
FOR EACH ROW
BEGIN
UPDATE data SET last_accessed = (strftime('%s', 'now')), last_modified = (strftime('%s', 'now')) WHERE id = NEW.id;
END;";
public WorldDatabase(string databaseLocation)
{
var options = new SQLiteConnectionString(databaseLocation, true);
sqlite = new SQLiteConnection(options);
sqlite.Execute(dbInitQuery);
// TODO: Split these init queries into their own functions so we can call/update them individually.
var queries = dbInitQuery.Split('\\');
sqlite.BeginTransaction();
foreach (var query in queries)
{
sqlite.Execute(query);
}
sqlite.Commit();
}
///
/// Checks if a world with the specified ID exists in the database.
///
/// The ID of the world to check for.
/// True if the world exists in the database, false otherwise.
public bool DoesWorldExist(string worldId)
{
var query = sqlite.Table().Where(w => w.WorldId == worldId).Select(w => w.WorldId);
return query.Any();
}
///
/// Gets the ID of the world with the specified connection key from the database.
///
/// The connection key of the world to get the ID for.
/// The ID of the world with the specified connection key, or null if no such world exists in the database.
public string GetWorldByConnectionKey(string connectionKey)
{
var query = sqlite.Table().Where(w => w.ConnectionKey == connectionKey).Select(w => w.WorldId);
return query.FirstOrDefault();
}
///
/// Gets the connection key for a world from the database.
///
/// The ID of the world to get the connection key for.
/// The connection key for the specified world, or null if the world does not exist in the database.
public string GetWorldConnectionKey(string worldId)
{
var query = sqlite.Table().Where(w => w.WorldId == worldId).Select(w => w.ConnectionKey);
return query.FirstOrDefault();
}
///
/// Sets the connection key for a world in the database. If the world already exists in the database, the connection key is updated. Otherwise, a new world is added to the database with the specified connection key.
///
/// The ID of the world to set the connection key for.
/// The connection key to set for the world.
/// The connection key that was set.
public string SetWorldConnectionKey(string worldId, string connectionKey)
{
var query = sqlite.Table().Where(w => w.WorldId == worldId).Select(w => w.ConnectionKey);
if (query.Any())
{
sqlite.Execute("UPDATE worlds SET connection_key = ? WHERE world_id = ?", connectionKey, worldId);
}
else
{
sqlite.Insert(new World() { WorldId = worldId, ConnectionKey = connectionKey });
}
return connectionKey;
}
///
/// Sets the value of the allow_external_read field for the world with the specified ID in the database.
///
/// The ID of the world to set the allow_external_read field for.
/// The value to set for the allow_external_read field.
public void SetWorldAllowExternalRead(string worldId, bool allowExternalRead)
{
sqlite.Execute("UPDATE worlds SET allow_external_read = ? WHERE world_id = ?", allowExternalRead, worldId);
}
///
/// Gets the value of the allow_external_read field for the world with the specified ID from the database.
///
/// The ID of the world to get the allow_external_read field for.
/// The value of the allow_external_read field for the specified world.
public bool GetWorldAllowExternalRead(string worldId)
{
var query = sqlite.Table().Where(w => w.WorldId == worldId).Select(w => w.AllowExternalRead);
return query.FirstOrDefault();
}
///
/// Adds a new world to the database.
///
/// The ID of the world to add.
/// The connection key of the world to add.
/// Thrown if a world with the specified ID already exists in the database.
public void AddWorld(string worldId, string connectionKey)
{
// * This will throw an error if the world already exists.. so don't do that
sqlite.Insert(new World() { WorldId = worldId, ConnectionKey = connectionKey });
}
///
/// Gets the world with the specified ID from the database.
///
/// The ID of the world to get.
/// The world with the specified ID, or null if no such world exists in the database.
public World GetWorld(string worldId)
{
var query = sqlite.Table().Where(w => w.WorldId == worldId);
return query.FirstOrDefault();
}
///
/// Gets the total data size shared across all rows, in bytes, for the world with the specified ID from the database.
///
/// The ID of the world to get the total data size for.
/// The total data size for the world, in bytes.
public int GetWorldDataSize(string worldId)
{
var query = sqlite.Table().Where(w => w.WorldId == worldId).Select(w => w.TotalDataSize);
return query.FirstOrDefault();
}
///
/// Updates the total data size, in bytes for the world with the specified ID in the database.
///
/// The ID of the world to update the total data size for.
/// The new total data size for the world, in bytes.
public void UpdateWorldDataSize(string worldId, int size)
{
sqlite.Execute("UPDATE worlds SET total_data_size = ? WHERE world_id = ?", size, worldId);
}
///
/// Adds or updates a data entry in the database with the specified world ID, key, and value.
///
/// The ID of the world to add the data entry for.
/// The key of the data entry to add or replace.
/// The value of the data entry to add or replace.
/// The size of the data entry to add or replace, in bytes. If null, the size is calculated from the value automatically.
public void AddDataEntry(string worldId, string key, string value, int? dataSize = null)
{
int byteSize = dataSize ?? Encoding.UTF8.GetByteCount(value);
// check if entry already exists;
// INSERT OR REPLACE(InsertOrReplace method) deletes the old row and creates a new one, incrementing the id, which I don't want
var query = sqlite.Table().Where(w => w.WorldId == worldId && w.Key == key);
if (query.Any())
{
sqlite.Execute("UPDATE data SET value = ?, value_size = ? WHERE world_id = ? AND key = ?", value, byteSize, worldId, key);
}
else
{
sqlite.Insert(new WorldData() { WorldId = worldId, Key = key, Value = value, ValueSize = byteSize });
}
}
///
/// Gets the data entry with the specified world ID and key from the database.
///
/// The ID of the world to get the data entry for.
/// The key of the data entry to get.
/// The data entry with the specified world ID and key, or null if no such data entry exists in the database.
public WorldData GetDataEntry(string worldId, string key)
{
var query = sqlite.Table().Where(w => w.WorldId == worldId && w.Key == key);
return query.FirstOrDefault();
}
///
/// Gets the data entries with the specified world ID and keys from the database.
///
/// The ID of the world to get the data entries for.
/// The keys of the data entries to get.
/// An enumerable collection of the data entries with the specified world ID and keys.
public IEnumerable GetDataEntries(string worldId, string[] keys)
{
var query = sqlite.Table().Where(w => w.WorldId == worldId && keys.Contains(w.Key));
return query.ToList();
}
///
/// Gets all data entries for the world with the specified ID from the database.
///
/// The ID of the world to get the data entries for.
/// An enumerable collection of all data entries for the world with the specified ID.
public IEnumerable GetAllDataEntries(string worldId)
{
var query = sqlite.Table().Where(w => w.WorldId == worldId).Take(10000);
return query.ToList();
}
///
/// Gets the size of the data entry, in bytes, with the specified world ID and key from the database.
///
/// The ID of the world to get the data entry size for.
/// The key of the data entry to get the size for.
/// The size of the data entry with the specified world ID and key, or 0 if no such data entry exists in the database.
public int GetDataEntrySize(string worldId, string key)
{
var query = sqlite.Table().Where(w => w.WorldId == worldId && w.Key == key).Select(w => w.ValueSize);
return query.FirstOrDefault();
}
///
/// Deletes the data entry with the specified world ID and key from the database.
///
/// The ID of the world to delete the data entry from.
/// The key of the data entry to delete.
public void DeleteDataEntry(string worldId, string key)
{
sqlite.Execute("DELETE FROM data WHERE world_id = ? AND key = ?", worldId, key);
}
///
/// Deletes all data entries for the world with the specified ID from the database.
///
/// The ID of the world to delete all data entries for.
public void DeleteAllDataEntriesForWorld(string worldId)
{
sqlite.Execute("DELETE FROM data WHERE world_id = ?", worldId);
}
public void Close()
{
sqlite.Close();
}
}
}