Files
Copilot e72bde9c02 Add Jellyfin.DbMigrator SQLite-to-PostgreSQL migration tool (#14)
* Initial plan

* Add Jellyfin.DbMigrator SQLite-to-PostgreSQL migration tool

Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>
2026-03-31 22:56:07 -04:00

98 lines
4.0 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Data.Sqlite;
namespace Jellyfin.DbMigrator;
/// <summary>
/// Reads rows from a SQLite database table using raw ADO.NET.
/// </summary>
public static class SqliteTableReader
{
/// <summary>
/// Returns all rows from the specified SQLite table as a list of column-name-to-value dictionaries.
/// </summary>
/// <param name="connection">An open <see cref="SqliteConnection"/>.</param>
/// <param name="tableName">The name of the table to read.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A list where each element is a dictionary mapping column name to its value (may be <see langword="null"/>).</returns>
public static async Task<List<IReadOnlyDictionary<string, object?>>> ReadAllRowsAsync(
SqliteConnection connection,
string tableName,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(connection);
ArgumentException.ThrowIfNullOrWhiteSpace(tableName);
TableNameValidator.EnsureSafe(tableName);
var rows = new List<IReadOnlyDictionary<string, object?>>();
var cmd = connection.CreateCommand();
await using (cmd.ConfigureAwait(false))
{
cmd.CommandText = $"SELECT * FROM \"{tableName}\"";
var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
await using (reader.ConfigureAwait(false))
{
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
var row = new Dictionary<string, object?>(reader.FieldCount, StringComparer.Ordinal);
for (int i = 0; i < reader.FieldCount; i++)
{
string col = reader.GetName(i);
bool isNull = await reader.IsDBNullAsync(i, cancellationToken).ConfigureAwait(false);
object? val = isNull ? null : reader.GetValue(i);
row[col] = val;
}
rows.Add(row);
}
}
}
return rows;
}
/// <summary>
/// Returns the row count for the specified table in the SQLite database.
/// </summary>
/// <param name="connection">An open <see cref="SqliteConnection"/>.</param>
/// <param name="tableName">The name of the table to count.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>The number of rows in the table, or -1 if the table does not exist.</returns>
public static async Task<long> CountRowsAsync(
SqliteConnection connection,
string tableName,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(connection);
ArgumentException.ThrowIfNullOrWhiteSpace(tableName);
TableNameValidator.EnsureSafe(tableName);
// Check if the table exists first.
var checkCmd = connection.CreateCommand();
await using (checkCmd.ConfigureAwait(false))
{
checkCmd.CommandText =
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=$name";
checkCmd.Parameters.AddWithValue("$name", tableName);
var exists = await checkCmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
if (exists is not long existsLong || existsLong == 0)
{
return -1L;
}
}
var cmd = connection.CreateCommand();
await using (cmd.ConfigureAwait(false))
{
cmd.CommandText = $"SELECT COUNT(*) FROM \"{tableName}\"";
var result = await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
return result is long count ? count : Convert.ToInt64(result, System.Globalization.CultureInfo.InvariantCulture);
}
}
}