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
16 changes: 16 additions & 0 deletions src/Marten.Testing/Documents/Target.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ public static Target Random(bool deep = false, bool includeFSharpUnionTypes = fa
target.StringDict = Enumerable.Range(0, _random.Next(0, 10)).ToDictionary(i => $"key{i}", i => $"value{i}");
target.String = _strings[_random.Next(0, 10)];
target.OtherGuid = Guid.NewGuid();

target.ChildrenDictionary.Add("originKey", Random());
}

return target;
Expand All @@ -141,15 +143,25 @@ public Target()
StringDict = new Dictionary<string, string>();
StringList = new List<string>();
GuidDict = new Dictionary<Guid, Guid>();
NumberByKey = new Dictionary<string, int>();
NumberByGuidKey = new Dictionary<Guid, int>();
LongByKey = new Dictionary<string, long>();
DoubleByKey = new Dictionary<string, double>();
DecimalByKey = new Dictionary<string, decimal>();
FloatByKey = new Dictionary<string, float>();
ChildrenDictionary = new Dictionary<string, Target>();
}

public Guid Id { get; set; }

public int Number { get; set; }
public Dictionary<string, int> NumberByKey { get; set; }
public Dictionary<Guid, int> NumberByGuidKey { get; set; }

public int AnotherNumber { get; set; }

public long Long { get; set; }
public Dictionary<string, long> LongByKey { get; set; }
public string String { get; set; }

public FSharpOption<Guid> FSharpGuidOption { get; set; }
Expand Down Expand Up @@ -182,13 +194,16 @@ public Target()
public string StringField;

public double Double { get; set; }
public Dictionary<string, double> DoubleByKey { get; set; }
public decimal Decimal { get; set; }
public Dictionary<string, decimal> DecimalByKey { get; set; }
public DateTime Date { get; set; }
public DateTimeOffset DateOffset { get; set; }
public DateTimeOffset? NullableDateOffset { get; set; }

[JsonInclude] // this is needed to make System.Text.Json happy
public float Float;
public Dictionary<string, float> FloatByKey { get; set; }

public int[] NumberArray { get; set; }

Expand All @@ -197,6 +212,7 @@ public Target()
public HashSet<string> TagsHashSet { get; set; }

public Target[] Children { get; set; }
public Dictionary<string, Target> ChildrenDictionary { get; set; }

public int? NullableNumber { get; set; }
public DateTime? NullableDateTime { get; set; }
Expand Down
17 changes: 17 additions & 0 deletions src/Marten/Linq/Parsing/IndexerKeyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Reflection;

namespace Marten.Linq.Parsing;

public sealed class IndexerKeyInfo(string key) : MemberInfo
{
public override string Name { get; } = key;

// Mandatory but not used
public override Type DeclaringType => null!;
public override Type ReflectedType => null!;
public override MemberTypes MemberType => MemberTypes.Custom;
public override object[] GetCustomAttributes(bool inherit) => [];
public override object[] GetCustomAttributes(Type attributeType, bool inherit) => [];
public override bool IsDefined(Type attributeType, bool inherit) => false;
}
81 changes: 81 additions & 0 deletions src/Marten/Linq/Parsing/PatchingMemberFinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace Marten.Linq.Parsing;

public sealed class PatchingMemberFinder : MemberFinder
{
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.Name == "get_Item" && IsDictionary(node.Object?.Type))
{
var keyValue = EvaluateExpression(node.Arguments[0]);
Members.Insert(0, new IndexerKeyInfo(keyValue?.ToString()!));

if (node.Object != null)
Visit(node.Object);

return node;
}

return base.VisitMethodCall(node);
}

private static bool IsDictionary(Type? type)
{
if (type == null) return false;

if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
return true;

return type.GetInterfaces()
.Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>));
}

private static object? EvaluateExpression(Expression expression)
=> expression switch
{
ConstantExpression constant => constant.Value,
MemberExpression member => EvaluateMemberExpression(member),
UnaryExpression { NodeType: ExpressionType.Convert } unary => EvaluateExpression(unary.Operand),
_ => CompileAndInvoke(expression)
};

