csharp/0x0ade/CelesteNet/CelesteNet.Server.SqliteModule/SqliteUserData.cs

SqliteUserData.cs
using MessagePack;
using Microsoft.Data.Sqlite;
using Microsoft.Xna.Framework;
using Monocle;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Codeastysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Transactions;

namespace Celeste.Mod.CelesteNet.Server.Sqlite {
    public sealed clast SqliteUserData : UserData {

        public static readonly HashSet Illegal = new("`´'\"^[]\\//");

        public readonly object GlobalLock = new();

        public readonly SqliteModule Module;

        public string UserRoot => Path.Combine(Module.Settings.UserDataRoot, "User");
        public string DBPath => Path.Combine(Module.Settings.UserDataRoot, "main.db");

        private readonly ThreadLocal _Batch = new();
        public BatchContext Batch => _Batch.Value ??= new(this);

        public SqliteUserData(SqliteModule module)
            : base(module.Server) {
            Module = module;

            if (!File.Exists(DBPath)) {
                using MiniCommand mini = new(this) {
                    SqliteOpenMode.ReadWriteCreate,
                    @"
                        -- sqlite can only into one database.
                        -- CREATE DATABASE main;

                        CREATE TABLE [meta] (
                            iid INTEGER PRIMARY KEY AUTOINCREMENT,
                            uid VARCHAR(255) UNIQUE,
                            key VARCHAR(255),
                            keyfull VARCHAR(255),
                            registered BOOLEAN
                        );

                        CREATE TABLE [data] (
                            iid INTEGER PRIMARY KEY AUTOINCREMENT,
                            name VARCHAR(255) UNIQUE,
                            real VARCHAR(255) UNIQUE,
                            type VARCHAR(255) UNIQUE
                        );

                        CREATE TABLE [file] (
                            iid INTEGER PRIMARY KEY AUTOINCREMENT,
                            name VARCHAR(255) UNIQUE,
                            real VARCHAR(255) UNIQUE
                        );
                    "
                };
                mini.Run();
            }
        }

        public override void Dispose() {
        }

        public override UserDataBatchContext OpenBatch() {
            BatchContext batch = Batch;
            batch.Count++;
            return batch;
        }

        public SqliteConnection Open(SqliteOpenMode mode) {
            SqliteConnection con = new(new SqliteConnectionStringBuilder() {
                DataSource = DBPath,
                Mode = mode,
            }.ToString());
            con.Open();
            return con;
        }

        public List GetAllTables() {
            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadOnly,
                @"
                    SELECT name
                    FROM sqlite_master
                    WHERE type = 'table';
                ",
            };
            (SqliteConnection con, SqliteCommand cmd, SqliteDataReader reader) = mini.Read();

            List names = new();
            while (reader.Read())
                names.Add(reader.GetString(0));

            return names;
        }

        public string GetDataTable(Type type, bool create) {
            type ??= typeof(object);
            string real = type.FullName ?? throw new Exception($"Type without full name: {type}");

            // FIXME: AAAAAAAAAAAAAAAAAAAAAAAAAA
            string name = $"data.{real}".Sanitize(Illegal);

            if (create) {
                string typename = type.astemblyQualifiedName ?? throw new Exception($"Type without astembly qualified name: {type}");
                using MiniCommand mini = new(this) {
                    SqliteOpenMode.ReadWrite,
                    @$"
                        CREATE TABLE IF NOT EXISTS [{name}] (
                            iid INTEGER PRIMARY KEY AUTOINCREMENT,
                            uid VARCHAR(255) UNIQUE,
                            format INTEGER,
                            value BLOB
                        );

                        INSERT OR IGNORE INTO data (name, real, type)
                        VALUES ($name, $real, $type);

                        SELECT name
                        FROM sqlite_master
                        WHERE type = 'table'
                        AND name = $name;
                    ",
                    { "$name", name },
                    { "$real", real },
                    { "$type", typename },
                };
                return mini.Run().value;

            } else {
                using MiniCommand mini = new(this) {
                    SqliteOpenMode.ReadOnly,
                    @"
                        SELECT name
                        FROM sqlite_master
                        WHERE type = 'table'
                        AND name = $name;
                    ",
                    { "$name", name }
                };
                return mini.Run().value ?? "";
            }
        }

