Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
483b24c
fix: The match/2 rule for filtering out <<>> should filter []
jcoglan May 11, 2026
9a8a10c
fix: Normalize negations inside $allMatch and other operators
jcoglan May 11, 2026
a9656d6
feat: Make mango_selector:match/2 return a list of failures
jcoglan Jan 13, 2026
8fed785
chore: Replace `Cmp` argument to `mango_selector:match/3` with a record
jcoglan Jan 15, 2026
2f37649
feat: Record the paths where selectors fail to match
jcoglan Jan 15, 2026
6f2dbab
feat: "verbose" mode for `mango_selector:match/3`
jcoglan Jan 16, 2026
0542d23
feat: Return VDU failure reports to the caller
jcoglan Jan 30, 2026
44e17fa
chore: Add some benchmarks for Mango selector matching
jcoglan Jan 26, 2026
c7d7aa0
fix: Mango VDUs should omit the `oldDoc` field from the matching stru…
jcoglan Apr 8, 2026
734dc8d
docs: More detailed explanation of the behaviour of the oldDocs field…
jcoglan Apr 8, 2026
91b2a0b
fix: Raise an error if a Mango VDU contains invalid top-level fields
jcoglan Apr 9, 2026
4fdeb7e
feat: Validate JS and Mango `validate_doc_update` on PUT /db/_design/doc
jcoglan Jun 1, 2026
9e15759
fix: Selectors like {"$not":{"$allMatch":S}} (and "$elemMatch" and "$…
jcoglan Jul 20, 2026
e908e68
fix: Format Mango VDU errors correctly when a doc contains an empty-s…
jcoglan Jul 20, 2026
c2d56e2
fix: Do not split literal field names like "a.b" into separate path e…
jcoglan Jul 20, 2026
910ee4a
fix: Reject Mango VDUs if they contain invalid field names or invalid…
jcoglan Jul 20, 2026
200cbda
[wip] increase timeouts in extract_selector_hints_test_
jcoglan Jul 31, 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
; 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.
;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
28 changes: 26 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(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,27 @@ 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(#doc{body = {Props}}) ->
case config:get_boolean("couchdb", "validate_vdu", false) of
true ->
case couch_util:get_value(<<"validate_doc_update">>, Props) of
undefined -> nil;
VDU -> {ok, VDU}
end;
_ ->
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.
]
}
}
6 changes: 3 additions & 3 deletions src/mango/src/mango_cursor.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1103,9 +1103,9 @@ extract_selector_hints_test_() ->
meck:unload()
end,
[
?TDEF_FE(t_extract_selector_hints_view, 100),
?TDEF_FE(t_extract_selector_hints_text, 100),
?TDEF_FE(t_extract_selector_hints_nouveau, 100)
?TDEF_FE(t_extract_selector_hints_view, 1000),
?TDEF_FE(t_extract_selector_hints_text, 1000),
?TDEF_FE(t_extract_selector_hints_nouveau, 1000)
]
}.

Expand Down
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}} ->
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