Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<EFCoreVersion>9.0.0-preview.1.24081.2</EFCoreVersion>
<MicrosoftExtensionsVersion>9.0.0-preview.1.24080.9</MicrosoftExtensionsVersion>
<EFCoreVersion>9.0.0-preview.2.24128.4</EFCoreVersion>
<MicrosoftExtensionsVersion>9.0.0-preview.2.24128.5</MicrosoftExtensionsVersion>
<NpgsqlVersion>8.0.2</NpgsqlVersion>
</PropertyGroup>

Expand Down
62 changes: 62 additions & 0 deletions src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,68 @@ protected override void Generate(RenameSequenceOperation operation, IModel? mode
EndStatement(builder);
}

/// <inheritdoc />
protected override void SequenceOptions(
string? schema,
string name,
SequenceOperation operation,
IModel? model,
MigrationCommandListBuilder builder,
bool forAlter)
{
var intTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(int));
var longTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(long));

builder
.Append(" INCREMENT BY ")
.Append(intTypeMapping.GenerateSqlLiteral(operation.IncrementBy));

if (operation.MinValue != null)
{
builder
.Append(" MINVALUE ")
.Append(longTypeMapping.GenerateSqlLiteral(operation.MinValue));
}
else if (forAlter)
{
builder
.Append(" NO MINVALUE");
}

if (operation.MaxValue != null)
{
builder
.Append(" MAXVALUE ")
.Append(longTypeMapping.GenerateSqlLiteral(operation.MaxValue));
}
else if (forAlter)
{
builder
.Append(" NO MAXVALUE");
}

builder.Append(operation.IsCyclic ? " CYCLE" : " NO CYCLE");

if (!operation.IsCached)
{
// The base implementation appends NO CACHE, which isn't supported by PG
builder
.Append(" CACHE 1");
}
else if (operation.CacheSize != null)
{
builder
.Append(" CACHE ")
.Append(intTypeMapping.GenerateSqlLiteral(operation.CacheSize.Value));
}
else if (forAlter)
{
// The base implementation just appends CACHE, which isn't supported by PG
builder
.Append(" CACHE 1");
}
}

/// <inheritdoc />
protected override void Generate(RestartSequenceOperation operation, IModel? model, MigrationCommandListBuilder builder)
{
Expand Down
125 changes: 96 additions & 29 deletions src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

namespace Npgsql.EntityFrameworkCore.PostgreSQL.Scaffolding.Internal;

// ReSharper disable StringLiteralTypo

/// <summary>
/// The default database model factory for Npgsql.
/// </summary>
Expand Down Expand Up @@ -458,7 +460,7 @@ ORDER BY attnum
MaxValue = seqInfo.MaxValue,
IncrementBy = (int)(seqInfo.IncrementBy ?? 1),
IsCyclic = seqInfo.IsCyclic ?? false,
NumbersToCache = seqInfo.NumbersToCache ?? 1
NumbersToCache = seqInfo.CacheSize ?? 1
};

