Skip to content

Commit

Permalink
Fixes for migrating partitions with foreign keys in PostgreSQL
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremydmiller committed Dec 23, 2024
1 parent 647a3ee commit c3fc7df
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 4 deletions.
9 changes: 9 additions & 0 deletions src/Weasel.Core/ISchemaObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ public SchemaMigrationException(AutoCreate autoCreate, IEnumerable<object> inval
}
}

/// <summary>
/// Schema objects that may need to analyze other schema objects in order to correctly
/// generate their own model. Originally introduced for PostgreSQL partitioning with foreign keys
/// </summary>
public interface ISchemaObjectWithPostProcessing : ISchemaObject
{
void PostProcess(ISchemaObject[] allObjects);
}

/// <summary>
/// Responsible for the desired configuration of a single database object like
/// a table, sequence, of function.
Expand Down
34 changes: 31 additions & 3 deletions src/Weasel.Core/Migrations/DatabaseBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ public string ToDatabaseScript()
.ToArray();
var writer = new StringWriter();

applyPostProcessingIfAny();

Migrator.WriteScript(writer, (m, w) =>
{
m.WriteSchemaCreationSql(schemaNames, writer);
Expand All @@ -123,6 +125,15 @@ public string ToDatabaseScript()
return writer.ToString();
}

private void applyPostProcessingIfAny()
{
var objects = AllObjects().ToArray();
foreach (var postProcessing in objects.OfType<ISchemaObjectWithPostProcessing>().ToArray())
{
postProcessing.PostProcess(objects);
}
}

public async Task WriteCreationScriptToFileAsync(string filename, CancellationToken ct = default)
{
var directory = Path.GetDirectoryName(filename);
Expand Down Expand Up @@ -152,6 +163,8 @@ private async Task initializeSchemaWithNewConnection(CancellationToken ct)
/// <param name="directory"></param>
public async Task WriteScriptsByTypeAsync(string directory, CancellationToken ct = default)
{
applyPostProcessingIfAny();

FileSystem.CleanDirectory(directory);

await initializeSchemaWithNewConnection(ct).ConfigureAwait(false);
Expand Down Expand Up @@ -195,6 +208,7 @@ await Migrator.WriteTemplatedFile(directory.AppendPath(scriptName), (m, w) =>

public async Task<SchemaMigration> CreateMigrationAsync(CancellationToken ct = default)
{
applyPostProcessingIfAny();
var objects = AllObjects().ToArray();

await using var conn = CreateConnection();
Expand Down Expand Up @@ -261,6 +275,11 @@ public async Task<SchemaPatchDifference> ApplyAllConfiguredChangesToDatabaseAsyn
Migrator.AssertValidIdentifier(objectName.Name);
}

foreach (var postProcessing in objects.OfType<ISchemaObjectWithPostProcessing>().ToArray())
{
postProcessing.PostProcess(objects);
}

TConnection? conn = null;
try
{
Expand Down Expand Up @@ -400,10 +419,11 @@ private async ValueTask ensureStorageExistsAsync(
await ensureStorageExistsAsync(types, dependentType, token).ConfigureAwait(false);
}

await generateOrUpdateFeature(featureType, feature, token).ConfigureAwait(false);
await generateOrUpdateFeature(featureType, feature, token, false).ConfigureAwait(false);
}

protected async ValueTask generateOrUpdateFeature(Type featureType, IFeatureSchema feature, CancellationToken token)
protected async ValueTask generateOrUpdateFeature(Type featureType, IFeatureSchema feature, CancellationToken token,
bool skipPostProcessing)
{
if (_checks.ContainsKey(featureType))
{
Expand All @@ -413,12 +433,20 @@ protected async ValueTask generateOrUpdateFeature(Type featureType, IFeatureSche

var schemaObjects = feature.Objects;


foreach (var objectName in schemaObjects.SelectMany(x => x.AllNames()))
{
Migrator.AssertValidIdentifier(objectName.Name);
}

if (!skipPostProcessing)
{
var allObjects = AllObjects().ToArray();
foreach (var processing in schemaObjects.OfType<ISchemaObjectWithPostProcessing>().ToArray())
{
processing.PostProcess(allObjects);
}
}

using (await _migrateLocker.Lock(5.Seconds(), token).ConfigureAwait(false))
{
if (_checks.ContainsKey(featureType))
Expand Down
31 changes: 31 additions & 0 deletions src/Weasel.Postgresql/Tables/ForeignKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,35 @@ public void WriteDropStatement(Table parent, TextWriter writer)
{
writer.WriteLine($"ALTER TABLE {parent.Identifier} DROP CONSTRAINT IF EXISTS {Name};");
}

public void TryToCorrectForLink(Table parentTable, Table linkedTable)
{
// Depends on "id" always being first in Marten world
LinkedNames = linkedTable.PrimaryKeyColumns.ToArray();
if (ColumnNames.Length != LinkedNames.Length)
{
// Leave the first column alone!
for (int i = 1; i < LinkedNames.Length; i++)
{
var columnName = LinkedNames[i];
var matching = parentTable.ColumnFor(columnName);
if (matching != null)
{
ColumnNames = ColumnNames.Concat([columnName]).ToArray();
}
else
{
throw new InvalidForeignKeyException(
$"Cannot make a foreign key relationship from {parentTable.Identifier}({ColumnNames.Join(", ")}) to {linkedTable.Identifier}({LinkedNames.Join(", ")}) ");
}
}
}
}
}

public class InvalidForeignKeyException: Exception
{
public InvalidForeignKeyException(string? message) : base(message)
{
}
}
17 changes: 16 additions & 1 deletion src/Weasel.Postgresql/Tables/Table.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using System.Globalization;
using JasperFx.Core;
using Npgsql;
Expand All @@ -7,7 +8,7 @@

namespace Weasel.Postgresql.Tables;

public partial class Table: ISchemaObject
public partial class Table: ISchemaObjectWithPostProcessing
{
private readonly List<TableColumn> _columns = new();

Expand Down Expand Up @@ -163,6 +164,20 @@ public IEnumerable<DbObjectName> AllNames()
foreach (var fk in ForeignKeys) yield return new PostgresqlObjectName(Identifier.Schema, fk.Name);
}

public void PostProcess(ISchemaObject[] allObjects)
{
if (!ForeignKeys.Any()) return;

foreach (var key in ForeignKeys)
{
var matching = allObjects.OfType<Table>().FirstOrDefault(x => x.Identifier.Equals(key.LinkedTable));
if (matching != null)
{
key.TryToCorrectForLink(this, matching);
}
}
}

/// <summary>
/// Mutate this table to change the identifier to being in a different schema
/// </summary>
Expand Down

0 comments on commit c3fc7df

Please sign in to comment.