        public string GetDataTable(string shortname, bool create) {
            // FIXME: AAAAAAAAAAAAAAAAAAAAAAAAAA
            string name = $"data.{shortname}".Sanitize(Illegal);

            if (create) {
                using MiniCommand mini = new(this) {
                    SqliteOpenMode.ReadWrite,
                    @$"
                        CREATE TABLE IF NOT EXISTS [{name}] (
                            iid INTEGER PRIMARY KEY AUTOINCREMENT,
                            uid VARCHAR(255) UNIQUE,
                            format INTEGER,
                            value BLOB
                        );

                        -- The data table name -> type name mapping will be set once GetDataTable(Type) is called.
                        -- INSERT OR IGNORE INTO data (name, real, type)
                        -- VALUES ($name, $real, $type);

                        SELECT name
                        FROM sqlite_master
                        WHERE type = 'table'
                        AND name = $name;
                    ",
                    { "$name", name },
                };
                return mini.Run().value;

            } else {
                using MiniCommand mini = new(this) {
                    SqliteOpenMode.ReadOnly,
                    @"
                        SELECT name
                        FROM sqlite_master
                        WHERE type = 'table'
                        AND name = $name;
                    ",
                    { "$name", name },
                };
                return mini.Run().value ?? "";
            }
        }

        public string GetFileTable(string name, bool create) {
            string real = name;

            // FIXME: AAAAAAAAAAAAAAAAAAAAAAAAAA
            name = $"file.{real}".Sanitize(Illegal);

            if (create) {
                using MiniCommand mini = new(this) {
                    SqliteOpenMode.ReadWrite,
                    @$"
                        CREATE TABLE IF NOT EXISTS [{name}] (
                            iid INTEGER PRIMARY KEY AUTOINCREMENT,
                            uid VARCHAR(255) UNIQUE,
                            value BLOB
                        );

                        INSERT OR IGNORE INTO file (name, real)
                        VALUES ($name, $real);

                        SELECT name
                        FROM sqlite_master
                        WHERE type = 'table'
                        AND name = $name;
                    ",
                    { "$name", name },
                    { "$real", real },
                };
                return mini.Run().value;

            } else {
                using MiniCommand mini = new(this) {
                    SqliteOpenMode.ReadOnly,
                    @"
                        SELECT name
                        FROM sqlite_master
                        WHERE type = 'table'
                        AND name = $name;
                    ",
                    { "$name", name },
                };
                return mini.Run().value ?? "";
            }
        }