private static object? EvaluateMemberExpression(MemberExpression member)
{
switch (member.Expression)
{
case ConstantExpression constant:
return member.Member switch
{
FieldInfo field => field.GetValue(constant.Value),
PropertyInfo prop => prop.GetValue(constant.Value),
_ => CompileAndInvoke(member)
};

case MemberExpression parentMember:
{
var parentValue = EvaluateMemberExpression(parentMember);
return member.Member switch
{
FieldInfo field => field.GetValue(parentValue),
PropertyInfo prop => prop.GetValue(parentValue),
_ => CompileAndInvoke(member)
};
}

default:
return CompileAndInvoke(member);
}
}

private static object CompileAndInvoke(Expression expression)
{
var converted = Expression.Convert(expression, typeof(object));
var lambda = Expression.Lambda<Func<object>>(converted);
return lambda.Compile()();
}
}
29 changes: 29 additions & 0 deletions src/Marten/Patching/IPatchExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ public interface IPatchExpression<T>
/// <returns></returns>
IPatchExpression<T> Append<TElement>(Expression<Func<T, IEnumerable<TElement>>> expression, TElement element);

/// <summary>
/// Append an element with the specified key to a child dictionary on the persisted document
/// </summary>
/// <typeparam name="TElement"></typeparam>
/// <param name="expression"></param>
/// <param name="key"></param>
/// <param name="element"></param>
/// <returns></returns>
IPatchExpression<T> Append<TElement>(Expression<Func<T, IDictionary<string, TElement>>> expression, string key, TElement element);

/// <summary>
/// Append an element to the end of a child collection on the persisted
/// document if the element does not already exist
Expand All @@ -110,6 +120,16 @@ public interface IPatchExpression<T>
/// <returns></returns>
IPatchExpression<T> AppendIfNotExists<TElement>(Expression<Func<T, IEnumerable<TElement>>> expression, TElement element);

/// <summary>
/// Append an element with the specified key to a child dictionary on the persisted document if the key does not already exist
/// </summary>
/// <typeparam name="TElement"></typeparam>
/// <param name="expression"></param>
/// <param name="key"></param>
/// <param name="element"></param>
/// <returns></returns>
IPatchExpression<T> AppendIfNotExists<TElement>(Expression<Func<T, IDictionary<string, TElement>>> expression, string key, TElement element);

/// <summary>
/// Append an element to the end of a child collection on the persisted
/// document if the element does not already exist by predicate
Expand Down Expand Up @@ -164,6 +184,15 @@ public interface IPatchExpression<T>
/// <returns></returns>
IPatchExpression<T> Remove<TElement>(Expression<Func<T, IEnumerable<TElement>>> expression, TElement element, RemoveAction action = RemoveAction.RemoveFirst);

/// <summary>
/// Remove an element with the specified key from a child dictionary on the persisted document
/// </summary>
/// <typeparam name="TElement"></typeparam>
/// <param name="expression"></param>
/// <param name="key"></param>
/// <returns></returns>
IPatchExpression<T> Remove<TElement>(Expression<Func<T, IDictionary<string, TElement>>> expression, string key);

/// <summary>
/// Remove element from a child collection by predicate on the persisted document
/// </summary>
Expand Down
40 changes: 39 additions & 1 deletion src/Marten/Patching/PatchExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,19 @@ public IPatchExpression<T> Append<TElement>(Expression<Func<T, IEnumerable<TElem
return this;
}

public IPatchExpression<T> Append<TElement>(Expression<Func<T, IDictionary<string, TElement>>> expression, string key, TElement element)
{
var patch = new Dictionary<string, object>();
patch.Add("type", "append_key_value");
patch.Add("key_value", new Dictionary<string, TElement> { { key, element } });
patch.Add("path", toPath(expression));

var possiblyPolymorphic = element!.GetType() != typeof(TElement);
_patchSet.Add(new PatchData(Items: patch, possiblyPolymorphic));

return this;
}

