Skip to content

Commit fefaaec

Browse files
Schema enums follow up (part 2) (#685)
* Follow-up to schema-enums (605, 724) - Refactors `PostgresEnum` to properly handle schema qualification without breaking the standard patterns for migration operations. - Late-access default schema (SQL generation time). - Return `IReadOnlyList<string>` for `PostgresEnum.Labels` since items of that collection are not writable (i.e. returns a copy from the annotation value). - Match the semantics of `Sequence.cs` such that `PostgresEnum.Schema` returns the annotation schema when it is not null, otherwise null indicates the default schema.
1 parent 053142c commit fefaaec

3 files changed

Lines changed: 121 additions & 44 deletions

File tree

src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public override MethodCallCodeFragment GenerateFluentApi(IModel model, IAnnotati
7575
extension.Name);
7676
}
7777

78-
if (annotation.Name.StartsWith(NpgsqlAnnotationNames.EnumPrefix))
78+
if (annotation.Name.StartsWith(NpgsqlAnnotationNames.EnumPrefix, StringComparison.Ordinal))
7979
{
8080
var enumTypeDef = new PostgresEnum(model, annotation.Name);
8181

src/EFCore.PG/Metadata/PostgresEnum.cs

Lines changed: 113 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,102 +9,182 @@
99

1010
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata
1111
{
12+
/// <summary>
13+
/// Represents the metadata for a PostgreSQL enum.
14+
/// </summary>
15+
[PublicAPI]
1216
public class PostgresEnum
1317
{
14-
readonly IAnnotatable _annotatable;
15-
readonly string _annotationName;
16-
17-
internal PostgresEnum(IAnnotatable annotatable, string annotationName)
18+
[NotNull] readonly IAnnotatable _annotatable;
19+
[NotNull] readonly string _annotationName;
20+
21+
/// <summary>
22+
/// Creates a <see cref="PostgresEnum"/>.
23+
/// </summary>
24+
/// <param name="annotatable">The annotatable to search for the annotation.</param>
25+
/// <param name="annotationName">The annotation name to search for in the annotatable.</param>
26+
/// <exception cref="ArgumentNullException"><paramref name="annotatable"/></exception>
27+
/// <exception cref="ArgumentNullException"><paramref name="annotationName"/></exception>
28+
internal PostgresEnum([NotNull] IAnnotatable annotatable, [NotNull] string annotationName)
1829
{
19-
_annotatable = annotatable;
20-
_annotationName = annotationName;
30+
_annotatable = Check.NotNull(annotatable, nameof(annotatable));
31+
_annotationName = Check.NotNull(annotationName, nameof(annotationName));
2132
}
2233

34+
/// <summary>
35+
/// Gets or adds a <see cref="PostgresEnum"/> from or to the <see cref="IMutableAnnotatable"/>.
36+
/// </summary>
37+
/// <param name="annotatable">The annotatable from which to get or add the enum.</param>
38+
/// <param name="schema">The enum schema or null to use the model's default schema.</param>
39+
/// <param name="name">The enum name.</param>
40+
/// <param name="labels">The enum labels.</param>
41+
/// <returns>
42+
/// The <see cref="PostgresEnum"/> from the <see cref="IMutableAnnotatable"/>.
43+
/// </returns>
44+
/// <exception cref="ArgumentException"><paramref name="schema"/></exception>
45+
/// <exception cref="ArgumentNullException"><paramref name="annotatable"/></exception>
46+
/// <exception cref="ArgumentNullException"><paramref name="name"/></exception>
47+
/// <exception cref="ArgumentNullException"><paramref name="labels"/></exception>
48+
[NotNull]
2349
public static PostgresEnum GetOrAddPostgresEnum(
2450
[NotNull] IMutableAnnotatable annotatable,
2551
[CanBeNull] string schema,
2652
[NotNull] string name,
2753
[NotNull] string[] labels)
2854
{
55+
Check.NotNull(annotatable, nameof(annotatable));
56+
Check.NullButNotEmpty(schema, nameof(schema));
57+
Check.NotEmpty(name, nameof(name));
58+
Check.NotNull(labels, nameof(labels));
59+
2960
if (FindPostgresEnum(annotatable, schema, name) is PostgresEnum enumType)
3061
return enumType;
3162

32-
enumType = new PostgresEnum(annotatable, BuildAnnotationName(schema, name));
33-
enumType.SetData(labels);
34-
return enumType;
63+
var annotationName = BuildAnnotationName(schema, name);
64+
65+
return new PostgresEnum(annotatable, annotationName) { Labels = labels };
3566
}
3667

68+
/// <summary>
69+
/// Gets or adds a <see cref="PostgresEnum"/> from or to the <see cref="IMutableAnnotatable"/>.
70+
/// </summary>
71+
/// <param name="annotatable">The annotatable from which to get or add the enum.</param>
72+
/// <param name="name">The enum name.</param>
73+
/// <param name="labels">The enum labels.</param>
74+
/// <returns>
75+
/// The <see cref="PostgresEnum"/> from the <see cref="IMutableAnnotatable"/>.
76+
/// </returns>
77+
/// <exception cref="ArgumentNullException"><paramref name="annotatable"/></exception>
78+
/// <exception cref="ArgumentNullException"><paramref name="name"/></exception>
79+
/// <exception cref="ArgumentNullException"><paramref name="labels"/></exception>
80+
[NotNull]
3781
public static PostgresEnum GetOrAddPostgresEnum(
3882
[NotNull] IMutableAnnotatable annotatable,
3983
[NotNull] string name,
4084
[NotNull] string[] labels)
4185
=> GetOrAddPostgresEnum(annotatable, null, name, labels);
4286

87+
/// <summary>
88+
/// Finds a <see cref="PostgresEnum"/> in the <see cref="IAnnotatable"/>, or returns null if not found.
89+
/// </summary>
90+
/// <param name="annotatable">The annotatable to search for the enum.</param>
91+
/// <param name="schema">The enum schema or null to use the model's default schema.</param>
92+
/// <param name="name">The enum name.</param>
93+
/// <returns>
94+
/// The <see cref="PostgresEnum"/> from the <see cref="IAnnotatable"/>.
95+
/// </returns>
96+
/// <exception cref="ArgumentException"><paramref name="schema"/></exception>
97+
/// <exception cref="ArgumentNullException"><paramref name="annotatable"/></exception>
98+
/// <exception cref="ArgumentNullException"><paramref name="name"/></exception>
99+
[CanBeNull]
43100
public static PostgresEnum FindPostgresEnum(
44101
[NotNull] IAnnotatable annotatable,
45102
[CanBeNull] string schema,
46103
[NotNull] string name)
47104
{
48105
Check.NotNull(annotatable, nameof(annotatable));
106+
Check.NullButNotEmpty(schema, nameof(schema));
49107
Check.NotEmpty(name, nameof(name));
50108

51109
var annotationName = BuildAnnotationName(schema, name);
52110

53111
return annotatable[annotationName] == null ? null : new PostgresEnum(annotatable, annotationName);
54112
}
55113

114+
[NotNull]
56115
static string BuildAnnotationName(string schema, string name)
57-
=> NpgsqlAnnotationNames.EnumPrefix + (schema == null ? name : schema + '.' + name);
58-
116+
=> schema != null
117+
? $"{NpgsqlAnnotationNames.EnumPrefix}{schema}.{name}"
118+
: $"{NpgsqlAnnotationNames.EnumPrefix}{name}";
119+
120+
/// <summary>
121+
/// Gets the collection of <see cref="PostgresEnum"/> stored in the <see cref="IAnnotatable"/>.
122+
/// </summary>
123+
/// <param name="annotatable">The annotatable to search for <see cref="PostgresEnum"/> annotations.</param>
124+
/// <returns>
125+
/// The collection of <see cref="PostgresEnum"/> stored in the <see cref="IAnnotatable"/>.
126+
/// </returns>
127+
/// <exception cref="ArgumentNullException"><paramref name="annotatable"/></exception>
128+
[NotNull]
129+
[ItemNotNull]
59130
public static IEnumerable<PostgresEnum> GetPostgresEnums([NotNull] IAnnotatable annotatable)
60-
{
61-
Check.NotNull(annotatable, nameof(annotatable));
62-
63-
return annotatable.GetAnnotations()
64-
.Where(a => a.Name.StartsWith(NpgsqlAnnotationNames.EnumPrefix, StringComparison.Ordinal))
65-
.Select(a => new PostgresEnum(annotatable, a.Name));
66-
}
67-
131+
=> Check.NotNull(annotatable, nameof(annotatable))
132+
.GetAnnotations()
133+
.Where(a => a.Name.StartsWith(NpgsqlAnnotationNames.EnumPrefix, StringComparison.Ordinal))
134+
.Select(a => new PostgresEnum(annotatable, a.Name));
135+
136+
/// <summary>
137+
/// The <see cref="Annotatable"/> that stores the enum.
138+
/// </summary>
139+
[NotNull]
68140
public Annotatable Annotatable => (Annotatable)_annotatable;
69141

142+
/// <summary>
143+
/// The enum schema or null to represent the default schema.
144+
/// </summary>
145+
[CanBeNull]
70146
public string Schema => GetData().Schema;
71147

148+
/// <summary>
149+
/// The enum name.
150+
/// </summary>
151+
[NotNull]
72152
public string Name => GetData().Name;
73153

74-
public string[] Labels
154+
/// <summary>
155+
/// The enum labels.
156+
/// </summary>
157+
[NotNull]
158+
public IReadOnlyList<string> Labels
75159
{
76160
get => GetData().Labels;
77161
set => SetData(value);
78162
}
79163

80164
(string Schema, string Name, string[] Labels) GetData()
81-
{
82-
return !(Annotatable[_annotationName] is string annotationValue)
83-
? (null, null, null)
84-
: Deserialize(_annotationName, annotationValue);
85-
}
165+
=> Deserialize(Annotatable.FindAnnotation(_annotationName));
86166

87-
void SetData(string[] labels)
167+
void SetData([NotNull] IEnumerable<string> labels)
88168
=> Annotatable[_annotationName] = string.Join(",", labels);
89169

90-
static (string schema, string name, string[] labels) Deserialize(
91-
[NotNull] string annotationName,
92-
[NotNull] string annotationValue)
170+
static (string Schema, string Name, string[] Labels) Deserialize([CanBeNull] IAnnotation annotation)
93171
{
94-
Check.NotEmpty(annotationValue, nameof(annotationValue));
172+
if (annotation == null || !(annotation.Value is string value) || string.IsNullOrEmpty(value))
173+
return (null, null, null);
95174

96-
var labels = annotationValue.Split(',').ToArray();
175+
var labels = value.Split(',');
97176

177+
// TODO: This would be a safer operation if we stored schema and name in the annotation value (see Sequence.cs).
98178
// Yes, this doesn't support dots in the schema/enum name, let somebody complain first.
99-
var schemaAndName = annotationName.Substring(NpgsqlAnnotationNames.EnumPrefix.Length).Split('.');
179+
var schemaAndName = annotation.Name.Substring(NpgsqlAnnotationNames.EnumPrefix.Length).Split('.');
100180
switch (schemaAndName.Length)
101181
{
102182
case 1:
103183
return (null, schemaAndName[0], labels);
104184
case 2:
105185
return (schemaAndName[0], schemaAndName[1], labels);
106186
default:
107-
throw new ArgumentException("Cannot parse enum name from annotation: " + annotationName);
187+
throw new ArgumentException($"Cannot parse enum name from annotation: {annotation.Name}");
108188
}
109189
}
110190
}

src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,7 @@ protected override void Generate(
638638
MigrationCommandListBuilder builder)
639639
{
640640
Check.NotNull(operation, nameof(operation));
641+
Check.NotNull(model, nameof(model));
641642
Check.NotNull(builder, nameof(builder));
642643

643644
GenerateEnumStatements(operation, model, builder);
@@ -691,7 +692,7 @@ protected virtual void GenerateEnumStatements(
691692
foreach (var enumTypeToDrop in operation.Npgsql().OldPostgresEnums
692693
.Where(oe => operation.Npgsql().PostgresEnums.All(ne => ne.Name != oe.Name)))
693694
{
694-
GenerateDropEnum(enumTypeToDrop, operation.OldDatabase, builder);
695+
GenerateDropEnum(enumTypeToDrop, model, builder);
695696
}
696697

697698
// TODO: Some forms of enum alterations are actually supported...
@@ -708,7 +709,7 @@ protected virtual void GenerateCreateEnum(
708709
[NotNull] IModel model,
709710
[NotNull] MigrationCommandListBuilder builder)
710711
{
711-
var schema = GetSchemaOrDefault(enumType.Schema, model);
712+
var schema = enumType.Schema ?? model.Relational().DefaultSchema;
712713

713714
// Schemas are normally created (or rather ensured) by the model differ, which scans all tables, sequences
714715
// and other database objects. However, it isn't aware of enums, so we always ensure schema on enum creation.
@@ -723,10 +724,10 @@ protected virtual void GenerateCreateEnum(
723724
var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string));
724725

725726
var labels = enumType.Labels;
726-
for (var i = 0; i < labels.Length; i++)
727+
for (var i = 0; i < labels.Count; i++)
727728
{
728729
builder.Append(stringTypeMapping.GenerateSqlLiteral(labels[i]));
729-
if (i < labels.Length - 1)
730+
if (i < labels.Count - 1)
730731
builder.Append(", ");
731732
}
732733

@@ -735,10 +736,10 @@ protected virtual void GenerateCreateEnum(
735736

736737
protected virtual void GenerateDropEnum(
737738
[NotNull] PostgresEnum enumType,
738-
[CanBeNull] IAnnotatable oldDatabase,
739+
[NotNull] IModel model,
739740
[NotNull] MigrationCommandListBuilder builder)
740741
{
741-
var schema = GetSchemaOrDefault(enumType.Schema, oldDatabase);
742+
var schema = enumType.Schema ?? model.Relational().DefaultSchema;
742743

743744
builder
744745
.Append("DROP TYPE ")
@@ -1117,10 +1118,6 @@ static string GenerateStorageParameterValue(object value)
11171118

11181119
#region Helpers
11191120

1120-
[CanBeNull]
1121-
static string GetSchemaOrDefault([CanBeNull] string schema, [CanBeNull] IAnnotatable model)
1122-
=> schema ?? model?.FindAnnotation(RelationalAnnotationNames.DefaultSchema)?.Value as string;
1123-
11241121
/// <summary>
11251122
/// True if <see cref="_postgresVersion"/> is null, greater than, or equal to the specified version.
11261123
/// </summary>

0 commit comments

Comments
 (0)