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
3 changes: 3 additions & 0 deletions src/Marten/Exceptions/MartenNotSupportedException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Marten.Exceptions;

public sealed class MartenNotSupportedException(string message) : MartenException(message);
124 changes: 124 additions & 0 deletions src/Marten/Linq/Parsing/JsonPathCreator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#nullable enable
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using JasperFx.Core.Reflection;
using Marten.Exceptions;
using Marten.Util;

namespace Marten.Linq.Parsing;

public sealed class JsonPathCreator(ISerializer serializer) : ExpressionVisitor
{
private readonly StringBuilder _jsonPathBuilder = new();
private readonly Stack<string> _fieldNames = new();
private readonly HashSet<MemberExpression> _memberIfInUnary = [];
private readonly HashSet<MemberExpression> _memberIfInBinary = [];
private static readonly Dictionary<ExpressionType, string> LogicalOperators = new()
{
[ExpressionType.Equal] = "==",
[ExpressionType.NotEqual] = "!=",
[ExpressionType.GreaterThan] = ">",
[ExpressionType.GreaterThanOrEqual] = ">=",
[ExpressionType.LessThan] = "<",
[ExpressionType.LessThanOrEqual] = "<=",
[ExpressionType.And] = "&&",
[ExpressionType.AndAlso] = "&&",
[ExpressionType.Or] = "||",
[ExpressionType.OrElse] = "||"
};


public string Build(Expression expression)
{
Visit(expression);

var jsonPath = $"$ ? ({_jsonPathBuilder})";
_jsonPathBuilder.Clear();

return jsonPath;
}

protected override Expression VisitUnary(UnaryExpression node)
{
if (node.NodeType == ExpressionType.Not)
{
if (node.Operand is MemberExpression operandMember)
_memberIfInUnary.Add(operandMember);

Visit(node.Operand);
}

return node;
}

protected override Expression VisitBinary(BinaryExpression node)
{
if (node.Left is MemberExpression leftMember)
_memberIfInBinary.Add(leftMember);

if (node.Right is MemberExpression rightMember)
_memberIfInBinary.Add(rightMember);

Visit(node.Left);
_jsonPathBuilder.Append($" {LogicalOperators[node.NodeType]} ");
Visit(node.Right);

return node;
}

protected override Expression VisitMember(MemberExpression node)
{
if (node.Expression is { NodeType: ExpressionType.Constant or ExpressionType.MemberAccess})
{
_fieldNames.Push(node.Member.Name);
Visit(node.Expression);
}
else
{
_jsonPathBuilder.Append($"@.{node.Member.Name.FormatCase(serializer.Casing)}");

if (!_memberIfInBinary.Contains(node) && node.Type == typeof(bool))
_jsonPathBuilder.Append($" {LogicalOperators[ExpressionType.Equal]} {(_memberIfInUnary.Contains(node) ? "false" : "true")}");
}

return node;
}

protected override Expression VisitConstant(ConstantExpression node)
{
var value = GetValue(node.Value);
_jsonPathBuilder.Append(GetFormatedValue(value));

return node;
}

protected override Expression VisitMethodCall(MethodCallExpression node)
=> throw new MartenNotSupportedException("Calling a method is not supported");

private string GetFormatedValue(object? value)
=> serializer.ToJson(value);

private object? GetValue(object? input)
{
if (input is null)
return null;

var type = input.GetType();

if (!type.IsClass || type == typeof(string))
return input;

var fieldName = _fieldNames.Pop();
var fieldInfo = type.GetField(fieldName);

var value = fieldInfo is not null
? fieldInfo.GetValue(input)
: type.GetProperty(fieldName)?.GetValue(input);

return GetValue(value);
}
}
33 changes: 33 additions & 0 deletions src/Marten/Patching/IPatchExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ public interface IPatchExpression<T>
/// <returns></returns>
IPatchExpression<T> AppendIfNotExists<TElement>(Expression<Func<T, IEnumerable<TElement>>> expression, 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
/// </summary>
/// <typeparam name="TElement"></typeparam>
/// <param name="expression"></param>
/// <param name="element"></param>
/// <param name="predicate"></param>
/// <returns></returns>
IPatchExpression<T> AppendIfNotExists<TElement>(Expression<Func<T, IEnumerable<TElement>>> expression, TElement element, Expression<Func<TElement, bool>> predicate);

/// <summary>
/// Insert an element at the designated index to a child collection on the persisted document
/// </summary>
Expand All @@ -131,6 +142,18 @@ public interface IPatchExpression<T>
/// <returns></returns>
IPatchExpression<T> InsertIfNotExists<TElement>(Expression<Func<T, IEnumerable<TElement>>> expression, TElement element, int? index = null);

/// <summary>
/// Insert an element at the designated index to a child collection on the persisted document
/// if the value does not already exist by predicate
/// </summary>
/// <typeparam name="TElement"></typeparam>
/// <param name="expression"></param>
/// <param name="element"></param>
/// <param name="predicate"></param>
/// <param name="index"></param>
/// <returns></returns>
IPatchExpression<T> InsertIfNotExists<TElement>(Expression<Func<T, IEnumerable<TElement>>> expression, TElement element, Expression<Func<TElement, bool>> predicate, int? index = null);

/// <summary>
/// Remove element from a child collection on the persisted document
/// </summary>
Expand All @@ -141,6 +164,16 @@ public interface IPatchExpression<T>
/// <returns></returns>
IPatchExpression<T> Remove<TElement>(Expression<Func<T, IEnumerable<TElement>>> expression, TElement element, RemoveAction action = RemoveAction.RemoveFirst);

/// <summary>
/// Remove element from a child collection by predicate on the persisted document
/// </summary>
/// <typeparam name="TElement"></typeparam>
/// <param name="expression"></param>
/// <param name="predicate"></param>
/// <param name="action"></param>
/// <returns></returns>
IPatchExpression<T> Remove<TElement>(Expression<Func<T, IEnumerable<TElement>>> expression, Expression<Func<TElement, bool>> predicate, RemoveAction action = RemoveAction.RemoveFirst);

/// <summary>
/// Rename a property or field in the persisted JSON document
/// </summary>
Expand Down
48 changes: 48 additions & 0 deletions src/Marten/Patching/PatchExpression.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Text.Json;
using JasperFx.Core;
using Marten.Internal.Sessions;
using Marten.Linq.Parsing;
Expand Down Expand Up @@ -159,6 +161,20 @@ public IPatchExpression<T> AppendIfNotExists<TElement>(Expression<Func<T, IEnume
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>();
patch.Add("type", "append_if_not_exists");
patch.Add("value", element);
patch.Add("expression", toPathExpression(predicate));
patch.Add("path", toPath(expression));

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

return this;
}

public IPatchExpression<T> Insert<TElement>(Expression<Func<T, IEnumerable<TElement>>> expression, TElement element, int? index = null)
{
var patch = new Dictionary<string, object>();
Expand Down Expand Up @@ -192,6 +208,23 @@ public IPatchExpression<T> InsertIfNotExists<TElement>(Expression<Func<T, IEnume
return this;
}

public IPatchExpression<T> InsertIfNotExists<TElement>(Expression<Func<T, IEnumerable<TElement>>> expression, TElement element, Expression<Func<TElement, bool>> predicate, int? index = null)
{
var patch = new Dictionary<string, object>();
patch.Add("type", "insert_if_not_exists");
patch.Add("value", element);
patch.Add("expression", toPathExpression(predicate));
patch.Add("path", toPath(expression));
if (index.HasValue)
{
patch.Add("index", index);
}

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

public IPatchExpression<T> Remove<TElement>(Expression<Func<T, IEnumerable<TElement>>> expression, TElement element, RemoveAction action = RemoveAction.RemoveFirst)
{
var patch = new Dictionary<string, object>();
Expand All @@ -205,6 +238,18 @@ public IPatchExpression<T> Remove<TElement>(Expression<Func<T, IEnumerable<TElem
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>();
patch.Add("type", "remove");
patch.Add("expression", toPathExpression(predicate));
patch.Add("path", toPath(expression));
patch.Add("action", (int)action);

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

public IPatchExpression<T> Rename(string oldName, Expression<Func<T, object>> expression)
{
var patch = new Dictionary<string, object>();
Expand Down Expand Up @@ -268,5 +313,8 @@ private string toPath(Expression expression)
return visitor.Members.Select(x => x.Name.FormatCase(_session.Serializer.Casing)).Join(".");
}

private string toPathExpression(Expression expression)
=> new JsonPathCreator(_session.Serializer).Build(expression);

private DbObjectName PatchFunction => new PostgresqlObjectName(_session.Options.DatabaseSchemaName, "mt_jsonb_patch");
}
9 changes: 6 additions & 3 deletions src/Marten/Schema/SQL/mt_jsonb_append.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CREATE OR REPLACE FUNCTION {databaseSchema}.mt_jsonb_append(jsonb, text[], jsonb, boolean)
CREATE OR REPLACE FUNCTION {databaseSchema}.mt_jsonb_append(jsonb, text[], jsonb, boolean, jsonb default null)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
Expand All @@ -7,16 +7,19 @@ DECLARE
location ALIAS FOR $2;
val ALIAS FOR $3;
if_not_exists ALIAS FOR $4;
patch_expression ALIAS FOR $5;
tmp_value jsonb;
BEGIN
tmp_value = retval #> location;
IF tmp_value IS NOT NULL AND jsonb_typeof(tmp_value) = 'array' THEN
CASE
WHEN NOT if_not_exists THEN
retval = jsonb_set(retval, location, tmp_value || val, FALSE);
WHEN jsonb_typeof(val) = 'object' AND NOT tmp_value @> jsonb_build_array(val) THEN
WHEN patch_expression IS NULL AND jsonb_typeof(val) = 'object' AND NOT tmp_value @> jsonb_build_array(val) THEN
retval = jsonb_set(retval, location, tmp_value || val, FALSE);
WHEN jsonb_typeof(val) <> 'object' AND NOT tmp_value @> val THEN
WHEN patch_expression IS NULL AND jsonb_typeof(val) <> 'object' AND NOT tmp_value @> val THEN
retval = jsonb_set(retval, location, tmp_value || val, FALSE);
WHEN patch_expression IS NOT NULL AND jsonb_typeof(patch_expression) = 'array' AND jsonb_array_length(patch_expression) = 0 THEN
retval = jsonb_set(retval, location, tmp_value || val, FALSE);
ELSE NULL;
END CASE;
Expand Down
9 changes: 6 additions & 3 deletions src/Marten/Schema/SQL/mt_jsonb_insert.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CREATE OR REPLACE FUNCTION {databaseSchema}.mt_jsonb_insert(jsonb, text[], jsonb, integer, boolean)
CREATE OR REPLACE FUNCTION {databaseSchema}.mt_jsonb_insert(jsonb, text[], jsonb, integer, boolean, jsonb default null)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
Expand All @@ -8,6 +8,7 @@ DECLARE
val ALIAS FOR $3;
elm_index ALIAS FOR $4;
if_not_exists ALIAS FOR $5;
patch_expression ALIAS FOR $6;
tmp_value jsonb;
BEGIN
tmp_value = retval #> location;
Expand All @@ -18,9 +19,11 @@ BEGIN
CASE
WHEN NOT if_not_exists THEN
retval = jsonb_insert(retval, location || elm_index::text, val);
WHEN jsonb_typeof(val) = 'object' AND NOT tmp_value @> jsonb_build_array(val) THEN
WHEN patch_expression IS NULL AND jsonb_typeof(val) = 'object' AND NOT tmp_value @> jsonb_build_array(val) THEN
retval = jsonb_insert(retval, location || elm_index::text, val);
WHEN jsonb_typeof(val) <> 'object' AND NOT tmp_value @> val THEN
WHEN patch_expression IS NULL AND jsonb_typeof(val) <> 'object' AND NOT tmp_value @> val THEN
retval = jsonb_insert(retval, location || elm_index::text, val);
WHEN patch_expression IS NOT NULL AND jsonb_typeof(patch_expression) = 'array' AND jsonb_array_length(patch_expression) = 0 THEN
retval = jsonb_insert(retval, location || elm_index::text, val);
ELSE NULL;
END CASE;
Expand Down
48 changes: 27 additions & 21 deletions src/Marten/Schema/SQL/mt_jsonb_patch.sql
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,42 @@ DECLARE
patchset ALIAS FOR $2;
patch jsonb;
patch_path text[];
patch_expression jsonb;
value jsonb;
BEGIN
FOR patch IN SELECT * from jsonb_array_elements(patchset)
LOOP
patch_path = {databaseSchema}.mt_jsonb_path_to_array((patch->>'path')::text, '\.');

patch_expression = null;
IF (patch->>'type') IN ('remove', 'append_if_not_exists', 'insert_if_not_exists') AND (patch->>'expression') IS NOT NULL THEN
patch_expression = jsonb_path_query_array(retval->(patch->>'path'), (patch->>'expression')::jsonpath);
END IF;

CASE patch->>'type'
WHEN 'set' THEN
retval = jsonb_set(retval, patch_path,(patch->'value')::jsonb, TRUE);
WHEN 'delete' THEN
retval = jsonb_set(retval, patch_path, (patch->'value')::jsonb, TRUE);
WHEN 'delete' THEN
retval = retval#-patch_path;
WHEN 'append' THEN
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);
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);
WHEN 'remove' THEN
retval = {databaseSchema}.mt_jsonb_remove(retval, patch_path,(patch->'value')::jsonb);
WHEN 'duplicate' THEN
retval = {databaseSchema}.mt_jsonb_duplicate(retval, patch_path,(patch->'targets')::jsonb);
WHEN 'rename' THEN
retval = {databaseSchema}.mt_jsonb_move(retval, patch_path,(patch->>'to')::text);
WHEN 'increment' THEN
retval = {databaseSchema}.mt_jsonb_increment(retval, patch_path,(patch->>'increment')::numeric);
WHEN 'increment_float' THEN
retval = {databaseSchema}.mt_jsonb_increment(retval, patch_path,(patch->>'increment')::numeric);
ELSE NULL;
WHEN 'append' THEN
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 '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 'duplicate' THEN
retval = {databaseSchema}.mt_jsonb_duplicate(retval, patch_path, (patch->'targets')::jsonb);
WHEN 'rename' THEN
retval = {databaseSchema}.mt_jsonb_move(retval, patch_path, (patch->>'to')::text);
WHEN 'increment' THEN
retval = {databaseSchema}.mt_jsonb_increment(retval, patch_path, (patch->>'increment')::numeric);
WHEN 'increment_float' THEN
retval = {databaseSchema}.mt_jsonb_increment(retval, patch_path, (patch->>'increment')::numeric);
ELSE NULL;
END CASE;
END LOOP;
RETURN retval;
Expand Down
Loading
Loading