Skip to content

Commit c3c39b3

Browse files
authored
Merge pull request supabase-community#60 from supabase-community/dev/linq
Add linq support for `Select`, `Where`, `OnConflict`, `Columns`, `Order`, `Update`, `Set`, and `Delete`
2 parents 15c3b34 + 07e7db0 commit c3c39b3

File tree

14 files changed

+2889
-1978
lines changed

14 files changed

+2889
-1978
lines changed
Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,56 @@
11
using Postgrest.Models;
22
using Postgrest.Responses;
33
using Supabase.Core.Interfaces;
4+
using System;
45
using System.Collections.Generic;
6+
using System.Linq.Expressions;
57
using System.Threading;
68
using System.Threading.Tasks;
79

810
namespace Postgrest.Interfaces
911
{
10-
public interface IPostgrestTable<T> : IGettableHeaders
11-
where T : BaseModel, new()
12-
{
13-
string BaseUrl { get; }
14-
string TableName { get; }
12+
public interface IPostgrestTable<T> : IGettableHeaders
13+
where T : BaseModel, new()
14+
{
15+
string BaseUrl { get; }
16+
string TableName { get; }
1517

16-
Table<T> And(List<QueryFilter> filters);
17-
void Clear();
18-
Table<T> Columns(string[] columns);
19-
Task<int> Count(Constants.CountType type, CancellationToken cancellationToken = default);
20-
Task Delete(QueryOptions? options = null, CancellationToken cancellationToken = default);
21-
Task<ModeledResponse<T>> Delete(T model, QueryOptions? options = null, CancellationToken cancellationToken = default);
22-
Table<T> Filter(string columnName, Constants.Operator op, object criterion);
23-
Task<ModeledResponse<T>> Get(CancellationToken cancellationToken = default);
24-
Task<ModeledResponse<T>> Insert(ICollection<T> models, QueryOptions? options = null, CancellationToken cancellationToken = default);
25-
Task<ModeledResponse<T>> Insert(T model, QueryOptions? options = null, CancellationToken cancellationToken = default);
26-
Table<T> Limit(int limit, string? foreignTableName = null);
27-
Table<T> Match(Dictionary<string, string> query);
28-
Table<T> Match(T model);
29-
Table<T> Not(QueryFilter filter);
30-
Table<T> Not(string columnName, Constants.Operator op, Dictionary<string, object> criteria);
31-
Table<T> Not(string columnName, Constants.Operator op, List<object> criteria);
32-
Table<T> Not(string columnName, Constants.Operator op, string criterion);
33-
Table<T> Offset(int offset, string? foreignTableName = null);
34-
Table<T> OnConflict(string columnName);
35-
Table<T> Or(List<QueryFilter> filters);
36-
Table<T> Order(string column, Constants.Ordering ordering, Constants.NullPosition nullPosition = Constants.NullPosition.First);
37-
Table<T> Order(string foreignTable, string column, Constants.Ordering ordering, Constants.NullPosition nullPosition = Constants.NullPosition.First);
38-
Table<T> Range(int from);
39-
Table<T> Range(int from, int to);
40-
Table<T> Select(string columnQuery);
41-
Task<T?> Single(CancellationToken cancellationToken = default);
42-
Task<ModeledResponse<T>> Update(T model, QueryOptions? options = null, CancellationToken cancellationToken = default);
43-
Task<ModeledResponse<T>> Upsert(ICollection<T> model, QueryOptions? options = null, CancellationToken cancellationToken = default);
44-
Task<ModeledResponse<T>> Upsert(T model, QueryOptions? options = null, CancellationToken cancellationToken = default);
45-
}
18+
Table<T> And(List<QueryFilter> filters);
19+
void Clear();
20+
Table<T> Columns(string[] columns);
21+
Table<T> Columns(Expression<Func<T, object[]>> predicate);
22+
Task<int> Count(Constants.CountType type, CancellationToken cancellationToken = default);
23+
Task Delete(QueryOptions? options = null, CancellationToken cancellationToken = default);
24+
Task<ModeledResponse<T>> Delete(T model, QueryOptions? options = null, CancellationToken cancellationToken = default);
25+
Table<T> Filter(string columnName, Constants.Operator op, object criterion);
26+
Table<T> Filter(Expression<Func<T, object>> predicate, Constants.Operator op, object criterion);
27+
Task<ModeledResponse<T>> Get(CancellationToken cancellationToken = default);
28+
Task<ModeledResponse<T>> Insert(ICollection<T> models, QueryOptions? options = null, CancellationToken cancellationToken = default);
29+
Task<ModeledResponse<T>> Insert(T model, QueryOptions? options = null, CancellationToken cancellationToken = default);
30+
Table<T> Limit(int limit, string? foreignTableName = null);
31+
Table<T> Match(Dictionary<string, string> query);
32+
Table<T> Match(T model);
33+
Table<T> Not(QueryFilter filter);
34+
Table<T> Not(string columnName, Constants.Operator op, Dictionary<string, object> criteria);
35+
Table<T> Not(string columnName, Constants.Operator op, List<object> criteria);
36+
Table<T> Not(string columnName, Constants.Operator op, string criterion);
37+
Table<T> Offset(int offset, string? foreignTableName = null);
38+
Table<T> OnConflict(string columnName);
39+
Table<T> OnConflict(Expression<Func<T, object>> predicate);
40+
Table<T> Or(List<QueryFilter> filters);
41+
Table<T> Order(string column, Constants.Ordering ordering, Constants.NullPosition nullPosition = Constants.NullPosition.First);
42+
Table<T> Order(Expression<Func<T, object>> predicate, Constants.Ordering ordering, Constants.NullPosition nullPosition = Constants.NullPosition.First);
43+
Table<T> Order(string foreignTable, string column, Constants.Ordering ordering, Constants.NullPosition nullPosition = Constants.NullPosition.First);
44+
Table<T> Range(int from);
45+
Table<T> Range(int from, int to);
46+
Table<T> Select(string columnQuery);
47+
Table<T> Select(Expression<Func<T, object[]>> predicate);
48+
Table<T> Where(Expression<Func<T, bool>> predicate);
49+
Task<T?> Single(CancellationToken cancellationToken = default);
50+
Table<T> Set(Expression<Func<T, KeyValuePair<object, object>>> keyValuePairExpression);
51+
Task<ModeledResponse<T>> Update(QueryOptions? options = null, CancellationToken cancellationToken = default);
52+
Task<ModeledResponse<T>> Update(T model, QueryOptions? options = null, CancellationToken cancellationToken = default);
53+
Task<ModeledResponse<T>> Upsert(ICollection<T> model, QueryOptions? options = null, CancellationToken cancellationToken = default);
54+
Task<ModeledResponse<T>> Upsert(T model, QueryOptions? options = null, CancellationToken cancellationToken = default);
55+
}
4656
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using Postgrest.Attributes;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Linq.Expressions;
6+
using System.Reflection;
7+
using System.Text;
8+
using static Postgrest.Constants;
9+
10+
namespace Postgrest.Linq
11+
{
12+
/// <summary>
13+
/// Helper class for parsing Select linq queries.
14+
/// </summary>
15+
internal class SelectExpressionVisitor : ExpressionVisitor
16+
{
17+
/// <summary>
18+
/// The columns that have been selected from this linq expression.
19+
/// </summary>
20+
public List<string> Columns { get; private set; } = new List<string>();
21+
22+
/// <summary>
23+
/// The root call that will be looped through to populate <see cref="Columns"/>.
24+
///
25+
/// Called like: `Table<Movies>().Select(x => new[] { x.Id, x.Name, x.CreatedAt }).Get()`
26+
/// </summary>
27+
/// <param name="node"></param>
28+
/// <returns></returns>
29+
protected override Expression VisitNewArray(NewArrayExpression node)
30+
{
31+
foreach (var expression in node.Expressions)
32+
Visit(expression);
33+
34+
return node;
35+
}
36+
37+
/// <summary>
38+
/// A Member Node, representing a property on a BaseModel.
39+
/// </summary>
40+
/// <param name="node"></param>
41+
/// <returns></returns>
42+
protected override Expression VisitMember(MemberExpression node)
43+
{
44+
var column = GetColumnFromMemberExpression(node);
45+
46+
if (column != null)
47+
Columns.Add(column);
48+
49+
return node;
50+
}
51+
52+
/// <summary>
53+
/// A Unary Node, delved into to represent a property on a BaseModel.
54+
/// </summary>
55+
/// <param name="node"></param>
56+
/// <returns></returns>
57+
protected override Expression VisitUnary(UnaryExpression node)
58+
{
59+
if (node.Operand is MemberExpression memberExpression)
60+
{
61+
var column = GetColumnFromMemberExpression(memberExpression);
62+
63+
if (column != null)
64+
Columns.Add(column);
65+
}
66+
67+
return node;
68+
}
69+
70+
/// <summary>
71+
/// Gets a column name from property based on it's supplied attributes.
72+
/// </summary>
73+
/// <param name="node"></param>
74+
/// <returns></returns>
75+
private string? GetColumnFromMemberExpression(MemberExpression node)
76+
{
77+
var type = node.Member.ReflectedType;
78+
var prop = type.GetProperty(node.Member.Name);
79+
var attrs = prop.GetCustomAttributes(true);
80+
81+
foreach (var attr in attrs)
82+
{
83+
if (attr is ColumnAttribute columnAttr)
84+
return columnAttr.ColumnName;
85+
else if (attr is PrimaryKeyAttribute primaryKeyAttr)
86+
return primaryKeyAttr.ColumnName;
87+
}
88+
89+
throw new ArgumentException(string.Format("Unknown argument '{0}' provided, does it have a `Column` or `PrimaryKey` attribute?", node.Member.Name));
90+
}
91+
}
92+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using Newtonsoft.Json.Linq;
2+
using Postgrest.Attributes;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Linq.Expressions;
7+
using System.Reflection;
8+
using System.Text;
9+
using static Postgrest.Constants;
10+
11+
namespace Postgrest.Linq
12+
{
13+
/// <summary>
14+
/// Helper class for parsing Set linq queries.
15+
/// </summary>
16+
internal class SetExpressionVisitor : ExpressionVisitor
17+
{
18+
/// <summary>
19+
/// The column that have been selected from this linq expression.
20+
/// </summary>
21+
public string? Column { get; private set; }
22+
23+
/// <summary>
24+
/// The Column's type that value should be checked against.
25+
/// </summary>
26+
public Type? ExpectedType { get; private set; }
27+
28+
/// <summary>
29+
/// Value to be updated.
30+
/// </summary>
31+
public object? Value { get; private set; }
32+
33+
/// <summary>
34+
/// Called when visiting a the expected new KeyValuePair().
35+
/// </summary>
36+
/// <param name="node"></param>
37+
/// <returns></returns>
38+
/// <exception cref="ArgumentException"></exception>
39+
protected override Expression VisitNew(NewExpression node)
40+
{
41+
if (node.Arguments.Count != 2)
42+
throw new ArgumentException("Unknown expression, should be a `KeyValuePair<object, object>`");
43+
44+
var member = node.Arguments[0] as MemberExpression;
45+
46+
if (member == null)
47+
throw new ArgumentException("Key should reference a Model Property.");
48+
49+
Column = GetColumnFromMemberExpression(member);
50+
ExpectedType = member.Type;
51+
52+
var valueArgument = Expression.Lambda(node.Arguments[1]).Compile().DynamicInvoke();
53+
Value = valueArgument;
54+
55+
if (!ExpectedType.IsAssignableFrom(Value.GetType()))
56+
throw new ArgumentException(string.Format("Expected Value to be of Type: {0}, instead received: {1}.", ExpectedType.Name, Value.GetType().Name));
57+
58+
return node;
59+
}
60+
61+
/// <summary>
62+
/// Gets a column name from property based on it's supplied attributes.
63+
/// </summary>
64+
/// <param name="node"></param>
65+
/// <returns></returns>
66+
private string GetColumnFromMemberExpression(MemberExpression node)
67+
{
68+
var type = node.Member.ReflectedType;
69+
var prop = type.GetProperty(node.Member.Name);
70+
var attrs = prop.GetCustomAttributes(true);
71+
72+
foreach (var attr in attrs)
73+
{
74+
if (attr is ColumnAttribute columnAttr)
75+
return columnAttr.ColumnName;
76+
else if (attr is PrimaryKeyAttribute primaryKeyAttr)
77+
return primaryKeyAttr.ColumnName;
78+
}
79+
80+
throw new ArgumentException(string.Format("Unknown argument '{0}' provided, does it have a Column or PrimaryKey attribute?", node.Member.Name));
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)