Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
49e4865
fix: The match/2 rule for filtering out <<>> should filter []
jcoglan May 11, 2026
6a5088e
fix: Normalize negations inside $allMatch and other operators
jcoglan May 11, 2026
28bed98
feat: Make mango_selector:match/2 return a list of failures
jcoglan Jan 13, 2026
2d38eab
chore: Replace `Cmp` argument to `mango_selector:match/3` with a record
jcoglan Jan 15, 2026
592eb95
feat: Record the paths where selectors fail to match
jcoglan Jan 15, 2026
ef17535
feat: "verbose" mode for `mango_selector:match/3`
jcoglan Jan 16, 2026
40c4ab8
feat: Return VDU failure reports to the caller
jcoglan Jan 30, 2026
e2bfdc2
chore: Add some benchmarks for Mango selector matching
jcoglan Jan 26, 2026
85728f8
fix: Mango VDUs should omit the `oldDoc` field from the matching stru…
jcoglan Apr 8, 2026
846f89f
docs: More detailed explanation of the behaviour of the oldDocs field…
jcoglan Apr 8, 2026
53cc327
fix: Raise an error if a Mango VDU contains invalid top-level fields
jcoglan Apr 9, 2026
d09c7e3
feat: Validate JS and Mango `validate_doc_update` on PUT /db/_design/doc
jcoglan Jun 1, 2026
b657e8b
fix: Selectors like {"$not":{"$allMatch":S}} (and "$elemMatch" and "$…
jcoglan Jul 20, 2026
e072153
fix: Format Mango VDU errors correctly when a doc contains an empty-s…
jcoglan Jul 20, 2026
f10bf98
fix: Do not split literal field names like "a.b" into separate path e…
jcoglan Jul 20, 2026
384cd8e
fix: Reject Mango VDUs if they contain invalid field names or invalid…
jcoglan Jul 20, 2026
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
7 changes: 7 additions & 0 deletions rel/overlay/etc/default.ini
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ view_index_dir = {{view_index_dir}}
; Javascript engine. The choices are: spidermonkey and quickjs
;js_engine = spidermonkey

; When set to "true", the `validate_doc_update` field will be validated when
; JavaScript-based design documents are updated. For `javascript` design docs,
; the field must contain a well-formed JavaScript function. For backwards
; compatibility, this setting defaults to false. Mango-based VDUs are always
; validated, regardless of this setting.
;validate_vdu = false

; Use cfile. This is a C-based file I/O module that can execute parallel file
; read calls. The regular Erlang VM file module, at least as of OTP 28 forces
; all file operations to go through a single controlling process which can
Expand Down
3 changes: 3 additions & 0 deletions share/server/dispatch-quickjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ globalThis.dispatch = function(line) {
case "reset":
State.reset.apply(null, cmd);
break;
case "validate_fun":
State.validateFun.apply(null, cmd);
break;
case "add_fun":
State.addFun.apply(null, cmd);
break;
Expand Down
1 change: 1 addition & 0 deletions share/server/loop.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ var Loop = function() {
"ddoc" : DDoc.ddoc,
// "view" : Views.handler,
"reset" : State.reset,
"validate_fun": State.validateFun,
"add_fun" : State.addFun,
"add_lib" : State.addLib,
"map_doc" : Views.mapDoc,
Expand Down
28 changes: 19 additions & 9 deletions share/server/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@
// License for the specific language governing permissions and limitations under
// the License.

var makefun = function(newFun, option) {
switch (option) {
case 'nouveau':
var sandbox = create_nouveau_sandbox();
break;
default:
var sandbox = create_dreyfus_sandbox();
break;
}
return Couch.compileFunction(newFun, {views : {lib : State.lib}}, undefined, sandbox);
}

var State = {
reset : function(config) {
// clear the globals and run gc
Expand All @@ -19,17 +31,15 @@ var State = {
gc();
print("true"); // indicates success
},
validateFun : function(newFun, option) {
// Validate a function but do not store it
makefun(newFun, option);
print("true");
},
addFun : function(newFun, option) {
// Compile to a function and add it to funs array
switch (option) {
case 'nouveau':
var sandbox = create_nouveau_sandbox();
break;
default:
var sandbox = create_dreyfus_sandbox();
break;
}
State.funs.push(Couch.compileFunction(newFun, {views : {lib : State.lib}}, undefined, sandbox));
var fun = makefun(newFun, option);
State.funs.push(fun);
print("true");
},
addLib : function(lib) {
Expand Down
7 changes: 6 additions & 1 deletion src/couch/src/couch_query_servers.erl
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@

try_compile(Proc, FunctionType, FunctionName, FunctionSource) ->
try
proc_prompt(Proc, [<<"add_fun">>, FunctionSource]),
case FunctionType of
validate_doc_update ->
proc_prompt(Proc, [<<"validate_fun">>, FunctionSource]);
_ ->
proc_prompt(Proc, [<<"add_fun">>, FunctionSource])
end,
ok
catch
{compilation_error, E} ->
Expand Down
30 changes: 28 additions & 2 deletions src/couch_mrview/src/couch_mrview.erl
Original file line number Diff line number Diff line change
Expand Up @@ -248,12 +248,15 @@ validate(Db, DDoc) ->
ok
end,

try Views =/= [] andalso couch_query_servers:get_os_process(Lang) of
VDU = should_validate_vdu(Lang, DDoc),

try (Views =/= [] orelse VDU =/= nil) andalso couch_query_servers:get_os_process(Lang) of
false ->
ok;
Proc ->
try
lists:foreach(fun(V) -> ValidateView(Proc, V) end, Views)
lists:foreach(fun(V) -> ValidateView(Proc, V) end, Views),
validate_vdu(Proc, VDU)
after
couch_query_servers:ret_os_process(Proc)
end
Expand All @@ -263,6 +266,29 @@ validate(Db, DDoc) ->
ok
end.

validate_vdu(Proc, VDU0) ->
case VDU0 of
{ok, VDU} ->
couch_query_servers:try_compile(
Proc, validate_doc_update, <<"validate_doc_update">>, VDU
);
_ ->
ok
end.

should_validate_vdu(Lang, #doc{body = {Props}}) ->
Res =
case couch_util:get_value(<<"validate_doc_update">>, Props) of
undefined -> nil;
VDU -> {ok, VDU}
end,
ConfigVal = config:get_boolean("couchdb", "validate_vdu", false),
case {Lang, ConfigVal} of
{<<"query">>, _} -> Res;
{_, true} -> Res;
_ -> nil
end.

check_rank(<<N/binary>>) ->
try binary_to_integer(N) of
Val when Val >= 1 andalso Val =< ?MAX_RANK ->
Expand Down
2 changes: 1 addition & 1 deletion src/couch_scanner/test/eunit/couch_scanner_test.erl
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ setup() ->
#{from => <<"x">>, to => <<"y">>}
],
updates => #{u1 => <<"function(d,r){return [];}">>},
validate_doc_update => <<"function(n,o,u,s){return true;">>
validate_doc_update => <<"function(n,o,u,s){return true;}">>
}),
ok = add_doc(DbName2, ?DOC3, #{foo3 => bax}),
ok = add_doc(DbName2, ?DOC4, #{foo4 => baw, <<>> => this_is_ok_apparently}),
Expand Down
13 changes: 13 additions & 0 deletions src/docs/src/config/couchdb.rst
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,19 @@ Base CouchDB Options
[couchdb]
js_engine = spidermonkey

.. config:option:: validate_vdu :: Enable checking of ``validate_doc_update``

.. versionadded:: TODO

When set to ``true``, the ``validate_doc_update`` field will be
validated when design documents are updated. For ``javascript`` design
docs, the field must contain a well-formed JavaScript function, and for
``query`` design docs it must contain a Mango selector that is correctly
structured to validate document updates. ::

[couchdb]
validate_vdu = true

.. config:option:: time_seq_min_time :: Minimum time-seq threshold

.. versionchanged:: 3.6
Expand Down
72 changes: 71 additions & 1 deletion src/docs/src/ddocs/ddocs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -955,7 +955,8 @@ To use Mango selectors for validation, the design document must have the
containing the following fields:

* ``newDoc``: New version of document that will be stored.
* ``oldDoc``: Previous version of document that is already stored.
* ``oldDoc``: Previous version of document that is already stored; this field is
absent if the doc is being created for the first time.

For example, to check that all docs contain a ``title`` which is a string, and a
``year`` which is a number:
Expand Down Expand Up @@ -1012,3 +1013,72 @@ this design document:
}
}
}

By using the ``oldDoc`` field, we can create rules that say a document can only
be updated if it is currently in a certain state. For example, this rule would
enforce that only documents describing actors can be updated:

.. code-block:: json

{
"language": "query",

"validate_doc_update": {
"oldDoc": { "type": "actor" }
}
}

This also makes it so that no new documents can be created, because a write is
only accepted if a previous version of the doc already exists. To relax this
constraint, allow ``oldDoc`` not to exist:

.. code-block:: json

{
"language": "query",

"validate_doc_update": {
"oldDoc": {
"$or": [
{ "$exists": false },
{ "type": "actor" }
]
}
}
}

This validator will allow any new document creation, and updates to docs where
the ``type`` field is ``"actor"``. We can also have multiple rules for new
document states that depend on the current state, by combining ``$or`` with
several sets of ``{ oldDoc, newDoc }`` rules:

.. code-block:: json

{
"language": "query",

"validate_doc_update": {
"$or": [
// allow creation of docs with an acceptable type
{
"oldDoc": { "$exists": false },
"newDoc": {
"type": { "$in": ["movie", "actor"] }
}
},
// if a doc currently has "type": "actor", make sure its "movies"
// field is a non-empty list of strings
{
"oldDoc": { "type": "actor" },
"newDoc": {
"movies": {
"$type": "array",
"$not": { "$size": 0 },
"$allMatch": { "$type": "string" }
}
}
},
// etc.
]
}
}
59 changes: 50 additions & 9 deletions src/mango/src/mango_native_proc.erl
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,12 @@ set_timeout(Pid, TimeOut) when is_integer(TimeOut), TimeOut > 0 ->
gen_server:call(Pid, {set_timeout, TimeOut}).

prompt(Pid, Data) ->
gen_server:call(Pid, {prompt, Data}).
case gen_server:call(Pid, {prompt, Data}) of
{error, Error} ->
throw(Error);
Other ->
Other
end.

init(_) ->
{ok, #st{}}.
Expand Down Expand Up @@ -95,6 +100,21 @@ handle_call({prompt, [<<"nouveau_index_doc">>, Doc]}, _From, St) ->
Else
end,
{reply, Vals, St};
handle_call({prompt, [<<"validate_fun">>, Selector0 | _Rest]}, _From, St) ->
try mango_selector:normalize(Selector0) of
Selector ->
case validate_vdu(Selector) of
ok -> {reply, true, St};
Error -> {reply, {error, Error}, St}
end
catch
throw:{mango_error, mango_selector, {invalid_operator, Op}} ->
Msg = io_lib:format("invalid operator: ~p", [Op]),
{reply, {error, {compilation_error, Msg}}, St};
throw:{mango_error, mango_util, {invalid_field_name, Field}} ->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder if this will catch all possible mango_error errors. What about {mango_error, mango_selector, {bad_arg, ...}}. Maybe we can just do

throw:{mango_error, _Mod, Reason} ->
    Msg = io_lib:format("invalid selector: ~p", [Reason]),
    {reply, {error, {compilation_error, Msg}}, St}

So we don't have to worry about every single corner case here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This results in responses containing literal Erlang values, which I'd rather avoid. I'm not sure how else to write a generic error formatting block that does not have this problem.

$ cdb '/asd/_design/vdu' -X PUT -d '{ "language": "query", "validate_doc_update": { "newDoc.x": { "$nope": 0 } } }'
{"error":"compilation_error","reason":"Compilation of the validate_doc_update function in the 'validate_doc_update' view failed: invalid selector: {invalid_operator,<<\"$nope\">>}"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This results in responses containing literal Erlang values, which I'd rather avoid. I'm not sure how else to write a generic error formatting block that does not have this problem.

We do have a function to format those in a dedicated module, that might work

        {_Code, _Name, Msg} = mango_error:info(Mod, Reason),
        {reply, {error, {compilation_error, Msg}}, St}

Even if we didn't it might still be preferable to return some ugly error which makes sense rather than a 500 crash which doesn't

Msg = io_lib:format("invalid field name: ~p", [Field]),
{reply, {error, {compilation_error, Msg}}, St}
end;
handle_call({prompt, [<<"ddoc">>, <<"new">>, DDocId, {DDoc}]}, _From, St) ->
NewSt =
case couch_util:get_value(<<"validate_doc_update">>, DDoc) of
Expand All @@ -112,18 +132,39 @@ handle_call({prompt, [<<"ddoc">>, DDocId, [<<"validate_doc_update">>], Args]}, _
Msg = [<<"validate_doc_update">>, DDocId],
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St};
Selector ->
[NewDoc, OldDoc, _Ctx, _SecObj] = Args,
Struct = {[{<<"newDoc">>, NewDoc}, {<<"oldDoc">>, OldDoc}]},
Reply =
case mango_selector:match(Selector, Struct) of
true -> true;
_ -> {[{<<"forbidden">>, <<"document is not valid">>}]}
end,
{reply, Reply, St}
case validate_vdu(Selector) of
{_, Error} ->
{stop, {invalid_call, Error}, {invalid_call, Error}, St};
ok ->
[NewDoc, OldDoc, _Ctx, _SecObj] = Args,
Struct =
case OldDoc of
null -> {[{<<"newDoc">>, NewDoc}]};
Doc -> {[{<<"newDoc">>, NewDoc}, {<<"oldDoc">>, Doc}]}
end,
Reply =
case mango_selector:match_failures(Selector, Struct) of
[] ->
true;
Failures ->
{[{<<"forbidden">>, {[{<<"failures">>, Failures}]}}]}
end,
{reply, Reply, St}
end
end;
handle_call(Msg, _From, St) ->
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.

validate_vdu(VDU) ->
case mango_selector:has_allowed_fields(VDU, [<<"newDoc">>, <<"oldDoc">>]) of
true ->
ok;
false ->
Msg =
<<"'validate_doc_update' may only contain 'newDoc' and 'oldDoc' as top-level fields">>,
{compilation_error, Msg}
end.

handle_cast(garbage_collect, St) ->
garbage_collect(),
{noreply, St};
Expand Down
Loading