In previous blogs, we covered executing N1QL (SQL++) from JavaScript functions, processing documents through iterators, manipulating data, handling errors, and using prepared statements. We have a few more advanced topics to cover before we move onto manipulating JavaScript libraries, such as calling other functions, using transactions and RBAC.
Calling functions
JavaScript functions can call other JavaScript functions directly.
Any N1QL statement execute in the called function will be executed in much the same way, and with exactly the same limitations, as if it was executed in the called function. Results can travel freely up and down the stack and any exception thrown in the called function will make its way to down the stack to the calling N1QL request (unless caught and handled somewhere).
This is definitely a more practical way to call functions than executing N1QL statements that call N1QL UDFs. By calling functions directly rather than going through EXECUTE FUNCTION you completely bypass the N1QL layer and save on all the resources needed to switch from JavaScript to N1QL and back to JavaScript. This reduces the cost of the context switches, having to move all the data through iterators and not directly, and having to leave iterators open for the duration of the call.
Executing N1QL which calls functions
There are times in which you will execute N1QL statements that call N1QL UDFs themselves and these UDFs may very well be JavaScript functions themselves.
This is absolutely expected, and works in exactly the same way as all that we have described so far.
The only limitation that you should be aware of is that while any N1QL statement that JavaScript may run will execute on the same servicer that is executing the main request. Each JavaScript function call will use a new JavaScript worker, meaning that the more nested N1QL UDF calls you make, the fewer workers will be available, and the function eventually fails like so:
1 2 3 |
function doRecursion() { Â Â Â Â var q = execute function doRecursion(); } |
1 |
CREATE FUNCTION doRecursion() LANGUAGE JAVASCRIPT AS "doRecursion" AT "udfblog" |
Which yields:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
[ Â Â { Â Â Â Â "code": 10109, Â Â Â Â "msg": "Error executing function 'doRecursion' (udfblog:doRecursion)", Â Â Â Â "reason": { Â Â Â Â Â Â "details": { Â Â Â Â Â Â Â Â "Code": "Â Â var q = N1QL('execute function doRecursion();', {}, true);", Â Â Â Â Â Â Â Â "Exception": { Â Â Â Â Â Â Â Â Â Â "caller": "javascript:133", Â Â Â Â Â Â Â Â Â Â "code": 10112, Â Â Â Â Â Â Â Â Â Â "key": "function.nested.error", Â Â Â Â Â Â Â Â Â Â "message": "Error executing function 'doRecursion': 25 nested javascript calls" Â Â Â Â Â Â Â Â }, Â Â Â Â Â Â Â Â "Location": "functions/udfblog.js:11", Â Â Â Â Â Â Â Â "Stack": " Â at doRecursion (functions/udfblog.js:11:13)" Â Â Â Â Â Â }, Â Â Â Â Â Â "type": "Exceptions from JS code" Â Â Â Â } Â Â } ] |
The nesting limit is not fixed, and depends on the number of available JavaScript workers at the time each nested call is executed – in general terms, the more JavaScript activity present on the node, the lower this limit will be.
In the example above, the node had 48 workers, and it failed at the 25th call.
RBAC
In order to execute N1QL as part of JavaScript functions, the user executing the function needs to have appropriate privileges to perform the actions (insert, select, …) specified in the function on the objects referenced by the function.
Since the identity of the user executing the function is not known until run time, any privilege errors will only be raised when the function is executed, assuming that the user executing the function has been granted privilege to execute JavaScript functions in the first place!
This is in line with what other database engines do.
Side effects
Functions that are executed as part of expressions, cannot have side effects: meaning that no DML is allowed to be executed as part of that function. This is in line with what other database engines do and it makes sense. You wouldn’t want a SELECT statement to insert data behind your back through the use of a JavaScript function.
The only way to have functions manipulate storage is to execute them in isolation through the EXECUTE FUNCTION statement.
Transactions
It is perfectly legal to execute transactional N1QL statements inside a JavaScript function, provided that the function is running with the ability to have side effects, namely, via the EXECUTE FUNCTION statement. The function can run DML in a transaction that had been started prior to executing the function, can start the transaction, can commit or rollback the transaction, or any legal combination thereof.
The only thing that you have to bear in mind is that N1QL statements and iterators only live entirely inside or outside of a transaction, but not half way. This means that if you had started a select statement prior to a transaction, but had not consumed all the values, as soon as the transaction starts, that iterator will be closed. The same goes for DML statements that return results (they are completed before the transaction is allowed to start), and a similar behaviour is applied when a transaction is committed or rolled back.
There are good reasons for this, and the behaviour is generally consistent with what happens with database engines. The rationale is that if we were to allow statements to survive transaction boundaries, select statements would not be able to access transaction changes, and DML statements would not be able to rollback in their entirety.
For the avoidance of doubt, you cannot start a transaction inside a loop processing a SELECT iterator, as the iterator would be closed as soon as the transaction starts. The whole outer SELECT should be inside the transaction.
Conclusion
With this blog we have covered the major topics related to executing N1QL inside JavaScript functions. The next blog will concentrate on new features in JavaScript library management.