        public override string GetUID(string key) {
            if (key.IsNullOrEmpty())
                return "";
            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadOnly,
                @"
                    SELECT uid
                    FROM meta
                    WHERE key = $key
                    LIMIT 1;
                ",
                { "$key", key },
            };
            (SqliteConnection con, SqliteCommand cmd, SqliteDataReader reader) = mini.Read();
            return reader.Read() ? reader.GetString(0) : "";
        }

        public override string GetKey(string uid) {
            if (uid.IsNullOrEmpty())
                return "";
            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadOnly,
                @"
                    SELECT key
                    FROM meta
                    WHERE uid = $uid
                    LIMIT 1;
                ",
                { "$uid", uid },
            };
            (SqliteConnection con, SqliteCommand cmd, SqliteDataReader reader) = mini.Read();
            return reader.Read() ? reader.GetString(0) : "";
        }

        public override bool TryLoad(string uid, out T value) {
            using UserDataBatchContext batch = OpenBatch();
            string table = GetDataTable(typeof(T), false);
            if (table.IsNullOrEmpty()) {
                value = new();
                return false;
            }
            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadOnly,
                @$"
                    SELECT format, value
                    FROM [{table}]
                    WHERE uid = $uid
                    LIMIT 1;
                ",
                { "$uid", uid },
            };
            (SqliteConnection con, SqliteCommand cmd, SqliteDataReader reader) = mini.Read();

            if (!reader.Read()) {
                value = new();
                return false;
            }

            switch ((DataFormat) reader.GetInt32(0)) {
                case DataFormat.MessagePack:
                default: {
                    using Stream stream = reader.GetStream(1);
                    value = MessagePackSerializer.Deserialize(stream, MessagePackHelper.Options) ?? new();
                    return true;
                }

                case DataFormat.Yaml: {
                    using Stream stream = reader.GetStream(1);
                    using StreamReader streamReader = new(stream);
                    value = YamlHelper.Deserializer.Deserialize(streamReader) ?? new();
                    return true;
                }
            }
        }

        public override Stream? ReadFile(string uid, string name) {
            using UserDataBatchContext batch = OpenBatch();
            string table = GetFileTable(name, false);
            if (table.IsNullOrEmpty())
                return null;
            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadOnly,
                @$"
                    SELECT value
                    FROM [{table}]
                    WHERE uid = $uid
                    LIMIT 1;
                ",
                { "$uid", uid },
            };
            (SqliteConnection con, SqliteCommand cmd, SqliteDataReader reader) = mini.Read();

            if (!reader.Read())
                return null;

            Stream stream = reader.GetStream(0);
            if (stream is MemoryStream ms)
                return ms;

            ms = new MemoryStream();
            using (stream)
                stream.CopyTo(ms);
            ms.Seek(0, SeekOrigin.Begin);
            return ms;
        }

        public override void Save(string uid, T value) {
            using UserDataBatchContext batch = OpenBatch();
            using MemoryStream ms = new();
            MessagePackSerializer.Serialize(typeof(T), ms, value, MessagePackHelper.Options);

            ms.Seek(0, SeekOrigin.Begin);

            string table = GetDataTable(typeof(T), true);
            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadWrite,
                EnsureExistsQuery + @$"
                    REPLACE INTO [{table}] (uid, format, value)
                    VALUES ($uid, $format, zeroblob($length));

                    SELECT _ROWID_
                    FROM [{table}]
                    WHERE uid = $uid;
                ",
                { "$uid", uid },
                { "$format", (int) DataFormat.MessagePack },
                { "$length", ms.Length },
            };
            (SqliteConnection con, SqliteCommand cmd) = mini.Run(out long rowid);

            using SqliteBlob blob = new(con, table, "value", rowid);
            ms.CopyTo(blob);
            blob.Dispose();
        }

        public override Stream WriteFile(string uid, string name) {
            MemoryStream ms = new();
            return new DisposeActionStream(ms, () => {
                ms.Seek(0, SeekOrigin.Begin);

                using UserDataBatchContext batch = OpenBatch();
                string table = GetFileTable(name, true);
                using MiniCommand mini = new(this) {
                    SqliteOpenMode.ReadWrite,
                    EnsureExistsQuery + @$"
                        REPLACE INTO [{table}] (uid, value)
                        VALUES ($uid, zeroblob($length));

                        SELECT _ROWID_
                        FROM [{table}]
                        WHERE uid = $uid;
                    ",
                    { "$uid", uid },
                    { "$length", ms.Length },
                };
                (SqliteConnection con, SqliteCommand cmd) = mini.Run(out long rowid);

                using SqliteBlob blob = new(con, table, "value", rowid);
                ms.CopyTo(blob);
                blob.Dispose();
            });
        }

        public override void Delete(string uid) {
            using UserDataBatchContext batch = OpenBatch();
            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadWrite,
                @$"
                    DELETE
                    FROM [{GetDataTable(typeof(T), true)}]
                    WHERE uid = $uid;
                ",
                { "$uid", uid },
            };
            mini.Run();

            CheckCleanup(uid);
        }

        public override void DeleteFile(string uid, string name) {
            using UserDataBatchContext batch = OpenBatch();
            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadWrite,
                @$"
                    DELETE
                    FROM [{GetFileTable(name, true)}]
                    WHERE uid = $uid;
                ",
                { "$uid", uid },
            };
            mini.Run();

            CheckCleanup(uid);
        }

        private void CheckCleanup(string uid) {
            using UserDataBatchContext batch = OpenBatch();
            using MiniCommand mini = new(this);

            mini.Next(new() {
                SqliteOpenMode.ReadOnly,
                @$"
                    SELECT registered
                    FROM meta
                    WHERE uid = $uid
                    LIMIT 1;
                ",
                { "$uid", uid },
            });
            (SqliteConnection con, SqliteCommand cmd, SqliteDataReader reader) = mini.Read();

            if (!reader.Read() || reader.GetBoolean(0))
                return;

            foreach (string table in GetAllTables()) {
                if (table == "meta")
                    continue;

                mini.Next(new() {
                SqliteOpenMode.ReadOnly,
                    [email protected]"
                        SELECT iid
                        FROM [{table}]
                        WHERE uid = $uid
                        LIMIT 1;
                    ",
                    { "$uid", uid },
                });
                (con, cmd, reader) = mini.Read();

                while (reader.Read())
                    if (!reader.IsDBNull(0))
                        return;
            }

            Wipe(uid);
        }

        public override void Wipe(string uid) {
            using UserDataBatchContext batch = OpenBatch();
            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadWrite,
                string.Join('\n', GetAllTables().Select(table => [email protected]"
                    DELETE
                    FROM [{table}]
                    WHERE uid = $uid;
                ")),
                { "$uid", uid },
            };
            mini.Run();
        }

        public override T[] LoadRegistered() {
            using UserDataBatchContext batch = OpenBatch();
            string table = GetDataTable(typeof(T), false);
            if (table.IsNullOrEmpty())
                return Dummy.EmptyArray;

            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadOnly,
                @$"
                    SELECT D.format, D.value
                    FROM [{table}] D
                    INNER JOIN meta M ON D.uid = M.uid
                    WHERE M.registered = 1;
                ",
            };
            (SqliteConnection con, SqliteCommand cmd, SqliteDataReader reader) = mini.Read();

            List values = new();
            while (reader.Read()) {
                switch ((DataFormat) reader.GetInt32(0)) {
                    case DataFormat.MessagePack:
                    default: {
                        using Stream stream = reader.GetStream(1);
                        values.Add(MessagePackSerializer.Deserialize(stream, MessagePackHelper.Options) ?? new());
                        break;
                    }

                    case DataFormat.Yaml: {
                        using Stream stream = reader.GetStream(1);
                        using StreamReader streamReader = new(stream);
                        values.Add(YamlHelper.Deserializer.Deserialize(streamReader) ?? new());
                        break;
                    }
                }
            }

            return values.ToArray();
        }

        public override T[] LoadAll() {
            using UserDataBatchContext batch = OpenBatch();
            string table = GetDataTable(typeof(T), false);
            if (table.IsNullOrEmpty())
                return Dummy.EmptyArray;

            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadOnly,
                @$"
                    SELECT format, value
                    FROM [{table}];
                ",
            };
            (SqliteConnection con, SqliteCommand cmd, SqliteDataReader reader) = mini.Read();

            List values = new();
            while (reader.Read()) {
                switch ((DataFormat) reader.GetInt32(0)) {
                    case DataFormat.MessagePack:
                    default: {
                        using Stream stream = reader.GetStream(1);
                        values.Add(MessagePackSerializer.Deserialize(stream, MessagePackHelper.Options) ?? new());
                        break;
                    }

                    case DataFormat.Yaml: {
                        using Stream stream = reader.GetStream(1);
                        using StreamReader streamReader = new(stream);
                        values.Add(YamlHelper.Deserializer.Deserialize(streamReader) ?? new());
                        break;
                    }
                }
            }

            return values.ToArray();
        }

        public override string[] GetRegistered() {
            using UserDataBatchContext batch = OpenBatch();
            string[] uids = new string[GetRegisteredCount()];
            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadOnly,
                @"
                    SELECT uid
                    FROM meta
                    WHERE registered = 1;
                ",
            };
            (SqliteConnection con, SqliteCommand cmd, SqliteDataReader reader) = mini.Read();
            for (int i = 0; i < uids.Length && reader.Read(); i++)
                uids[i] = reader.GetString(0);
            return uids;
        }

        public override string[] GetAll() {
            using UserDataBatchContext batch = OpenBatch();
            string[] uids = new string[GetAllCount()];
            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadOnly,
                @"
                    SELECT uid
                    FROM meta;
                ",
            };
            (SqliteConnection con, SqliteCommand cmd, SqliteDataReader reader) = mini.Read();
            for (int i = 0; i < uids.Length && reader.Read(); i++)
                uids[i] = reader.GetString(0);
            return uids;
        }

        public override int GetRegisteredCount() {
            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadOnly,
                @"
                    SELECT COUNT(uid)
                    FROM meta
                    WHERE registered = 1;
                ",
            };
            return (int) mini.Run().value;
        }

        public override int GetAllCount() {
            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadOnly,
                @"
                    SELECT COUNT(uid)
                    FROM meta;
                ",
            };
            return (int) mini.Run().value;
        }

        public static readonly string EnsureExistsQuery = @"
            INSERT OR IGNORE INTO meta (uid, key, keyfull, registered)
            VALUES ($uid, '', '', 0);
        ";
        public void EnsureExists(string uid) {
            lock (GlobalLock) {
                using MiniCommand mini = new(this) {
                    EnsureExistsQuery,
                    { "$uid", uid },
                };
                mini.Run();
            }
        }

        public override string Create(string uid) {
            lock (GlobalLock) {
                string key = GetKey(uid);
                if (!key.IsNullOrEmpty())
                    return key;

                string keyFull;
                do {
                    keyFull = Guid.NewGuid().ToString().Replace("-", "");
                    key = keyFull.Substring(0, 16);
                } while (!GetUID(key).IsNullOrEmpty());

                using MiniCommand mini = new(this) {
                    SqliteOpenMode.ReadWrite,
                    @"
                        REPLACE INTO meta (uid, key, keyfull, registered)
                        VALUES ($uid, $key, $keyfull, 1);
                    ",
                    { "$uid", uid },
                    { "$key", key },
                    { "$keyfull", keyFull },
                };
                mini.Run();

                return key;
            }
        }

        public override void RevokeKey(string key) {
            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadWrite,
                @"
                    UPDATE meta
                    SET key = ''
                    WHERE key = $key;
                ",
                { "$key", key },
            };
            mini.Run();
        }

        public override void CopyTo(UserData other) {
            using UserDataBatchContext batchOther = other.OpenBatch();
            using UserDataBatchContext batch = OpenBatch();
            lock (GlobalLock) {
                MiniCommand mini = new(this);

                mini.Next(new() {
                    SqliteOpenMode.ReadOnly,
                    [email protected]"
                        SELECT uid, key, keyfull, registered
                        FROM meta;
                    "
                });
                (SqliteConnection con, SqliteCommand cmd, SqliteDataReader reader) = mini.Read();
                List uids = new();
                while (reader.Read())
                    other.Insert(reader.GetString(0), reader.GetString(1), reader.GetString(2), reader.GetBoolean(3));

                foreach (string table in GetAllTables()) {
                    bool data = table.StartsWith("data.");
                    bool file = table.StartsWith("file.");
                    if (!data && !file)
                        continue;

                    mini.Next(new() {
                        SqliteOpenMode.ReadOnly,
                        [email protected]"
                            SELECT uid, value
                            FROM [{table}];
                        ",
                    });
                    (con, cmd, reader) = mini.Read();

                    if (data) {
                        using MiniCommand miniName = new(this) {
                            mini.Connection,
                            SqliteOpenMode.ReadOnly,
                            @"
                                SELECT real, format, type
                                FROM data
                                WHERE name = $name;
                            ",
                            { "$name", table }
                        };
                        (_, _, SqliteDataReader readerName) = miniName.Read();
                        miniName.Add((SqliteConnection?) null);
                        if (!readerName.Read())
                            continue;
                        string name = readerName.GetString(0);
                        string typeName = readerName.GetString(1);
                        Type? type = Type.GetType(typeName);

                        while (reader.Read()) {
                            string uid = reader.GetString(0);
                            switch ((DataFormat) reader.GetInt32(1)) {
                                case DataFormat.MessagePack:
                                default: {
                                    if (type == null) {
                                        // TODO: Cannot transform data from MessagePack to Yaml!
                                    } else {
                                        using Stream stream = reader.GetStream(2);
                                        object? value = MessagePackSerializer.Deserialize(type, stream, MessagePackHelper.Options);
                                        using MemoryStream ms = new();
                                        using StreamWriter msWriter = new(ms);
                                        YamlHelper.Serializer.Serialize(msWriter, value);
                                        ms.Seek(0, SeekOrigin.Begin);
                                        other.InsertData(uid, name, type, ms);
                                    }
                                    break;
                                }

                                case DataFormat.Yaml: {
                                    using Stream stream = reader.GetStream(2);
                                    other.InsertData(uid, name, type, stream);
                                    break;
                                }
                            }
                        }
                    } else if (file) {
                        using MiniCommand miniName = new(this) {
                            mini.Connection,
                            SqliteOpenMode.ReadOnly,
                            @"
                                SELECT real
                                FROM file
                                WHERE name = $name;
                            ",
                            { "$name", table }
                        };
                        (_, _, SqliteDataReader readerName) = miniName.Read();
                        miniName.Add((SqliteConnection?) null);
                        if (!readerName.Read())
                            continue;
                        string name = readerName.GetString(0);

                        while (reader.Read()) {
                            string uid = reader.GetString(0);
                            using Stream stream = reader.GetStream(1);
                            other.InsertFile(uid, name, stream);
                        }

                    } else {
                        // ??
                    }
                }
            }
        }

        public override void Insert(string uid, string key, string keyFull, bool registered) {
            lock (GlobalLock) {
                using MiniCommand mini = new(this) {
                    SqliteOpenMode.ReadWrite,
                    @"
                        REPLACE INTO meta (uid, key, keyfull, registered)
                        VALUES ($uid, $key, $keyfull, $registered);
                    ",
                    { "$uid", uid },
                    { "$key", key },
                    { "$keyfull", keyFull },
                    { "$registered", registered },
                };
                mini.Run();
            }
        }

        public override void InsertData(string uid, string name, Type? type, Stream stream) {
            using MemoryStream ms = new();
            DataFormat format;

            if (type == null) {
                format = DataFormat.Yaml;
                stream.CopyTo(ms);

            } else {
                format = DataFormat.MessagePack;
                using StreamReader reader = new(stream);
                object? value = YamlHelper.Deserializer.Deserialize(reader, type);
                MessagePackSerializer.Serialize(type, ms, value, MessagePackHelper.Options);
            }

            ms.Seek(0, SeekOrigin.Begin);

            using UserDataBatchContext batch = OpenBatch();
            string table = type != null ? GetDataTable(type, true) : GetDataTable(name, true);
            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadWrite,
                EnsureExistsQuery + @$"
                    REPLACE INTO [{table}] (uid, format, value)
                    VALUES ($uid, $format, zeroblob($length));

                    SELECT _ROWID_
                    FROM [{table}]
                    WHERE uid = $uid;
                ",
                { "$uid", uid },
                { "$format", (int) format },
                { "$length", ms.Length },
            };
            (SqliteConnection con, SqliteCommand cmd) = mini.Run(out long rowid);

            using SqliteBlob blob = new(con, table, "value", rowid);
            ms.CopyTo(blob);
            blob.Dispose();
        }

        public override void InsertFile(string uid, string name, Stream stream) {
            using MemoryStream ms = new();
            stream.CopyTo(ms);

            ms.Seek(0, SeekOrigin.Begin);

            using UserDataBatchContext batch = OpenBatch();
            string table = GetFileTable(name, true);
            using MiniCommand mini = new(this) {
                SqliteOpenMode.ReadWrite,
                EnsureExistsQuery + @$"
                    REPLACE INTO [{table}] (uid, value)
                    VALUES ($uid, zeroblob($length));

                    SELECT _ROWID_
                    FROM [{table}]
                    WHERE uid = $uid;
                ",
                { "$uid", uid },
                { "$length", ms.Length },
            };
            (SqliteConnection con, SqliteCommand cmd) = mini.Run(out long rowid);

            using SqliteBlob blob = new(con, table, "value", rowid);
            ms.CopyTo(blob);
            blob.Dispose();
        }

        public struct MiniCommand : IDisposable, IEnumerable {
            public SqliteUserData UserData;
            public string CommandText;
            public List? Parameters;
            public SqliteOpenMode PreferredMode;

            public BatchContext? Batch;

            public SqliteOpenMode CurrentMode;
            public SqliteConnection? Connection;
            public SqliteCommand? Command;
            public SqliteDataReader? Reader;

            public MiniCommand(SqliteUserData userData) {
                UserData = userData;
                CommandText = "";
                Parameters = default;
                PreferredMode = default;
                Batch = default;
                CurrentMode = default;
                Connection = default;
                Command = default;
                Reader = default;
            }

            public void Next(MiniCommand cmd) {
                CommandText = cmd.CommandText;
                Parameters = cmd.Parameters;
                PreferredMode = cmd.PreferredMode;
            }

            public void Add(SqliteOpenMode mode) => PreferredMode = mode;
            public void Add(SqliteConnection? connection) => Connection = connection;
            public void Add(string text) => CommandText = text;
            public void Add(string key, object value) => (Parameters ??= new()).Add((key, value));

            IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

            public IEnumerator GetEnumerator() {
                yield return ("", CommandText);
                if (Parameters != null)
                    foreach ((string, object) param in Parameters)
                        yield return param;
            }

            public (SqliteConnection, SqliteCommand) Prepare() {
                if (Batch == null) {
                    BatchContext batch = UserData.Batch;
                    if (batch.Count > 0)
                        Batch = batch;
                }

                Reader?.Close();
                ((IDisposable?) Reader)?.Dispose();
                Reader = null;

                if (CurrentMode > PreferredMode) {
                    Command?.Dispose();
                    Command = null;
                    Connection?.Dispose();
                    Connection = null;
                    if (Batch != null) {
                        Batch.Command = null;
                        Batch.Connection = null;
                    }
                }

                SqliteConnection con = Connection ??= (Batch?.Open(PreferredMode) ?? UserData.Open(PreferredMode));

                SqliteCommand cmd = Command ??= (Batch?.Command ?? con.CreateCommand());
                cmd.CommandText = CommandText;
                if (Parameters != null) {
                    cmd.Parameters.Clear();
                    foreach ((string Key, object Value) param in Parameters)
                        cmd.Parameters.AddWithValue(param.Key, param.Value);
                }

                return (con, cmd);
            }

            public (SqliteConnection, SqliteCommand, SqliteDataReader) Read() {
                (SqliteConnection con, SqliteCommand cmd) = Prepare();
                SqliteDataReader reader = Reader = cmd.ExecuteReader();
                return (con, cmd, reader);
            }

            public (SqliteConnection con, SqliteCommand cmd, int value) Run() {
                (SqliteConnection con, SqliteCommand cmd) = Prepare();
                return (con, cmd, cmd.ExecuteNonQuery());
            }

            public (SqliteConnection con, SqliteCommand cmd, T value) Run() {
                (SqliteConnection con, SqliteCommand cmd) = Prepare();
                return (con, cmd, (T) cmd.ExecuteScalar());
            }

            public (SqliteConnection con, SqliteCommand cmd) Run(out T value) {
                (SqliteConnection con, SqliteCommand cmd) = Prepare();
                value = (T) cmd.ExecuteScalar();
                return (con, cmd);
            }

            public void Dispose() {
                Reader?.Close();
                ((IDisposable?) Reader)?.Dispose();
                Reader = null;
                if (Batch == null) {
                    Command?.Dispose();
                    Command = null;
                    Connection?.Dispose();
                    Connection = null;
                }
            }
        }

        public enum DataFormat {
            MessagePack,
            Yaml
        }

        public sealed clast BatchContext : UserDataBatchContext {

            public SqliteUserData UserData;
            public SqliteOpenMode Mode;
            public SqliteConnection? Connection;
            public SqliteCommand? Command;

            public int Count;

            internal BatchContext(SqliteUserData userData) {
                UserData = userData;
            }

            [MemberNotNull(nameof(Connection))]
            [MemberNotNull(nameof(Command))]
            public SqliteConnection Open(SqliteOpenMode mode) {
                if (Connection != null && Command != null && Mode