public IPatchExpression<T> AppendIfNotExists<TElement>(Expression<Func<T, IEnumerable<TElement>>> expression, TElement element)
{
var patch = new Dictionary<string, object>();
Expand All @@ -161,6 +174,19 @@ public IPatchExpression<T> AppendIfNotExists<TElement>(Expression<Func<T, IEnume
return this;
}

public IPatchExpression<T> AppendIfNotExists<TElement>(Expression<Func<T, IDictionary<string, TElement>>> expression, string key, TElement element)
{
var patch = new Dictionary<string, object>();
patch.Add("type", "append_key_value_if_not_exists");
patch.Add("key_value", new Dictionary<string, TElement> { { key, element } });
patch.Add("path", toPath(expression));

var possiblyPolymorphic = element!.GetType() != typeof(TElement);
_patchSet.Add(new PatchData(Items: patch, possiblyPolymorphic));

return this;
}

public IPatchExpression<T> AppendIfNotExists<TElement>(Expression<Func<T, IEnumerable<TElement>>> expression, TElement element, Expression<Func<TElement, bool>> predicate)
{
var patch = new Dictionary<string, object>();
Expand Down Expand Up @@ -238,6 +264,18 @@ public IPatchExpression<T> Remove<TElement>(Expression<Func<T, IEnumerable<TElem
return this;
}

public IPatchExpression<T> Remove<TElement>(Expression<Func<T, IDictionary<string, TElement>>> expression, string key)
{
var patch = new Dictionary<string, object>();
patch.Add("type", "remove_key");
patch.Add("key", key);
patch.Add("path", toPath(expression));

_patchSet.Add(new PatchData(Items: patch, PossiblyPolymorphic: false));

return this;
}

public IPatchExpression<T> Remove<TElement>(Expression<Func<T, IEnumerable<TElement>>> expression, Expression<Func<TElement, bool>> predicate, RemoveAction action = RemoveAction.RemoveFirst)
{
var patch = new Dictionary<string, object>();
Expand Down Expand Up @@ -306,7 +344,7 @@ private IPatchExpression<T> delete(string path)

private string toPath(Expression expression)
{
var visitor = new MemberFinder();
var visitor = new PatchingMemberFinder();
visitor.Visit(expression);

// TODO -- don't like this. Smells like duplication in logic
Expand Down
26 changes: 26 additions & 0 deletions src/Marten/Schema/SQL/mt_jsonb_append_key_value.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
CREATE OR REPLACE FUNCTION {databaseSchema}.mt_jsonb_append_key_value(jsonb, text[], jsonb, boolean)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
DECLARE
retval ALIAS FOR $1;
location ALIAS FOR $2;
val ALIAS FOR $3;
if_not_exists ALIAS FOR $4;
tmp_value jsonb;
_key text;
BEGIN
tmp_value = retval #> location;

IF tmp_value IS NOT NULL AND jsonb_typeof(tmp_value) = 'object' THEN
CASE
WHEN NOT if_not_exists THEN
retval = jsonb_set(retval, location, tmp_value || val, FALSE);
WHEN NOT tmp_value ?| ARRAY(SELECT jsonb_object_keys(val)) THEN
retval = jsonb_set(retval, location, tmp_value || val, FALSE);
ELSE NULL;
END CASE;
END IF;
RETURN retval;
END;
$function$;
6 changes: 6 additions & 0 deletions src/Marten/Schema/SQL/mt_jsonb_patch.sql
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,18 @@ BEGIN
retval = {databaseSchema}.mt_jsonb_append(retval, patch_path, (patch->'value')::jsonb, FALSE);
WHEN 'append_if_not_exists' THEN
retval = {databaseSchema}.mt_jsonb_append(retval, patch_path, (patch->'value')::jsonb, TRUE, patch_expression);
WHEN 'append_key_value' THEN
retval = {databaseSchema}.mt_jsonb_append_key_value(retval, patch_path, (patch->'key_value')::jsonb, FALSE);
WHEN 'append_key_value_if_not_exists' THEN
retval = {databaseSchema}.mt_jsonb_append_key_value(retval, patch_path, (patch->'key_value')::jsonb, TRUE);
WHEN 'insert' THEN
retval = {databaseSchema}.mt_jsonb_insert(retval, patch_path, (patch->'value')::jsonb, (patch->>'index')::integer, FALSE);
WHEN 'insert_if_not_exists' THEN
retval = {databaseSchema}.mt_jsonb_insert(retval, patch_path, (patch->'value')::jsonb, (patch->>'index')::integer, TRUE, patch_expression);
WHEN 'remove' THEN
retval = {databaseSchema}.mt_jsonb_remove(retval, patch_path, COALESCE(patch_expression, (patch->'value')::jsonb));
WHEN 'remove_key' THEN
retval = {databaseSchema}.mt_jsonb_remove_key(retval, patch_path, (patch->>'key'));
WHEN 'duplicate' THEN
retval = {databaseSchema}.mt_jsonb_duplicate(retval, patch_path, (patch->'targets')::jsonb);
WHEN 'rename' THEN
Expand Down
19 changes: 19 additions & 0 deletions src/Marten/Schema/SQL/mt_jsonb_remove_key.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
CREATE OR REPLACE FUNCTION {databaseSchema}.mt_jsonb_remove_key(jsonb, text[], text)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
DECLARE
retval ALIAS FOR $1;
location ALIAS FOR $2;
_key ALIAS FOR $3;
tmp_value jsonb;
tmp_remove jsonb;
patch_remove jsonb;
BEGIN
tmp_value = retval #> location;
IF tmp_value IS NOT NULL AND jsonb_typeof(tmp_value) = 'object' THEN
RETURN jsonb_set(retval, location, tmp_value - _key, FALSE);
END IF;
RETURN retval;
END;
$function$;
4 changes: 3 additions & 1 deletion src/Marten/Storage/StorageFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,8 @@ internal void PostProcessConfiguration()
SystemFunctions.AddSystemFunction(_options, "mt_grams_vector", "text,boolean");
SystemFunctions.AddSystemFunction(_options, "mt_grams_query", "text,boolean");
SystemFunctions.AddSystemFunction(_options, "mt_grams_array", "text,boolean");
SystemFunctions.AddSystemFunction(_options, "mt_jsonb_append", "jsonb,text[],jsonb,boolean");
SystemFunctions.AddSystemFunction(_options, "mt_jsonb_append", "jsonb,text[],jsonb,boolean,jsonb");
SystemFunctions.AddSystemFunction(_options, "mt_jsonb_append_key_value", "jsonb,text[],jsonb,boolean");
SystemFunctions.AddSystemFunction(_options, "mt_jsonb_copy", "jsonb,text[],text[]");
SystemFunctions.AddSystemFunction(_options, "mt_jsonb_duplicate", "jsonb,text[],jsonb");
SystemFunctions.AddSystemFunction(_options, "mt_jsonb_fix_null_parent", "jsonb,text[]");
Expand All @@ -283,6 +284,7 @@ internal void PostProcessConfiguration()
SystemFunctions.AddSystemFunction(_options, "mt_jsonb_move", "jsonb,text[],text");
SystemFunctions.AddSystemFunction(_options, "mt_jsonb_path_to_array", "text,char(1)");
SystemFunctions.AddSystemFunction(_options, "mt_jsonb_remove", "jsonb,text[],jsonb");
SystemFunctions.AddSystemFunction(_options, "mt_jsonb_remove_key", "jsonb,text[],text");
SystemFunctions.AddSystemFunction(_options, "mt_jsonb_patch", "jsonb,jsonb");
SystemFunctions.AddSystemFunction(_options, "mt_safe_unaccent", "boolean,text");

Expand Down
Loading
Loading