Skip to content

Commit 10a8f79

Browse files
committed
Re: supabase-community/supabase-csharp#81 - Clarifies ReferenceAttribute by changing shouldFilterTopLevel to useInnerJoin and adds an additional constructor for ReferenceAttribute with a shortcut for specifying the JoinType
1 parent aa2b11d commit 10a8f79

File tree

3 files changed

+179
-144
lines changed

3 files changed

+179
-144
lines changed

Postgrest/Attributes/ReferenceAttribute.cs

Lines changed: 176 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -5,146 +5,181 @@
55
using Postgrest.Exceptions;
66
using Postgrest.Extensions;
77
using Postgrest.Models;
8+
89
namespace Postgrest.Attributes
910
{
10-
11-
/// <summary>
12-
/// Used to specify that a foreign key relationship exists in PostgreSQL
13-
///
14-
/// See: https://postgrest.org/en/stable/api.html#resource-embedding
15-
/// </summary>
16-
[AttributeUsage(AttributeTargets.Property)]
17-
public class ReferenceAttribute : Attribute
18-
{
19-
/// <summary>
20-
/// Type of the model referenced
21-
/// </summary>
22-
public Type Model { get; }
23-
24-
/// <summary>
25-
/// Associated property name
26-
/// </summary>
27-
public string PropertyName { get; private set; }
28-
29-
/// <summary>
30-
/// Table name of model
31-
/// </summary>
32-
public string TableName { get; }
33-
34-
/// <summary>
35-
/// Columns that exist on the model we will select from.
36-
/// </summary>
37-
public List<string> Columns { get; private set; } = new();
38-
39-
/// <summary>
40-
/// If the performed query is an Insert or Upsert, should this value be ignored? (DEFAULT TRUE)
41-
/// </summary>
42-
public bool IgnoreOnInsert { get; private set; }
43-
44-
/// <summary>
45-
/// If the performed query is an Update, should this value be ignored? (DEFAULT TRUE)
46-
/// </summary>
47-
public bool IgnoreOnUpdate { get; private set; }
48-
49-
/// <summary>
50-
/// If Reference should automatically be included in queries on this reference. (DEFAULT TRUE)
51-
/// </summary>
52-
public bool IncludeInQuery { get; }
53-
54-
/// <summary>
55-
/// As to whether the query will filter top-level rows.
56-
///
57-
/// See: https://postgrest.org/en/stable/api.html#resource-embedding
58-
/// </summary>
59-
public bool ShouldFilterTopLevel { get; }
60-
61-
/// <param name="model">Model referenced</param>
62-
/// <param name="includeInQuery">Should referenced be included in queries?</param>
63-
/// <param name="ignoreOnInsert">Should reference data be excluded from inserts/upserts?</param>
64-
/// <param name="ignoreOnUpdate">Should reference data be excluded from updates?</param>
65-
/// <param name="shouldFilterTopLevel">As to whether the query will filter top-level rows.</param>
66-
/// <param name="propertyName"></param>
67-
/// <exception cref="Exception"></exception>
68-
public ReferenceAttribute(Type model, bool includeInQuery = true, bool ignoreOnInsert = true, bool ignoreOnUpdate = true, bool shouldFilterTopLevel = true,
69-
[CallerMemberName] string propertyName = "")
70-
{
71-
if (!IsDerivedFromBaseModel(model))
72-
{
73-
throw new PostgrestException("ReferenceAttribute must be used with Postgrest BaseModels.") { Reason = FailureHint.Reason.InvalidArgument };
74-
}
75-
76-
Model = model;
77-
IncludeInQuery = includeInQuery;
78-
IgnoreOnInsert = ignoreOnInsert;
79-
IgnoreOnUpdate = ignoreOnUpdate;
80-
PropertyName = propertyName;
81-
ShouldFilterTopLevel = shouldFilterTopLevel;
82-
83-
var attr = GetCustomAttribute(model, typeof(TableAttribute));
84-
TableName = attr is TableAttribute tableAttr ? tableAttr.Name : model.Name;
85-
}
86-
87-
internal void ParseProperties(List<ReferenceAttribute>? seenRefs = null)
88-
{
89-
seenRefs ??= new List<ReferenceAttribute>();
90-
91-
ParseColumns();
92-
ParseRelationships(seenRefs);
93-
}
94-
95-
private void ParseColumns()
96-
{
97-
foreach (var property in Model.GetProperties())
98-
{
99-
var attrs = property.GetCustomAttributes(true);
100-
101-
foreach (var item in attrs)
102-
{
103-
switch (item)
104-
{
105-
case ColumnAttribute columnAttribute:
106-
Columns.Add(columnAttribute.ColumnName);
107-
break;
108-
case PrimaryKeyAttribute primaryKeyAttribute:
109-
Columns.Add(primaryKeyAttribute.ColumnName);
110-
break;
111-
}
112-
}
113-
}
114-
}
115-
116-
/// <inheritdoc />
117-
public override bool Equals(object obj)
118-
{
119-
if (obj is ReferenceAttribute attribute)
120-
{
121-
return TableName == attribute.TableName && PropertyName == attribute.PropertyName && Model == attribute.Model;
122-
}
123-
124-
return false;
125-
}
126-
127-
private void ParseRelationships(List<ReferenceAttribute> seenRefs)
128-
{
129-
foreach (var property in Model.GetProperties())
130-
{
131-
var attrs = property.GetCustomAttributes(true);
132-
133-
foreach (var attr in attrs)
134-
{
135-
if (attr is not ReferenceAttribute { IncludeInQuery: true } refAttr) continue;
136-
137-
if (seenRefs.FirstOrDefault(r => r.Equals(refAttr)) != null) continue;
138-
139-
seenRefs.Add(refAttr);
140-
refAttr.ParseProperties(seenRefs);
141-
142-
Columns.Add(ShouldFilterTopLevel ? $"{refAttr.TableName}!inner({string.Join(",", refAttr.Columns.ToArray())})" : $"{refAttr.TableName}({string.Join(",", refAttr.Columns.ToArray())})");
143-
}
144-
}
145-
}
146-
147-
private static bool IsDerivedFromBaseModel(Type type) =>
148-
type.GetInheritanceHierarchy().Any(t => t == typeof(BaseModel));
149-
}
150-
}
11+
/// <summary>
12+
/// Used to specify that a foreign key relationship exists in PostgreSQL
13+
///
14+
/// See: https://postgrest.org/en/stable/api.html#resource-embedding
15+
/// </summary>
16+
[AttributeUsage(AttributeTargets.Property)]
17+
public class ReferenceAttribute : Attribute
18+
{
19+
/// <summary>
20+
/// Specifies the Join type on this reference. PostgREST only allows for a LEFT join and an INNER join.
21+
/// </summary>
22+
public enum JoinType
23+
{
24+
/// <summary>
25+
/// INNER JOIN: returns rows when there is a match on both the source and the referenced tables.
26+
/// </summary>
27+
Inner,
28+
29+
/// <summary>
30+
/// LEFT JOIN: returns all rows from the source table, even if there are no matches in the referenced table
31+
/// </summary>
32+
Left
33+
}
34+
35+
/// <summary>
36+
/// Type of the model referenced
37+
/// </summary>
38+
public Type Model { get; }
39+
40+
/// <summary>
41+
/// Associated property name
42+
/// </summary>
43+
public string PropertyName { get; private set; }
44+
45+
/// <summary>
46+
/// Table name of model
47+
/// </summary>
48+
public string TableName { get; }
49+
50+
/// <summary>
51+
/// Columns that exist on the model we will select from.
52+
/// </summary>
53+
public List<string> Columns { get; private set; } = new();
54+
55+
/// <summary>
56+
/// If the performed query is an Insert or Upsert, should this value be ignored? (DEFAULT TRUE)
57+
/// </summary>
58+
public bool IgnoreOnInsert { get; private set; }
59+
60+
/// <summary>
61+
/// If the performed query is an Update, should this value be ignored? (DEFAULT TRUE)
62+
/// </summary>
63+
public bool IgnoreOnUpdate { get; private set; }
64+
65+
/// <summary>
66+
/// If Reference should automatically be included in queries on this reference. (DEFAULT TRUE)
67+
/// </summary>
68+
public bool IncludeInQuery { get; }
69+
70+
/// <summary>
71+
/// As to whether the query will filter top-level rows.
72+
///
73+
/// See: https://postgrest.org/en/stable/api.html#resource-embedding
74+
/// </summary>
75+
public bool UseInnerJoin { get; }
76+
77+
/// <summary>Establishes a reference between two tables</summary>
78+
/// <param name="model">Model referenced</param>
79+
/// <param name="includeInQuery">Should referenced be included in queries?</param>
80+
/// <param name="ignoreOnInsert">Should reference data be excluded from inserts/upserts?</param>
81+
/// <param name="ignoreOnUpdate">Should reference data be excluded from updates?</param>
82+
/// <param name="joinType">Specifies the join type for this relationship</param>
83+
/// <param name="propertyName"></param>
84+
/// <exception cref="Exception"></exception>
85+
public ReferenceAttribute(Type model, JoinType joinType, bool includeInQuery = true, bool ignoreOnInsert = true,
86+
bool ignoreOnUpdate = true, [CallerMemberName] string propertyName = "")
87+
: this(model, includeInQuery, ignoreOnInsert, ignoreOnUpdate, joinType == JoinType.Inner, propertyName)
88+
{
89+
}
90+
91+
/// <summary>Establishes a reference between two tables</summary>
92+
/// <param name="model">Model referenced</param>
93+
/// <param name="includeInQuery">Should referenced be included in queries?</param>
94+
/// <param name="ignoreOnInsert">Should reference data be excluded from inserts/upserts?</param>
95+
/// <param name="ignoreOnUpdate">Should reference data be excluded from updates?</param>
96+
/// <param name="useInnerJoin">As to whether the query will filter top-level rows.</param>
97+
/// <param name="propertyName"></param>
98+
/// <exception cref="Exception"></exception>
99+
public ReferenceAttribute(Type model, bool includeInQuery = true, bool ignoreOnInsert = true,
100+
bool ignoreOnUpdate = true, bool useInnerJoin = true,
101+
[CallerMemberName] string propertyName = "")
102+
{
103+
if (!IsDerivedFromBaseModel(model))
104+
throw new PostgrestException("ReferenceAttribute must be used with Postgrest BaseModels.")
105+
{ Reason = FailureHint.Reason.InvalidArgument };
106+
107+
Model = model;
108+
IncludeInQuery = includeInQuery;
109+
IgnoreOnInsert = ignoreOnInsert;
110+
IgnoreOnUpdate = ignoreOnUpdate;
111+
PropertyName = propertyName;
112+
UseInnerJoin = useInnerJoin;
113+
114+
var attr = GetCustomAttribute(model, typeof(TableAttribute));
115+
TableName = attr is TableAttribute tableAttr ? tableAttr.Name : model.Name;
116+
}
117+
118+
internal void ParseProperties(List<ReferenceAttribute>? seenRefs = null)
119+
{
120+
seenRefs ??= new List<ReferenceAttribute>();
121+
122+
ParseColumns();
123+
ParseRelationships(seenRefs);
124+
}
125+
126+
private void ParseColumns()
127+
{
128+
foreach (var property in Model.GetProperties())
129+
{
130+
var attrs = property.GetCustomAttributes(true);
131+
132+
foreach (var item in attrs)
133+
{
134+
switch (item)
135+
{
136+
case ColumnAttribute columnAttribute:
137+
Columns.Add(columnAttribute.ColumnName);
138+
break;
139+
case PrimaryKeyAttribute primaryKeyAttribute:
140+
Columns.Add(primaryKeyAttribute.ColumnName);
141+
break;
142+
}
143+
}
144+
}
145+
}
146+
147+
/// <inheritdoc />
148+
public override bool Equals(object obj)
149+
{
150+
if (obj is ReferenceAttribute attribute)
151+
{
152+
return TableName == attribute.TableName && PropertyName == attribute.PropertyName &&
153+
Model == attribute.Model;
154+
}
155+
156+
return false;
157+
}
158+
159+
160+
private void ParseRelationships(List<ReferenceAttribute> seenRefs)
161+
{
162+
foreach (var property in Model.GetProperties())
163+
{
164+
var attrs = property.GetCustomAttributes(true);
165+
166+
foreach (var attr in attrs)
167+
{
168+
if (attr is not ReferenceAttribute { IncludeInQuery: true } refAttr) continue;
169+
170+
if (seenRefs.FirstOrDefault(r => r.Equals(refAttr)) != null) continue;
171+
172+
seenRefs.Add(refAttr);
173+
refAttr.ParseProperties(seenRefs);
174+
175+
Columns.Add(UseInnerJoin
176+
? $"{refAttr.TableName}!inner({string.Join(",", refAttr.Columns.ToArray())})"
177+
: $"{refAttr.TableName}({string.Join(",", refAttr.Columns.ToArray())})");
178+
}
179+
}
180+
}
181+
182+
private static bool IsDerivedFromBaseModel(Type type) =>
183+
type.GetInheritanceHierarchy().Any(t => t == typeof(BaseModel));
184+
}
185+
}

Postgrest/Table.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ public string GenerateUrl()
615615

616616
var columns = string.Join(",", reference.Columns.ToArray());
617617

618-
if (reference.ShouldFilterTopLevel)
618+
if (reference.UseInnerJoin)
619619
query["select"] += $",{reference.TableName}!inner({columns})";
620620
else
621621
query["select"] += $",{reference.TableName}({columns})";

PostgrestTests/Models/LinkedModels.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ namespace PostgrestTests.Models;
88
[Table("movie")]
99
public class Movie : BaseModel
1010
{
11-
[PrimaryKey("id", false)] public string Id { get; set; }
11+
[PrimaryKey("id")] public string Id { get; set; }
1212

1313
[Column("name")] public string? Name { get; set; }
1414

15-
[Reference(typeof(Person), shouldFilterTopLevel: false)]
15+
[Reference(typeof(Person), ReferenceAttribute.JoinType.Left)]
1616
public List<Person> People { get; set; } = new();
1717

1818
[Column("created_at")] public DateTime CreatedAt { get; set; }

0 commit comments

Comments
 (0)