Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
2f99e22
feat: Add project and launch settings
MH321Productions Aug 18, 2025
6dee93a
feat: Add extensions for the console and static file paths
MH321Productions Aug 18, 2025
1eae560
feat: Add the two commands and the main program
MH321Productions Aug 18, 2025
b2b5ec8
Merge branch 'main' into identity-deletion-verifier
MH321Productions Aug 18, 2025
0aeba1b
feat: Prepare github workflow
MH321Productions Aug 19, 2025
fd48095
ci: Finish workflow
MH321Productions Aug 20, 2025
bc74073
fix: Change ports of baseUrls
MH321Productions Aug 20, 2025
d00bc69
fix: Create temp export directory if it doesn't exist
MH321Productions Aug 20, 2025
93020f9
chore: Add failure archive of docker logs
MH321Productions Aug 20, 2025
65592cc
ci: upload exported db file and identity file (not only on failure fo…
MH321Productions Aug 20, 2025
0087b7a
fix: Fix variables
MH321Productions Aug 20, 2025
f9692c9
chore: Only upload exported db files on failure
MH321Productions Aug 20, 2025
4d15bbc
Merge branch 'main' into identity-deletion-verifier
MH321Productions Aug 21, 2025
e3f2021
fix: Delete token allocations instead of anonymizing them when deleti…
MH321Productions Aug 22, 2025
a7f481f
chore: Add multi-run config for the identity deletion verifier workflow
MH321Productions Aug 22, 2025
63d9e25
feat: Add datawallets
MH321Productions Aug 22, 2025
c42fbec
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Aug 23, 2025
186ed54
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Aug 24, 2025
ce4c321
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Aug 24, 2025
cd35c90
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Aug 25, 2025
1e6e749
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Aug 25, 2025
68c783b
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Aug 26, 2025
2f1a772
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Aug 26, 2025
395ccbf
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Aug 26, 2025
920183a
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Aug 26, 2025
7b4250a
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Aug 27, 2025
8637877
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Aug 27, 2025
23e5f0d
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Aug 27, 2025
18c22df
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Aug 28, 2025
9f2e612
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Aug 31, 2025
634a201
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 1, 2025
94b3d97
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 1, 2025
ca37a82
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 1, 2025
b0ffc1c
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 1, 2025
2b2fb4d
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 1, 2025
aac6368
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 1, 2025
e329e5f
chore: Remove old IdentityDeletionVerifier project
MH321Productions Sep 9, 2025
fc34241
fix: Fix syntax and output file of SqlServer dump script
MH321Productions Sep 9, 2025
8ba8f49
chore: Import PgDump and SMO
MH321Productions Sep 9, 2025
79e98d6
feat: Add database exporters
MH321Productions Sep 9, 2025
5c73924
feat: Add Sql extractors
MH321Productions Sep 9, 2025
bceecc3
feat: Add deletion verifiers
MH321Productions Sep 9, 2025
1148aa1
chore: Add database configuration to identity deletion job configuration
MH321Productions Sep 9, 2025
6936ef3
feat: Verify deletion after finishing
MH321Productions Sep 9, 2025
631361c
chore: Add helper method to deconstruct connection string
MH321Productions Sep 9, 2025
c5ad7aa
feat: Register deletion verifier, db exporter and sql extractor for d…
MH321Productions Sep 9, 2025
d7caf17
test: Add tests for the deletion verifier and the worker when the ver…
MH321Productions Sep 9, 2025
ae149c8
chore: Remove old deletion verifier workflow
MH321Productions Sep 9, 2025
ed09310
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 9, 2025
d1300d0
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 9, 2025
e5777b7
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 9, 2025
556b4b9
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 10, 2025
382e581
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 10, 2025
dbaa330
chore: Remove unneccessary file check
MH321Productions Sep 10, 2025
37a2cea
chore: Install pg_dump in docker image
MH321Productions Sep 10, 2025
df5e405
fix: Typo in Postgres connection string
MH321Productions Sep 10, 2025
ede5825
chore: Change pg_dump version in docker image
MH321Productions Sep 10, 2025
9884e96
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 10, 2025
dfed17f
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 10, 2025
0105ea2
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 11, 2025
50f2f6f
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 11, 2025
777a41f
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 11, 2025
aa3ee48
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 11, 2025
8271e56
fix: Make User ID uppercase in postgres appsettings override json
MH321Productions Sep 11, 2025
4fa3017
Merge remote-tracking branch 'origin/identity-deletion-verifier' into…
MH321Productions Sep 11, 2025
9fd59bc
test: Update apk package list before installing
MH321Productions Sep 12, 2025
6985b86
test: Log pg_dump version after installing
MH321Productions Sep 12, 2025
6ec6c26
test: Log pg dump version on program startup
MH321Productions Sep 12, 2025
49d2efc
test: Log pg dump version at the start of the worker
MH321Productions Sep 12, 2025
a73d51e
test: Log pg dump version in integration test
MH321Productions Sep 12, 2025
2f7108a
test: Log pg dump path
MH321Productions Sep 12, 2025
2918783
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 12, 2025
9a3fd87
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 13, 2025
c7c69b9
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 15, 2025
ee2042f
ci: Install Postgres 17 in pipeline
MH321Productions Sep 15, 2025
01d7db7
Merge remote-tracking branch 'origin/identity-deletion-verifier' into…
MH321Productions Sep 15, 2025
312ed0e
chore: Change Postgres User ID
MH321Productions Sep 15, 2025
4b95189
fix: Change User ID in local postgres appsettings override file
MH321Productions Sep 16, 2025
c6a1e39
chore: Clean up unneccessary debug messages
MH321Productions Sep 16, 2025
2d4c314
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 17, 2025
8101f04
test: Include passed integration tests in summary
MH321Productions Sep 17, 2025
1f79cf2
Merge remote-tracking branch 'origin/identity-deletion-verifier' into…
MH321Productions Sep 17, 2025
412b0c9
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 17, 2025
e5096b2
test: Set a timeout of 30 seconds per test to identify the faulty one
MH321Productions Sep 17, 2025
5e44cbb
Merge remote-tracking branch 'origin/identity-deletion-verifier' into…
MH321Productions Sep 17, 2025
e76f56f
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 18, 2025
ca21c4c
test: Log every single step
MH321Productions Sep 18, 2025
cfaa695
test: Capture everything
MH321Productions Sep 18, 2025
ea7eec0
test: Remove one capture to trigger new run
MH321Productions Sep 19, 2025
22da433
test: Don't verify
MH321Productions Sep 19, 2025
f362ec7
test: Re-add verification
MH321Productions Sep 19, 2025
7af156e
test: Write a separate verification test and comment out the faulty one
MH321Productions Sep 19, 2025
f8bf433
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 20, 2025
cfaa71d
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 20, 2025
0e9174a
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 22, 2025
19ee5e2
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 22, 2025
e99e730
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 22, 2025
6adac25
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 22, 2025
7173e09
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 22, 2025
10c90e4
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 23, 2025
32112c8
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 23, 2025
50a1749
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 23, 2025
5e6bc84
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 23, 2025
5d67d6b
chore: Re-add faulty test, increase timeout to 60s
MH321Productions Sep 23, 2025
dceabe2
test: Log time in every test
MH321Productions Sep 24, 2025
0fe748c
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 29, 2025
b2f63b3
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 29, 2025
53f1768
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Sep 29, 2025
f51a89f
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Oct 2, 2025
1489355
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Oct 6, 2025
3c8f8fc
Merge branch 'main' into identity-deletion-verifier
mergify[bot] Oct 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .ci/appsettings.override.postgres.docker.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
},
"SqlDatabase": {
"Provider": "Postgres",
"ConnectionString": "User Id=adminUi;Password=Passw0rd;Server=postgres;Port=5432;Database=enmeshed;"
"ConnectionString": "User ID=postgres;Password=Passw0rd;Server=postgres;Port=5432;Database=enmeshed;"
}
},
"ModuleDefaults": {
Expand Down
2 changes: 1 addition & 1 deletion .ci/appsettings.override.postgres.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
},
"SqlDatabase": {
"Provider": "Postgres",
"ConnectionString": "User Id=adminUi;Password=Passw0rd;Server=localhost;Port=5432;Database=enmeshed;"
"ConnectionString": "User ID=postgres;Password=Passw0rd;Server=localhost;Port=5432;Database=enmeshed;"
}
},
"ModuleDefaults": {
Expand Down
14 changes: 11 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ jobs:
strategy:
fail-fast: false
matrix:
database: [sqlserver, postgres]
database: [ sqlserver, postgres ]
test-project:
- display-name: admin-api
path: Applications/AdminApi/test/AdminApi.Tests.Integration
Expand Down Expand Up @@ -207,6 +207,14 @@ jobs:
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: ${{ runner.os }}-nuget-

- name: Install Postgres 17
run: |
sudo apt update
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' # Add Postgres APT Repo
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg # Add GPG Key
sudo apt update
sudo apt install postgresql-17

- name: Download cached Docker images
uses: actions/download-artifact@v5
with:
Expand Down Expand Up @@ -236,7 +244,7 @@ jobs:
docker compose -f ./.ci/compose.test.yml -f ./.ci/compose.test.${{matrix.database}}.yml wait admin-cli

- name: Run integration tests
run: dotnet test --no-restore --no-build --logger "GitHubActions;summary.includeNotFoundTests=false;summary.includeSkippedTests=false;summary.includePassedTests=false" ${{matrix.test-project.path}}
run: dotnet test --no-restore --no-build --logger "GitHubActions;summary.includeNotFoundTests=false;summary.includeSkippedTests=false;summary.includePassedTests=true" --blame-hang-timeout 60s ${{matrix.test-project.path}}
env:
ADMIN_API_BASE_ADDRESS: "http://localhost:5173"
CONSUMER_API_BASE_ADDRESS: "http://localhost:5000"
Expand All @@ -258,7 +266,7 @@ jobs:
strategy:
fail-fast: false
matrix:
database: [sqlserver, postgres]
database: [ sqlserver, postgres ]
needs: image-test-builds
steps:
- name: Checkout backbone repository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ FROM base AS final
RUN apk add icu-libs
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=0

RUN apk add postgresql17-client

WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Backbone.Job.IdentityDeletion.dll"]
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using Backbone.BuildingBlocks.Infrastructure.EventBus;
using Backbone.BuildingBlocks.Infrastructure.Persistence.Database;

namespace Backbone.Job.IdentityDeletion;

Expand All @@ -16,4 +17,7 @@ public class InfrastructureConfiguration
{
[Required]
public required EventBusConfiguration EventBus { get; init; }

[Required]
public required DatabaseConfiguration SqlDatabase { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System.Text.Json;
using System.Text.RegularExpressions;
using Backbone.Job.IdentityDeletion.IdentityDeletionVerifier.Exporters;
using Backbone.Job.IdentityDeletion.IdentityDeletionVerifier.Extractors;

namespace Backbone.Job.IdentityDeletion.IdentityDeletionVerifier;

public class DeletionVerifier : IDeletionVerifier
{
private static readonly JsonSerializerOptions SERIALIZER_OPTIONS = new()
{
IndentSize = 4,
PropertyNameCaseInsensitive = true,
WriteIndented = true
};

private readonly IDbExporter _dbExporter;
private readonly ISqlExtractor _sqlExtractor;

public DeletionVerifier(IDbExporter dbExporter, ISqlExtractor sqlExtractor)
{
_dbExporter = dbExporter;
_sqlExtractor = sqlExtractor;
}

public async Task<DatabaseCheckResult> VerifyDeletion(List<string> addressesToVerify, CancellationToken cancellationToken)
{
await ExportDatabase(cancellationToken);

return await CheckExportedDatabase(addressesToVerify, cancellationToken);
}

private async Task ExportDatabase(CancellationToken cancellationToken)
{
if (!FilePaths.TempDirExists()) Directory.CreateDirectory(FilePaths.PATH_TO_TEMP_DIR);

await _dbExporter.ExportDb(FilePaths.PATH_TO_DUMP_FILE, cancellationToken);
}

private async Task<DatabaseCheckResult> CheckExportedDatabase(List<string> addressesToCheck, CancellationToken cancellationToken)
{
var result = new DatabaseCheckResult
{
Success = true,
FoundOccurrences = []
};

await foreach (var extractedTable in _sqlExtractor.ExtractTables(FilePaths.PATH_TO_DUMP_FILE).WithCancellation(cancellationToken))
{
var found = addressesToCheck.ToDictionary(i => i, _ => 0);

await foreach (var line in extractedTable.EntryLines.WithCancellation(cancellationToken))
{
foreach (var identity in addressesToCheck)
{
var count = Regex.Matches(line, identity).Count;
found[identity] += count;

if (count != 0) result.Success = false;
}
}

if (found.Any(entry => entry.Value != 0))
result.FoundOccurrences.Add(extractedTable.Id, found.Where(kvp => kvp.Value != 0).ToDictionary());
}

return result;
}

public async Task SaveFoundOccurrences(DatabaseCheckResult result, CancellationToken cancellationToken)
{
var groupedOccurrences = result.FoundOccurrences
.GroupBy(x => x.Key.Schema)
.ToDictionary(
group => group.Key,
group => group.ToDictionary(
v => v.Key.Table,
v => v.Value
)
);

await using var stream = new FileStream(FilePaths.PATH_TO_FOUND_FILE, FileMode.Create);
await JsonSerializer.SerializeAsync(stream, groupedOccurrences, SERIALIZER_OPTIONS, cancellationToken);
}
}

public class DeletionFailedException(DatabaseCheckResult result)
: Exception($"Some identities were still found in the database ({result.NumberOfOccurrences} times in total). Check \"{FilePaths.PATH_TO_FOUND_FILE}\" for more details.");

file static class FilePaths
{
private const string DUMP_FILENAME = "db-dump.sql";
private const string FOUND_FILENAME = "found-identities.json";

public static readonly string PATH_TO_TEMP_DIR = Path.Combine(Path.GetTempPath(), "enmeshed", "backbone");
public static readonly string PATH_TO_DUMP_FILE = Path.Combine(PATH_TO_TEMP_DIR, DUMP_FILENAME);
public static readonly string PATH_TO_FOUND_FILE = Path.Combine(PATH_TO_TEMP_DIR, FOUND_FILENAME);

public static bool TempDirExists() => Directory.Exists(PATH_TO_TEMP_DIR);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Backbone.Job.IdentityDeletion.IdentityDeletionVerifier.Exporters;

public interface IDbExporter
{
Task ExportDb(string targetFile, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Backbone.BuildingBlocks.Infrastructure.Persistence.Database;
using Backbone.Tooling.Extensions;
using Microsoft.Extensions.Options;
using PgDump;

namespace Backbone.Job.IdentityDeletion.IdentityDeletionVerifier.Exporters;

public class PostgresDbExporter : IDbExporter
{
private readonly DatabaseConfiguration _configuration;
private readonly ILogger<PostgresDbExporter> _logger;

public PostgresDbExporter(IOptions<IdentityDeletionJobConfiguration> configuration, ILogger<PostgresDbExporter> logger)
{
_configuration = configuration.Value.Infrastructure.SqlDatabase;
_logger = logger;
}

public async Task ExportDb(string targetFile, CancellationToken cancellationToken)
{
_logger.LogInformation("Starting Postgres database dump");

var parts = _configuration.ConnectionString.Split(';');
var host = parts.ExtractString("Server=");
var port = int.Parse(parts.ExtractString("Port="));
var username = parts.ExtractString("User ID=");
var password = parts.ExtractString("Password=");
var database = parts.ExtractString("Database=");

var connection = new ConnectionOptions(host, port, username, password, database);
var client = new PgClient(connection);

await client.DumpAsync(new FileOutputProvider(targetFile), 1.Minutes(), DumpFormat.Plain, cancellationToken);

_logger.LogInformation("Postgres database dump completed");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Text;
using Backbone.BuildingBlocks.Infrastructure.Persistence.Database;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Options;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.Smo;

namespace Backbone.Job.IdentityDeletion.IdentityDeletionVerifier.Exporters;

public class SqlServerDbExporter : IDbExporter
{
private readonly DatabaseConfiguration _configuration;
private readonly ILogger<SqlServerDbExporter> _logger;

public SqlServerDbExporter(IOptions<IdentityDeletionJobConfiguration> configuration, ILogger<SqlServerDbExporter> logger)
{
_configuration = configuration.Value.Infrastructure.SqlDatabase;
_logger = logger;
}

public async Task ExportDb(string targetFile, CancellationToken cancellationToken)
{
_logger.LogInformation("Starting Sql Server database dump");

var parts = _configuration.ConnectionString.Split(';');
var databaseName = parts.ExtractString("Database=");
var server = new Server(new ServerConnection(new SqlConnection(_configuration.ConnectionString)));
var database = server.Databases[databaseName] ?? throw new ArgumentException($"No database with name \"{databaseName}\" found in sql server");
var scripter = new Scripter(server)
{
Options = new ScriptingOptions
{
EnforceScriptingOptions = true,
WithDependencies = false,
IncludeHeaders = false,
ScriptDrops = false,
AppendToFile = false,
ScriptSchema = false,
ScriptData = true,
IncludeIfNotExists = false,
Default = true,
Indexes = false
}
};

await using var writer = new StreamWriter(new FileStream(targetFile, FileMode.Create), Encoding.UTF8);
foreach (var table in database.Tables.Cast<Table>())
{
if (table.IsSystemObject) continue;

_logger.LogInformation("Writing table {schema}.{table}", table.Schema, table.Name);

await writer.WriteLineAsync($"table;{table.Schema};{table.Name}");

foreach (var line in scripter.EnumScript(new[] { table.Urn }))
{
await writer.WriteLineAsync(line);
}

await writer.WriteLineAsync();
await writer.FlushAsync(cancellationToken);
}

_logger.LogInformation("Sql Server database dump completed");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Backbone.Job.IdentityDeletion.IdentityDeletionVerifier.Extractors;

public interface ISqlExtractor
{
IAsyncEnumerable<ExtractedTable> ExtractTables(string file);
}

public record ExtractedTable
{
public required TableId Id { get; init; }
public required IAsyncEnumerable<string> EntryLines { get; init; }
}

public record TableId
{
public required string Schema { get; init; }
public required string Table { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.Text;
using System.Text.RegularExpressions;
using Backbone.Tooling.Extensions;

namespace Backbone.Job.IdentityDeletion.IdentityDeletionVerifier.Extractors;

public partial class PostgresSqlExtractor : ISqlExtractor
{
[GeneratedRegex("""
\"[^\"]*\"
""")]
private static partial Regex TableNameAndSchemaRegex();

private readonly ILogger<PostgresSqlExtractor> _logger;

public PostgresSqlExtractor(ILogger<PostgresSqlExtractor> logger)
{
_logger = logger;
}

public async IAsyncEnumerable<ExtractedTable> ExtractTables(string file)
{
_logger.LogInformation("Extracting dumped postgres data");

var reader = new StreamReader(new FileStream(file, FileMode.Open, FileAccess.Read), Encoding.UTF8);

while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
if (line.IsNullOrEmpty()) continue;

if (line.Trim().StartsWith("COPY"))
{
var id = ExtractTableNameAndSchema(line);
var entryLines = ReadCopyLines(reader);
yield return new ExtractedTable
{
Id = id,
EntryLines = entryLines
};
}
}

_logger.LogInformation("Extraction complete");
}

private static TableId ExtractTableNameAndSchema(string startLine)
{
var matches = TableNameAndSchemaRegex().Matches(startLine);
var schemaName = matches[0].Value.Trim('"');
var tableName = matches[1].Value.Trim('"');

return new TableId
{
Schema = schemaName,
Table = tableName
};
}

private static async IAsyncEnumerable<string> ReadCopyLines(StreamReader reader)
{
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
if (line is null or "\\.") break;

yield return line;
}
}
}
Loading