In previous blogs, we covered executing N1QL (SQL++) from JavaScript functions, processing documents through iteratorsmanipulating 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:

Which yields:

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.

 

Author

Posted by Marco Greco, Software Architect, Couchbase

In a previous life, Marco used to be CTO, radiation physicist, software architect, sysadmin, DBA, trainer and general handyman at Italy's largest radiation theraphy practice. Having switched career and country, he spent more than two decades in various support and development positions in Informix first and IBM later, before finally taking the plunge and joining Couchbase, to help them make gold out of N1QL. He holds several patents and has authored open source projects of his own.

Leave a reply