|
| 1 | +using Microsoft.Data.Sqlite; |
| 2 | +using MySqlPunk.Core.Models; |
| 3 | +using MySqlPunk.Core.Services; |
| 4 | +using Npgsql; |
| 5 | + |
| 6 | +var oldMajor = int.Parse(Environment.GetEnvironmentVariable("MYSQLPUNK_OLD_PG_MAJOR") ?? "14"); |
| 7 | +if (oldMajor is not (14 or 16)) throw new ArgumentException("Only the 14/17 and 16/17 fixtures are defined."); |
| 8 | +var ports = new[] { int.Parse(args[0]), int.Parse(args[1]) }; |
| 9 | +var password = Environment.GetEnvironmentVariable("MYSQLPUNK_PG_TEST_PASSWORD") |
| 10 | + ?? throw new InvalidOperationException("Missing isolated fixture password."); |
| 11 | +string Connection(int port, string database) => new NpgsqlConnectionStringBuilder |
| 12 | +{ |
| 13 | + Host = "127.0.0.1", Port = port, Username = "postgres", Password = password, |
| 14 | + Database = database, SslMode = SslMode.Disable, Pooling = false, Timeout = 3 |
| 15 | +}.ConnectionString; |
| 16 | +foreach (var port in ports) |
| 17 | +{ |
| 18 | + await using var connection = new NpgsqlConnection(Connection(port, "postgres")); |
| 19 | + await connection.OpenAsync(); |
| 20 | + Console.WriteLine("Actual PostgreSQL version=" + connection.PostgreSqlVersion); |
| 21 | + var expectedMajor = port == ports[0] ? oldMajor : 17; |
| 22 | + if (connection.PostgreSqlVersion.Major != expectedMajor) throw new Exception("Unexpected engine major."); |
| 23 | +} |
| 24 | +if (args.Length > 2) return; |
| 25 | +var root = Path.Combine(Path.GetTempPath(), "mysqlpunk-version-fixture-" + Guid.NewGuid().ToString("N")); |
| 26 | +Directory.CreateDirectory(root); |
| 27 | +try |
| 28 | +{ |
| 29 | + foreach (var port in ports) |
| 30 | + { |
| 31 | + await using var connection = new NpgsqlConnection(Connection(port, "postgres")); |
| 32 | + await connection.OpenAsync(); |
| 33 | + foreach (var database in new[] { "migration_source", "migration_back" }) |
| 34 | + { |
| 35 | + await using var command = connection.CreateCommand(); |
| 36 | + command.CommandText = "CREATE DATABASE " + database; |
| 37 | + await command.ExecuteNonQueryAsync(); |
| 38 | + } |
| 39 | + } |
| 40 | + ConnectionProfile Pg(int index, string database) => new() |
| 41 | + { |
| 42 | + Name = "isolated PG", Provider = DatabaseProviderKind.PostgreSql, Host = "127.0.0.1", |
| 43 | + Port = ports[index], Username = "postgres", Password = password, |
| 44 | + Database = database, TlsMode = ConnectionTlsMode.Disabled |
| 45 | + }; |
| 46 | + var source = new ConnectionProfile |
| 47 | + { |
| 48 | + Name = "isolated SQLite", Provider = DatabaseProviderKind.Sqlite, |
| 49 | + Database = Path.Combine(root, "source.db") |
| 50 | + }; |
| 51 | + using (var connection = new SqliteConnection("Data Source=" + source.Database)) |
| 52 | + { |
| 53 | + connection.Open(); |
| 54 | + using var command = connection.CreateCommand(); |
| 55 | + command.CommandText = """ |
| 56 | + CREATE TABLE records (id INTEGER NOT NULL, label TEXT NOT NULL, note TEXT, payload BLOB, |
| 57 | + PRIMARY KEY(id,label)) STRICT, WITHOUT ROWID; |
| 58 | + CREATE INDEX records_label ON records(label); |
| 59 | + INSERT INTO records VALUES |
| 60 | + (1,'abc','繁體中文 😺',X'00FF'),(1,'ABC','',NULL), |
| 61 | + (2,'a','前後空白 ',X''),(2,'a ',NULL,X'010203'),(3,'é','é',NULL); |
| 62 | + CREATE TABLE empty_table (number INTEGER,payload BLOB,note TEXT) STRICT; |
| 63 | + WITH RECURSIVE numbers(number) AS (VALUES(1) UNION ALL SELECT number+1 FROM numbers WHERE number<1000) |
| 64 | + INSERT INTO records SELECT 100+number,'row_'||number, |
| 65 | + CASE WHEN number%3=0 THEN NULL ELSE '測試😀 ' END, |
| 66 | + CASE WHEN number%2=0 THEN NULL ELSE X'00FF' END FROM numbers; |
| 67 | + INSERT INTO records VALUES(-9223372036854775808,'min',NULL,X''),(9223372036854775807,'max','',NULL); |
| 68 | + """; |
| 69 | + command.ExecuteNonQuery(); |
| 70 | + } |
| 71 | + async Task Move(ConnectionProfile from, ConnectionProfile to, int major, string label) |
| 72 | + { |
| 73 | + var preview = await LogicalMigrationService.PreviewAsync(from, to, major); |
| 74 | + Console.WriteLine(label + " preview=" + preview.CanStart + " " + string.Join(";", preview.Blockers)); |
| 75 | + if (!preview.CanStart) throw new Exception(label + " preflight rejected"); |
| 76 | + var result = await LogicalMigrationService.RunAsync(from, to, major, preview, to.Database); |
| 77 | + Console.WriteLine(label + " " + result.Outcome + " rows=" + preview.Tables.Sum(table => table.Rows)); |
| 78 | + if (result.Outcome != LogicalMigrationOutcome.Verified) throw new Exception(label + " " + result.Report); |
| 79 | + } |
| 80 | + var old = Pg(0, "migration_source"); |
| 81 | + var newer = Pg(1, "migration_source"); |
| 82 | + var back = Pg(1, "migration_back"); |
| 83 | + await Move(source, newer, 17, "SQLite -> PG17 seed"); |
| 84 | + await Move(newer, old, oldMajor, "PG17 -> PG" + oldMajor + " downgrade"); |
| 85 | + await Move(old, back, 17, "PG" + oldMajor + " -> PG17 upgrade"); |
| 86 | + var final = source.Clone(); final.Database = Path.Combine(root, "back.db"); |
| 87 | + using (var connection = new SqliteConnection("Data Source=" + final.Database)) connection.Open(); |
| 88 | + await Move(back, final, 3, "PG17 -> SQLite roundtrip"); |
| 89 | + using (var connection = new SqliteConnection("Data Source=" + final.Database)) |
| 90 | + { |
| 91 | + connection.Open(); |
| 92 | + using var command = connection.CreateCommand(); |
| 93 | + command.CommandText = "SELECT count(*) FROM records"; |
| 94 | + if (Convert.ToInt64(command.ExecuteScalar()) != 1007) throw new Exception("Roundtrip count."); |
| 95 | + command.CommandText = "SELECT count(*) FROM sqlite_schema WHERE type='index' AND name='records_label'"; |
| 96 | + if (Convert.ToInt64(command.ExecuteScalar()) != 1) throw new Exception("Named index lost."); |
| 97 | + command.CommandText = "INSERT INTO records VALUES(1,'abc',NULL,NULL)"; |
| 98 | + try { command.ExecuteNonQuery(); throw new Exception("Composite PK lost."); } |
| 99 | + catch (SqliteException exception) when (exception.SqliteErrorCode == 19) { } |
| 100 | + } |
| 101 | + Console.WriteLine("PASS actual PG17 -> PG" + oldMajor + " -> PG17 plus SQLite roundtrip; 1007 rows + empty table; composite PK, named index, UTF8/emoji, NULL/empty binary/text, signed Int64 extrema; production transaction and SHA256 verification"); |
| 102 | +} |
| 103 | +finally |
| 104 | +{ |
| 105 | + SqliteConnection.ClearAllPools(); |
| 106 | + Directory.Delete(root, true); |
| 107 | +} |
0 commit comments