if (!sequenceData.Equals(IdentitySequenceOptionsData.Empty))
Expand Down Expand Up @@ -976,11 +978,67 @@ private static IEnumerable<DatabaseSequence> GetSequences(
Func<string, string>? schemaFilter,
IDiagnosticsLogger<DbLoggerCategory.Scaffolding> logger)
{
// Note: we consult information_schema.sequences instead of pg_sequence but the latter was only introduced in PG 10
var commandText = $"""
// pg_sequence was only introduced in PG 10; we prefer that (cleaner and also exposes sequence caching info), but retain the old
// code for backwards compat
return connection.PostgreSqlVersion >= new Version(10, 0)
? GetSequencesNew(connection, databaseModel, schemaFilter, logger)
: GetSequencesOld(connection, databaseModel, schemaFilter, logger);

static IEnumerable<DatabaseSequence> GetSequencesNew(
NpgsqlConnection connection,
DatabaseModel databaseModel,
Func<string, string>? schemaFilter,
IDiagnosticsLogger<DbLoggerCategory.Scaffolding> logger)
{
var commandText = $"""
SELECT nspname, relname, typname, seqstart, seqincrement, seqmax, seqmin, seqcache, seqcycle
FROM pg_sequence
JOIN pg_class AS cls ON cls.oid=seqrelid
JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace
JOIN pg_type AS typ ON typ.oid = seqtypid
/* Filter out owned serial and identity sequences */
WHERE NOT EXISTS (SELECT * FROM pg_depend AS dep WHERE dep.objid = cls.oid AND dep.deptype IN ('i', 'I', 'a'))
{(schemaFilter is not null ? $"AND {schemaFilter("nspname")}" : null)}
""";

using var command = new NpgsqlCommand(commandText, connection);
using var reader = command.ExecuteReader();

foreach (var record in reader.Cast<DbDataRecord>())
{
var sequenceSchema = reader.GetFieldValue<string>("nspname");
var sequenceName = reader.GetFieldValue<string>("relname");

var seqInfo = ReadSequenceInfo(record, connection.PostgreSqlVersion);
var sequence = new DatabaseSequence
{
Database = databaseModel,
Name = sequenceName,
Schema = sequenceSchema,
StoreType = seqInfo.StoreType,
StartValue = seqInfo.StartValue,
MinValue = seqInfo.MinValue,
MaxValue = seqInfo.MaxValue,
IncrementBy = (int?)seqInfo.IncrementBy,
IsCyclic = seqInfo.IsCyclic,
IsCached = seqInfo.CacheSize is not null,
CacheSize = seqInfo.CacheSize
};

yield return sequence;
}
}

static IEnumerable<DatabaseSequence> GetSequencesOld(
NpgsqlConnection connection,
DatabaseModel databaseModel,
Func<string, string>? schemaFilter,
IDiagnosticsLogger<DbLoggerCategory.Scaffolding> logger)
{
var commandText = $"""
SELECT
sequence_schema, sequence_name,
data_type AS seqtype,
data_type AS typname,
{(connection.PostgreSqlVersion >= new Version(9, 1) ? "start_value" : "1")}::bigint AS seqstart,
minimum_value::bigint AS seqmin,
maximum_value::bigint AS seqmax,
Expand All @@ -1001,29 +1059,30 @@ AND NOT EXISTS (SELECT * FROM pg_depend AS dep WHERE dep.objid = cls.oid AND dep
{(schemaFilter is not null ? $"AND {schemaFilter("nspname")}" : null)}
""";

using var command = new NpgsqlCommand(commandText, connection);
using var reader = command.ExecuteReader();

foreach (var record in reader.Cast<DbDataRecord>())
{
var sequenceName = reader.GetFieldValue<string>("sequence_name");
var sequenceSchema = reader.GetFieldValue<string>("sequence_schema");
using var command = new NpgsqlCommand(commandText, connection);
using var reader = command.ExecuteReader();

var seqInfo = ReadSequenceInfo(record, connection.PostgreSqlVersion);
var sequence = new DatabaseSequence
foreach (var record in reader.Cast<DbDataRecord>())
{
Database = databaseModel,
Name = sequenceName,
Schema = sequenceSchema,
StoreType = seqInfo.StoreType,
StartValue = seqInfo.StartValue,
MinValue = seqInfo.MinValue,
MaxValue = seqInfo.MaxValue,
IncrementBy = (int?)seqInfo.IncrementBy,
IsCyclic = seqInfo.IsCyclic
};

yield return sequence;
var sequenceName = reader.GetFieldValue<string>("sequence_name");
var sequenceSchema = reader.GetFieldValue<string>("sequence_schema");

var seqInfo = ReadSequenceInfo(record, connection.PostgreSqlVersion);
var sequence = new DatabaseSequence
{
Database = databaseModel,
Name = sequenceName,
Schema = sequenceSchema,
StoreType = seqInfo.StoreType,
StartValue = seqInfo.StartValue,
MinValue = seqInfo.MinValue,
MaxValue = seqInfo.MaxValue,
IncrementBy = (int?)seqInfo.IncrementBy,
IsCyclic = seqInfo.IsCyclic
};

yield return sequence;
}
}
}

Expand Down Expand Up @@ -1225,16 +1284,24 @@ private static void AdjustDefaults(DatabaseColumn column, string systemTypeName)

private static SequenceInfo ReadSequenceInfo(DbDataRecord record, Version postgresVersion)
{
var storeType = record.GetFieldValue<string>("seqtype");
var storeType = record.GetFieldValue<string>("typname");
var startValue = record.GetValueOrDefault<long>("seqstart");
var minValue = record.GetValueOrDefault<long>("seqmin");
var maxValue = record.GetValueOrDefault<long>("seqmax");
var incrementBy = record.GetValueOrDefault<long>("seqincrement");
var isCyclic = record.GetValueOrDefault<bool>("seqcycle");
var numbersToCache = (int)record.GetValueOrDefault<long>("seqcache");
var cacheSize = (int?)record.GetValueOrDefault<long>("seqcache");

long defaultStart, defaultMin, defaultMax;

storeType = storeType switch
{
"int2" => "smallint",
"int4" => "integer",
"int8" => "bigint",
_ => storeType
};

switch (storeType)
{
case "smallint" when incrementBy > 0:
Expand Down Expand Up @@ -1293,7 +1360,7 @@ private static SequenceInfo ReadSequenceInfo(DbDataRecord record, Version postgr
MaxValue = maxValue == defaultMax ? null : maxValue,
IncrementBy = incrementBy == 1 ? null : incrementBy,
IsCyclic = isCyclic == false ? null : true,
NumbersToCache = numbersToCache == 1 ? null : numbersToCache
CacheSize = cacheSize is 1 or null ? null : cacheSize
};
}

Expand All @@ -1305,7 +1372,7 @@ private sealed class SequenceInfo(string storeType)
public long? MaxValue { get; set; }
public long? IncrementBy { get; set; }
public bool? IsCyclic { get; set; }
public long? NumbersToCache { get; set; }
public int? CacheSize { get; set; }
}

#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ public interface INpgsqlSequenceValueGeneratorFactory
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
ValueGenerator Create(
ValueGenerator? TryCreate(
IProperty property,
Type clrType,
NpgsqlSequenceValueGeneratorState generatorState,
INpgsqlRelationalConnection connection,
IRawSqlCommandBuilder rawSqlCommandBuilder,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@ public NpgsqlSequenceValueGeneratorFactory(
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual ValueGenerator Create(
public virtual ValueGenerator? TryCreate(
IProperty property,
Type type,
NpgsqlSequenceValueGeneratorState generatorState,
INpgsqlRelationalConnection connection,
IRawSqlCommandBuilder rawSqlCommandBuilder,
IRelationalCommandDiagnosticsLogger commandLogger)
{
var type = property.ClrType.UnwrapNullableType().UnwrapEnumType();

if (type == typeof(long))
{
return new NpgsqlSequenceHiLoValueGenerator<long>(
Expand Down Expand Up @@ -89,8 +88,6 @@ public virtual ValueGenerator Create(
rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger);
}

throw new ArgumentException(
CoreStrings.InvalidValueGeneratorFactoryProperty(
nameof(NpgsqlSequenceValueGeneratorFactory), property.Name, property.DeclaringType.DisplayName()));
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,50 @@ public NpgsqlValueGeneratorSelector(
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override ValueGenerator Select(IProperty property, ITypeBase typeBase)
=> property.GetValueGeneratorFactory() is null
&& property.GetValueGenerationStrategy() == NpgsqlValueGenerationStrategy.SequenceHiLo
? _sequenceFactory.Create(
property,
Cache.GetOrAddSequenceState(property, _connection),
_connection,
_rawSqlCommandBuilder,
_commandLogger)
: base.Select(property, typeBase);
public override bool TrySelect(IProperty property, ITypeBase typeBase, out ValueGenerator? valueGenerator)
{
if (property.GetValueGeneratorFactory() != null
|| property.GetValueGenerationStrategy() != NpgsqlValueGenerationStrategy.SequenceHiLo)
{
return base.TrySelect(property, typeBase, out valueGenerator);
}

var propertyType = property.ClrType.UnwrapNullableType().UnwrapEnumType();

valueGenerator = _sequenceFactory.TryCreate(
property,
propertyType,
Cache.GetOrAddSequenceState(property, _connection),
_connection,
_rawSqlCommandBuilder,
_commandLogger);

if (valueGenerator != null)
{
return true;
}

var converter = property.GetTypeMapping().Converter;
if (converter != null
&& converter.ProviderClrType != propertyType)
{
valueGenerator = _sequenceFactory.TryCreate(
property,
converter.ProviderClrType,
Cache.GetOrAddSequenceState(property, _connection),
_connection,
_rawSqlCommandBuilder,
_commandLogger);

if (valueGenerator != null)
{
valueGenerator = valueGenerator.WithConverter(converter);
return true;
}
}

return false;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates;

public class NorthwindBulkUpdatesNpgsqlFixture<TModelCustomizer> : NorthwindBulkUpdatesFixture<TModelCustomizer>
where TModelCustomizer : IModelCustomizer, new()
where TModelCustomizer : ITestModelCustomizer, new()
{
protected override ITestStoreFactory TestStoreFactory
=> NpgsqlNorthwindTestStoreFactory.Instance;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL;
public class ManyToManyFieldsLoadNpgsqlTest(ManyToManyFieldsLoadNpgsqlTest.ManyToManyFieldsLoadNpgsqlFixture fixture)
: ManyToManyFieldsLoadTestBase<ManyToManyFieldsLoadNpgsqlTest.ManyToManyFieldsLoadNpgsqlFixture>(fixture)
{
public class ManyToManyFieldsLoadNpgsqlFixture : ManyToManyFieldsLoadFixtureBase
public class ManyToManyFieldsLoadNpgsqlFixture : ManyToManyFieldsLoadFixtureBase, ITestSqlLoggerFactory
{
public TestSqlLoggerFactory TestSqlLoggerFactory
=> (TestSqlLoggerFactory)ListLoggerFactory;
Expand Down
2 changes: 1 addition & 1 deletion test/EFCore.PG.FunctionalTests/ManyToManyLoadNpgsqlTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL;
public class ManyToManyLoadNpgsqlTest(ManyToManyLoadNpgsqlTest.ManyToManyLoadNpgsqlFixture fixture)
: ManyToManyLoadTestBase<ManyToManyLoadNpgsqlTest.ManyToManyLoadNpgsqlFixture>(fixture)
{
public class ManyToManyLoadNpgsqlFixture : ManyToManyLoadFixtureBase
public class ManyToManyLoadNpgsqlFixture : ManyToManyLoadFixtureBase, ITestSqlLoggerFactory
{
public TestSqlLoggerFactory TestSqlLoggerFactory
=> (TestSqlLoggerFactory)ListLoggerFactory;
Expand Down
Loading