Merge lp:~hingo/drizzle/drizzle-js_eval into lp:~drizzle-trunk/drizzle/development

Proposed by Henrik Ingo
Status: Merged
Approved by: Brian Aker
Approved revision: 2385
Merged at revision: 2439
Proposed branch: lp:~hingo/drizzle/drizzle-js_eval
Merge into: lp:~drizzle-trunk/drizzle/development
Diff against target: 877 lines (+747/-9)
10 files modified
configure.ac (+1/-1)
drizzled/error.cc (+3/-0)
drizzled/error_t.h (+2/-1)
drizzled/temporal.h (+7/-7)
m4/pandora_have_libv8.m4 (+53/-0)
plugin/js/docs/index.rst (+230/-0)
plugin/js/js.cc (+371/-0)
plugin/js/plugin.ini (+10/-0)
plugin/js/tests/r/js.result (+35/-0)
plugin/js/tests/t/js.test (+35/-0)
To merge this branch: bzr merge lp:~hingo/drizzle/drizzle-js_eval
Reviewer Review Type Date Requested Status
Drizzle Merge Team Pending
Review via email: mp+76674@code.launchpad.net

Description of the change

Hi

I've written a plugin that embeds the v8 javascript engine and exposes it as a function JS(<javascript code snippet>, <arg1>, <arg2>, ...). The code is now feature complete for the first iteration. It still lacks test cases and documentation. I intend to complete those before this is merged, but I'm sending this to ask for a first review at this stage. This is my first Drizzle code I've ever written, so getting feedback at this stage would be appreciated. So please note, I'm asking for review but not yet asking to merge this.

The primary reason to do this is to get a JSON parser + ability to manipulate and return the JSON documents. This is analogous to the MySQL functions ExtractValue() and UpdateXML() for XML strings, there just isn't a counterpart to XPath in JSON. The side effect of being able to execute arbitrary Javascript inside Drizzle is cool, and can be built upon in the future. Currently there is no Drizzle functionality available inside the javascript environment, for instance you couldn't query any tables or do anything else you typically do with stored procedures.

The code comments still contain some todos which are more like questions - things I'd like to learn about Drizzle internals. If the reviewer can answer those in feedback via drizzle-discuss, that would be great.

The code comments also contain todos which I don't intend to fix in the first version, they are mostly performance work in particular related to the fact that v8 is very single threaded (Chrome browser has a separate browser and separate v8 instance for each tab). The intent is to focus short term on feature completeness and leave the performance work for later. (The current code will not degrade Drizzle performance in general, but it would not be a good idea to do a hundred simultaneous calls to JS().)

Usage examples can be seen in the comments to this blog post:
http://openlife.cc/blogs/2011/august/stored-procedures-javascript-my-drizzle-repository-can-do-it

Please note that the function is now called JS(...) and there is no JS_EVAL(...).

To post a comment you must log in.
lp:~hingo/drizzle/drizzle-js_eval updated
2385. By Henrik Ingo

Added tests for JS().

Revision history for this message
Olaf van der Spek (olafvdspek) wrote :

Some style comments, I hope you don't mind.

206 +namespace drizzle_plugin {
207 +
208 +namespace js {

Redundant empty line

220 +class JsFunction :public Item_str_func

Missing space after ':'

221 +{
222 +public:
223 + JsFunction() :Item_str_func() {}
224 + ~JsFunction() {}

Empty constructors and destructors aren't necessary.

260 +void emit_drizzle_error(v8::TryCatch* try_catch) {

'{' should be on the next line

Greetings,

Olaf

lp:~hingo/drizzle/drizzle-js_eval updated
2386. By Henrik Ingo

Add 2 tests I forgot:
 - connect from 2 clients to make sure we are handling multi threaded
   mode correctly.
 - run a script that gives JavaScript syntax error.

2387. By Henrik Ingo

Wrote end user documentation for JS().
Labeled this as version 0.9.
This is now ready to start the journey towards trunk!

2388. By Henrik Ingo

Small style fixes from Olaf's review (thanks).
Removed @todo items that have been done.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'configure.ac'
2--- configure.ac 2011-08-16 01:07:54 +0000
3+++ configure.ac 2011-10-01 10:16:24 +0000
4@@ -155,7 +155,7 @@
5 PANDORA_DRIZZLE_BUILD
6 PANDORA_HAVE_BOOST_TEST
7 PANDORA_HAVE_LIBSQLITE3
8-
9+PANDORA_HAVE_LIBV8
10
11
12 #########################################################################
13
14=== modified file 'drizzled/error.cc'
15--- drizzled/error.cc 2011-08-16 11:47:29 +0000
16+++ drizzled/error.cc 2011-10-01 10:16:24 +0000
17@@ -639,6 +639,9 @@
18 ADD_ERROR_MESSAGE(ER_USE_DATA_DICTIONARY, N_("Engine status is now stored in the data_dictionary tables, please use these instead."));
19 ADD_ERROR_MESSAGE(ER_TRANSACTION_ALREADY_STARTED, N_("There is already a transaction in progress"));
20 ADD_ERROR_MESSAGE(ER_NO_LOCK_HELD, N_("No lock is held by this connection."));
21+
22+ // Errors in scripts, such as JavaScript
23+ ADD_ERROR_MESSAGE(ER_SCRIPT, N_("Script error: %s"));
24 }
25
26 } /* namespace drizzled */
27
28=== modified file 'drizzled/error_t.h'
29--- drizzled/error_t.h 2011-08-16 11:47:29 +0000
30+++ drizzled/error_t.h 2011-10-01 10:16:24 +0000
31@@ -866,7 +866,8 @@
32 ER_USE_DATA_DICTIONARY,
33 ER_TRANSACTION_ALREADY_STARTED,
34 ER_CARTESIAN_JOIN_ATTEMPTED,
35- ER_NO_LOCK_HELD
36+ ER_NO_LOCK_HELD,
37+ ER_SCRIPT /* Error executing script: (such as JavaScript) */
38 };
39
40
41
42=== modified file 'drizzled/temporal.h'
43--- drizzled/temporal.h 2011-03-29 12:45:08 +0000
44+++ drizzled/temporal.h 2011-10-01 10:16:24 +0000
45@@ -76,7 +76,7 @@
46 /**
47 * Base class for all temporal data classes.
48 */
49-class Temporal
50+class DRIZZLED_API Temporal
51 {
52 protected:
53 enum calendar _calendar;
54@@ -188,7 +188,7 @@
55 * Class representing temporal components in a valid
56 * SQL date range, with no time component
57 */
58-class Date: public Temporal
59+class DRIZZLED_API Date: public Temporal
60 {
61 public:
62 Date() :Temporal() {}
63@@ -435,7 +435,7 @@
64 * Class representing temporal components having only
65 * a time component, with no date structure
66 */
67-class Time: public Temporal
68+class DRIZZLED_API Time: public Temporal
69 {
70 public:
71 Time() :Temporal() {}
72@@ -567,7 +567,7 @@
73 * Class representing temporal components in a valid
74 * SQL datetime range, including a time component
75 */
76-class DateTime: public Date
77+class DRIZZLED_API DateTime: public Date
78 {
79 public:
80 DateTime() :Date() {}
81@@ -680,7 +680,7 @@
82 /**
83 * Class representing temporal components in the UNIX epoch
84 */
85-class Timestamp: public DateTime
86+class DRIZZLED_API Timestamp: public DateTime
87 {
88 public:
89 Timestamp() :DateTime() {}
90@@ -746,7 +746,7 @@
91 * Class representing temporal components in the UNIX epoch
92 * with an additional microsecond component.
93 */
94-class MicroTimestamp: public Timestamp
95+class DRIZZLED_API MicroTimestamp: public Timestamp
96 {
97 public:
98 MicroTimestamp() :Timestamp() {}
99@@ -789,7 +789,7 @@
100 * Class representing temporal components in the UNIX epoch
101 * with an additional nanosecond component.
102 */
103-class NanoTimestamp: public Timestamp
104+class DRIZZLED_API NanoTimestamp: public Timestamp
105 {
106 public:
107 NanoTimestamp() :Timestamp() {}
108
109=== added file 'm4/pandora_have_libv8.m4'
110--- m4/pandora_have_libv8.m4 1970-01-01 00:00:00 +0000
111+++ m4/pandora_have_libv8.m4 2011-10-01 10:16:24 +0000
112@@ -0,0 +1,53 @@
113+dnl Copyright (C) 2009 Sun Microsystems, Inc.
114+dnl This file is free software; Sun Microsystems, Inc.
115+dnl gives unlimited permission to copy and/or distribute it,
116+dnl with or without modifications, as long as this notice is preserved.
117+
118+#--------------------------------------------------------------------
119+# Check for libv8
120+#--------------------------------------------------------------------
121+
122+
123+AC_DEFUN([_PANDORA_SEARCH_LIBV8],[
124+ AC_REQUIRE([AC_LIB_PREFIX])
125+
126+ # v8 is written in C++, need to use g++ for test link below
127+ AC_LANG_CPLUSPLUS
128+
129+ AC_LIB_HAVE_LINKFLAGS(v8, pthread,
130+ [
131+ #include <v8.h>
132+ ],[
133+ v8::HandleScope handle_scope;
134+ ])
135+
136+ AM_CONDITIONAL(HAVE_LIBV8, [test "x${ac_cv_libv8}" = "xyes"])
137+])
138+
139+AC_DEFUN([_PANDORA_HAVE_LIBV8],[
140+ AC_ARG_ENABLE([libv8],
141+ [AS_HELP_STRING([--disable-libv8],
142+ [Build with libv8 support @<:@default=on@:>@])],
143+ [ac_enable_libv8="$enableval"],
144+ [ac_enable_libv8="yes"])
145+
146+ _PANDORA_SEARCH_LIBV8
147+])
148+
149+
150+AC_DEFUN([PANDORA_HAVE_LIBV8],[
151+ AC_REQUIRE([_PANDORA_HAVE_LIBV8])
152+])
153+
154+AC_DEFUN([_PANDORA_REQUIRE_LIBV8],[
155+ ac_enable_libv8="yes"
156+ _PANDORA_SEARCH_LIBV8
157+
158+ AS_IF([test x$ac_cv_libv8 = xno],[
159+ PANDORA_MSG_ERROR([libv8 is required for ${PACKAGE}. On Debian this can be found in libv8-dev. On RedHat this can be found in libv8-devel.])
160+ ])
161+])
162+
163+AC_DEFUN([PANDORA_REQUIRE_LIBV8],[
164+ AC_REQUIRE([_PANDORA_REQUIRE_LIBV8])
165+])
166
167=== added directory 'plugin/js'
168=== added directory 'plugin/js/docs'
169=== added file 'plugin/js/docs/index.rst'
170--- plugin/js/docs/index.rst 1970-01-01 00:00:00 +0000
171+++ plugin/js/docs/index.rst 2011-10-01 10:16:24 +0000
172@@ -0,0 +1,230 @@
173+JS
174+===========
175+
176+.. code-block:: mysql
177+
178+ JS(javascript_code [, arg1 [AS arg_name]] [, ...])
179+
180+``JS()`` executes a JavaScript code snippet and returns the value of the last executed statement. Additional arguments are passed to the JavaScript environment and are available in the ``arguments[]`` array. If the optional ``AS arg_name`` is used, the same argument value is made available as a global variable with that name.
181+
182+
183+.. _js_loading:
184+
185+Loading
186+-------
187+
188+This plugin is loaded by default.
189+
190+If you want to prevent the loading of this plugin, start :program:`drizzled` with::
191+
192+ --plugin-remove=js
193+
194+.. _js_examples:
195+
196+Examples
197+--------
198+
199+The first argument is required and should be a string of valid JavaScript code. The value of the last statement is returned, note that you should not use a ``return`` keyword. This is a top level JavaScript code snippet, not a JavaScript function.
200+
201+.. code-block:: mysql
202+
203+ SELECT JS('var d = new Date(); "Drizzle started running JavaScript at: " + d;');
204+
205+Will output
206+
207++----------------------------------------------------------------------------------+
208+| JS('var d = new Date(); "Drizzle started running JavaScript at: " + d;') |
209++==================================================================================+
210+| Drizzle started running JavaScript at: Mon Aug 29 2011 00:23:31 GMT+0300 (EEST) |
211++----------------------------------------------------------------------------------+
212+
213+
214+Additional arguments are passed to the JavaScript environment and are available in the ``arguments[]`` array.
215+
216+.. code-block:: mysql
217+
218+ SELECT JS("arguments[0] + arguments[1] + arguments[2];", 1, 2, 3) AS 'JS(...)';
219+
220+Will output
221+
222++--------------+
223+| JS(...) |
224++==============+
225+| 6 |
226++--------------+
227+
228+
229+
230+If the optional ``AS arg_name`` is used, the same argument value is made available as a global variable with that name.
231+
232+.. code-block:: mysql
233+
234+ SELECT JS("first + second + third;", 1 AS 'first', 2.0 AS 'second', 3.5 AS 'third') AS 'JS(...)';
235+
236+Will output
237+
238++--------------+
239+| JS(...) |
240++==============+
241+| 6.5 |
242++--------------+
243+
244+.. _json_parse:
245+
246+Using JS() to parse JSON documents
247+-----------------------------------
248+
249+JavaScript includes a JSON parser. This means you can use ``JS()`` as a JSON parser, and optionally use JavaScript to manipulate or select fragments of the JSON document. To do this, pass your JSON document as an argument, and use the ``JSON.parse()`` method to return it as a JavaScript object:
250+
251+.. code-block:: mysql
252+
253+ SELECT JS("var jsondoc = JSON.parse(arguments[0]); jsondoc['name']['firstname'];",
254+ '{ "name" : {"firstname" : "Henrik", "lastname" : "Ingo"} }') AS 'JS(...)';
255+
256+Will output
257+
258++--------------+
259+| JS(...) |
260++==============+
261+| Henrik |
262++--------------+
263+
264+
265+To return a JSON document from JavaScript, use ``JSON.stringify()``:
266+
267+.. code-block:: mysql
268+
269+ SELECT JS("var jsondoc = JSON.parse(arguments[0]);
270+ JSON.stringify(jsondoc['name']);",
271+ '{ "name" : {"firstname" : "Henrik", "lastname" : "Ingo"} }') AS 'JS(...)';
272+
273+
274+Will output
275+
276++------------------------------------------+
277+| JS(...) |
278++==========================================+
279+| {"firstname":"Henrik","lastname":"Ingo"} |
280++------------------------------------------+
281+
282+Note that since a Drizzle function can only return scalar values, if you want to return arrays or objects from your JavaScript, JSON is a recommended way of doing that.
283+
284+.. _js_queries:
285+
286+Using JS in queries, passing columns as arguments
287+-------------------------------------------------
288+
289+Naturally, the arguments can also be columns in a query. For instance in the case of JSON data, if you have stored JSON documents as TEXT or BLOB in a table, you can now use ``JSON.parse()`` to select individual fields out of it:
290+
291+.. code-block:: mysql
292+
293+ CREATE TABLE t (k INT PRIMARY KEY auto_increment, v TEXT);
294+ INSERT INTO t (v) VALUES ('{ "person" : { "firstname" : "Roland", "lastname" : "Bouman" } }');
295+ INSERT INTO t (v) VALUES ('{ "person" : { "firstname" : "Henrik", "lastname" : "Ingo" } }');
296+ INSERT INTO t (v) VALUES ('{ "person" : { "firstname" : "Brian", "lastname" : "Aker" } }');
297+ SELECT JS('var person = JSON.parse(jsondoc); person["person"]["firstname"];',
298+ v as jsondoc) AS 'JS(...)'
299+ FROM t WHERE k=2;
300+
301+
302+Will output
303+
304++--------------+
305+| JS(...) |
306++==============+
307+| Henrik |
308++--------------+
309+
310+
311+And
312+
313+.. code-block:: mysql
314+
315+ SELECT k, JS('var person = JSON.parse(jsondoc); person["person"]["firstname"];',
316+ v as jsondoc) AS 'firstname',
317+ JS('var person = JSON.parse(jsondoc); person["person"]["lastname"];',
318+ v as jsondoc) AS 'lastname'
319+ FROM t;
320+
321+Will break your unstructured JSON data back into a relational table:
322+
323++---+-----------+----------+
324+| k | firstname | lastname |
325++===+===========+==========+
326+| 1 | Roland | Bouman |
327++---+-----------+----------+
328+| 2 | Henrik | Ingo |
329++---+-----------+----------+
330+| 3 | Brian | Aker |
331++---+-----------+----------+
332+
333+.. _js_stored_procedure_surrogate:
334+
335+Using JS as surrogate for stored procedures:
336+--------------------------------------------
337+
338+Especially if the JavaScript you want to use is more complex, it might be a good idea to store the javascript itself in a table in Drizzle, or alternatively a variable. This simplifies queries that use the script:
339+
340+.. code-block:: mysql
341+
342+ CREATE TABLE sp (name VARCHAR(255) PRIMARY KEY, script TEXT);
343+ INSERT INTO sp (name, script) VALUES ('get_person_property', 'var person = JSON.parse(jsondoc); person["person"][property];');
344+ SELECT k, JS( (SELECT script FROM sp WHERE name='get_person_property'),
345+ v as jsondoc, 'firstname' as 'property') AS 'firstname',
346+ JS( (SELECT script FROM sp WHERE name='get_person_property'),
347+ v as jsondoc, 'lastname' as 'property') AS 'lastname'
348+ FROM t;
349+
350+
351+Will output the same result as above:
352+
353++---+-----------+----------+
354+| k | firstname | lastname |
355++===+===========+==========+
356+| 1 | Roland | Bouman |
357++---+-----------+----------+
358+| 2 | Henrik | Ingo |
359++---+-----------+----------+
360+| 3 | Brian | Aker |
361++---+-----------+----------+
362+
363+.. _js_future_work:
364+
365+Limitations and future work
366+---------------------------
367+
368+The current version of ``JS()`` is complete in the sense that any type of arguments (integer, real, decimal, string, date) can be used, JavaScript code can be of arbitrary complexity and scalar values of any type can be returned. However, apart from the input parameters and the return value, there is no way to interact with Drizzle from the JavaScript environment. The plan is that in a future version ``JS()`` will expose some Drizzle API's, such as the ``Execute()`` API, so that one could query Drizzle tables and call other Drizzle functions from the JavaScript environment. This would essentially make JS() a form of JavaScript stored procedures. Of course, a next step after that could be to actually support ``STORED PROCEDURE`` syntax and permissions.
369+
370+Values of type ``DECIMAL`` will be passed as JavaScript ``Double`` values. This may lead to loss of precision. If you want to keep the precision, you must explicitly cast ``DECIMAL`` values into ``CHAR`` when you pass them as arguments. Note that this will affect how the JavaScript ``+`` operator works on the value (string concatenation instead of addition).
371+
372+The current version lacks several obvious performance optimizations. Most importantly the v8 JavaScript engine is single threaded, so heavy use of ``JS()`` on busy production servers is not recommended. A future version will use the v8 Isolate class to run several instances of the single threaded v8 engine.
373+
374+.. _js_authors:
375+
376+Authors
377+-------
378+
379+Henrik Ingo
380+
381+Thanks to Roland Bouman for suggesting to use v8 engine instead of just a JSON parser and for review and comments on JavaScript and JSON conventions.
382+
383+.. _js_version:
384+
385+Version
386+-------
387+
388+This documentation applies to **js 0.9**.
389+
390+To see which version of the plugin a Drizzle server is running, execute:
391+
392+.. code-block:: mysql
393+
394+ SELECT MODULE_VERSION FROM DATA_DICTIONARY.MODULES WHERE MODULE_NAME='js'
395+
396+
397+Changelog
398+---------
399+
400+v0.9
401+^^^^
402+* First release. Complete JS() functionality, but no APIs back to Drizzle are exposed yet and several performance optimizations were left for later release.
403
404=== added file 'plugin/js/js.cc'
405--- plugin/js/js.cc 1970-01-01 00:00:00 +0000
406+++ plugin/js/js.cc 2011-10-01 10:16:24 +0000
407@@ -0,0 +1,371 @@
408+/* -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*-
409+ * vim:expandtab:shiftwidth=2:tabstop=2:smarttab:
410+ *
411+ * Copyright (C) 2011, Henrik Ingo.
412+ *
413+ * This program is free software; you can redistribute it and/or modify
414+ * it under the terms of the GNU General Public License as published by
415+ * the Free Software Foundation; version 2 of the License.
416+ *
417+ * This program is distributed in the hope that it will be useful,
418+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
419+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
420+ * GNU General Public License for more details.
421+ *
422+ * You should have received a copy of the GNU General Public License
423+ * along with this program; if not, write to the Free Software
424+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
425+ */
426+
427+#include <config.h>
428+#include <stdio.h>
429+
430+#include <drizzled/error.h>
431+#include <drizzled/plugin/function.h>
432+#include <drizzled/function/str/strfunc.h>
433+#include <drizzled/temporal.h>
434+
435+#include <v8.h>
436+#define JS_ENGINE "v8"
437+
438+using namespace std;
439+using namespace drizzled;
440+
441+
442+namespace drizzle_plugin {
443+namespace js {
444+
445+v8::Handle<v8::Value> V8Version(const v8::Arguments& args);
446+v8::Handle<v8::Value> JsEngine(const v8::Arguments& args);
447+const char* v8_to_char(const v8::String::Utf8Value& value);
448+void emit_drizzle_error(v8::TryCatch* try_catch);
449+
450+
451+// TODO: So this is a function that returns strings?
452+// What is the class for functions that return mixed types?
453+// Or is this as it should be, apparently js('1') + js('2') does the right thing already.
454+
455+class JsFunction : public Item_str_func
456+{
457+public:
458+ String *val_str(String *);
459+
460+ const char *func_name() const
461+ {
462+ return "js";
463+ }
464+
465+ void fix_length_and_dec()
466+ {
467+ maybe_null= 1;
468+ max_length= MAX_BLOB_WIDTH;
469+ }
470+
471+ bool check_argument_count(int n)
472+ {
473+ return (n >= 1);
474+ }
475+};
476+
477+/**
478+ * @brief Extracts a C string from a V8 Utf8Value
479+ *
480+ * Idea copied from v8 sources, samples/shell.cc. Makes code easier to read than
481+ * (char *)(*utf8value)
482+ */
483+const char* v8_to_char(const v8::String::Utf8Value& value) {
484+ return *value ? *value : "<javascript v8 string conversion failed>";
485+}
486+
487+/**
488+ * @brief Take v8 exception and emit Drizzle error to client
489+ *
490+ * This is adapted from ReportException() in v8 samples/shell.cc.
491+ */
492+void emit_drizzle_error(v8::TryCatch* try_catch)
493+{
494+ v8::HandleScope handle_scope;
495+ v8::String::Utf8Value exception(try_catch->Exception());
496+ const char* exception_string = v8_to_char(exception);
497+ v8::Handle<v8::Message> message = try_catch->Message();
498+ if (message.IsEmpty()) {
499+ // V8 didn't provide any extra information about this error; just
500+ // print the exception.
501+ my_error(ER_SCRIPT, MYF(0), exception_string);
502+ } else {
503+ char buf[2048];
504+ int linenum = message->GetLineNumber();
505+ sprintf(buf, "At line %i: %.1900s (Do SHOW ERRORS for more information.)", linenum, exception_string);
506+ my_error(ER_SCRIPT, MYF(0), buf);
507+ // Print line of source code and where error happened.
508+ v8::String::Utf8Value sourceline(message->GetSourceLine());
509+ const char* sourceline_string = v8_to_char(sourceline);
510+ sprintf(buf, "Line %i: %.160s", linenum, sourceline_string);
511+ my_error(ER_SCRIPT, MYF(0), buf);
512+ int start = message->GetStartColumn();
513+ sprintf(buf, "Check your script starting at: '%.50s'", &sourceline_string[start]);
514+ my_error(ER_SCRIPT, MYF(0), buf);
515+ v8::String::Utf8Value stack_trace(try_catch->StackTrace());
516+ if (stack_trace.length() > 0) {
517+ const char* stack_trace_string = v8_to_char(stack_trace);
518+ my_error(ER_SCRIPT, MYF(0), stack_trace_string);
519+ }
520+ }
521+}
522+
523+/**
524+ * @brief Implements js() - execute JavaScript code
525+ *
526+ * @todo row_result types are not yet handled, what are they anyway?
527+ * @todo Lot's of performance optimizations postponed for later version:
528+ * * When available, use v8::Isolate instead of v8::Locker for multithreading
529+ * (or a mix of both).
530+ * * As part of this work, refactor v8 stuff into separate
531+ * function, proxy, factory or something...
532+ * * Save the compiled script so it can be used again if same script is run
533+ * many times
534+ * * Some of the v8 stuff should be done in initialize()
535+ *
536+ * @note DECIMAL_RESULT type is now a double in JavaScript. This could lose
537+ * precision. But to send them as strings would also be awkward (+ operator will
538+ * do unexpected things). In any case, we'd need some biginteger (bigdecimal?)
539+ * kind of library to do anything with higher precision values anyway. If you
540+ * want to keep the precision, you can cast your decimal values to strings
541+ * explicitly when passing them as arguments.
542+ *
543+ * @param res Pointer to the drizzled::String object that will contain the result
544+ * @return a drizzled::String containing the value returned by executed JavaScript code (value of last executed statement)
545+ */
546+String *JsFunction::val_str( String *str )
547+{
548+ assert( fixed == 1 );
549+ // If we return from any of the error conditions during method, then
550+ // return value of the drizzle function is null.
551+ null_value= true;
552+
553+ String *source_str=NULL;
554+ source_str = args[0]->val_str( str );
555+
556+ // Need to use Locker in multi-threaded app. v8 is unlocked by the destructor
557+ // when locker goes out of scope.
558+ // TODO: Newer versions of v8 provide an Isolate class where you can get a
559+ // separate instance of v8 (ie one per thread). v8 2.5.9.9 in Ubuntu 11.04 does
560+ // not yet offer it.
561+ v8::Locker locker;
562+ // Pass code and arguments into v8...
563+ v8::HandleScope handle_scope;
564+ // Create a template for the global object and populate a drizzle object.
565+ v8::Handle<v8::ObjectTemplate> global = v8::ObjectTemplate::New();
566+ // Drizzle will contain API's to drizzle variables, functions and tables
567+ v8::Handle<v8::ObjectTemplate> db = v8::ObjectTemplate::New();
568+ v8::Handle<v8::ObjectTemplate> js = v8::ObjectTemplate::New();
569+ // Bind the 'version' function
570+ global->Set( v8::String::New("db"), db );
571+ db->Set( v8::String::New("js"), js );
572+ js->Set( v8::String::New("version"), v8::FunctionTemplate::New(V8Version) );
573+ js->Set( v8::String::New("engine"), v8::FunctionTemplate::New(JsEngine) );
574+
575+ // Now bind the arguments into argv[]
576+ // v8::Array can only be created when context is already entered (otherwise v8 segfaults!)
577+ v8::Persistent<v8::Context> context = v8::Context::New( NULL, global );
578+ if ( context.IsEmpty() ) {
579+ char buf[100];
580+ sprintf(buf, "Error in js() while creating JavaScript context in %s.", JS_ENGINE);
581+ my_error(ER_SCRIPT, MYF(0), buf);
582+ return NULL;
583+ }
584+ context->Enter();
585+
586+ v8::Handle<v8::Array> a = v8::Array::New(arg_count-1);
587+ for( uint64_t n = 1; n < arg_count; n++ )
588+ {
589+ // Need to do this differently for ints, doubles and strings
590+ // TODO: There is also ROW_RESULT. Is that relevant here? What does it look like? I could pass rows as an array or object.
591+ if( args[n]->result_type() == INT_RESULT ){
592+ // TODO: Turns out Drizzle doesn't do unsigned. So this code path can never happen? (I can't test it at least...)
593+ if( args[n]->is_unsigned() ) {
594+ a->Set( n-1, v8::Integer::NewFromUnsigned( (uint32_t) args[n]->val_uint() ) );
595+ } else {
596+ a->Set( n-1, v8::Integer::New((int32_t)args[n]->val_int() ) );
597+ }
598+ } else if ( args[n]->result_type() == REAL_RESULT || args[n]->result_type() == DECIMAL_RESULT ) {
599+ a->Set( n-1, v8::Number::New(args[n]->val_real() ) );
600+ } else if ( true || args[n]->result_type() == STRING_RESULT ) {
601+ if ( args[n]->is_datetime() ) {
602+ // DATE/TIME values are also STRING_RESULT, make them a Date type in v8
603+ // Now we need to get the unix timestamp integer, surprisingly tricky...
604+ // TODO: This should really be just args[n]->get_epoch_seconds(). I need to write a separate patch for Item class one of these days.
605+ type::Time ltime;
606+ Timestamp temporal;
607+ args[n]->get_date(ltime, 0);
608+ temporal.set_years(ltime.year);
609+ temporal.set_months(ltime.month);
610+ temporal.set_days(ltime.day);
611+ temporal.set_hours(ltime.hour);
612+ temporal.set_minutes(ltime.minute);
613+ temporal.set_seconds(ltime.second);
614+ temporal.set_epoch_seconds();
615+ if (temporal.is_valid())
616+ {
617+ time_t tmp;
618+ temporal.to_time_t(tmp);
619+ // Pay attention, Ecmascript defines a date as *milliseconds* since unix epoch
620+ // Also, on platforms where time_t is 32 bit, we need explicit cast to 64 bit integer
621+ a->Set( n-1, v8::Date::New(((uint64_t)tmp)*1000) );
622+ } else {
623+ a->Set( n-1, v8::String::New(args[n]->val_str(str)->c_str() ) );
624+ }
625+ } else {
626+ // Default to creating string values in JavaScript
627+ a->Set( n-1, v8::String::New(args[n]->val_str(str)->c_str() ) );
628+ }
629+ }
630+ // If user has given a name to the arguments, pass these as global variables
631+ if( ! args[n]->is_autogenerated_name ) {
632+ if( args[n]->result_type() == INT_RESULT ){
633+ if( args[n]->is_unsigned() ) {
634+ context->Global()->Set( v8::String::New( args[n]->name ), v8::Integer::NewFromUnsigned( (uint32_t) args[n]->val_uint() ) );
635+ } else {
636+ context->Global()->Set( v8::String::New( args[n]->name ), v8::Integer::New((int32_t)args[n]->val_int() ) );
637+ }
638+ } else if ( args[n]->result_type() == REAL_RESULT || args[n]->result_type() == DECIMAL_RESULT ) {
639+ context->Global()->Set( v8::String::New( args[n]->name ), v8::Number::New(args[n]->val_real() ) );
640+ } else if ( true || args[n]->result_type() == STRING_RESULT ) {
641+ if ( args[n]->is_datetime() ) {
642+ // DATE/TIME values are also STRING_RESULT, make them a Date type in v8
643+ // Now we need to get the unix timestamp integer, surprisingly tricky...
644+ // TODO: This should really be just args[n]->get_epoch_seconds(). I need to write a separate patch for Item class one of these days.
645+ type::Time ltime;
646+ Timestamp temporal;
647+ args[n]->get_date(ltime, 0);
648+ temporal.set_years(ltime.year);
649+ temporal.set_months(ltime.month);
650+ temporal.set_days(ltime.day);
651+ temporal.set_hours(ltime.hour);
652+ temporal.set_minutes(ltime.minute);
653+ temporal.set_seconds(ltime.second);
654+ temporal.set_epoch_seconds();
655+ if (temporal.is_valid())
656+ {
657+ time_t tmp;
658+ temporal.to_time_t(tmp);
659+ // Pay attention, Ecmascript defines a date as *milliseconds* since unix epoch
660+ // Also, on platforms where time_t is 32 bit, we need explicit cast to 64 bit integer
661+ context->Global()->Set( v8::String::New( args[n]->name ), v8::Date::New(((uint64_t)tmp)*1000) );
662+ } else {
663+ context->Global()->Set( v8::String::New( args[n]->name ), v8::String::New(args[n]->val_str(str)->c_str() ) );
664+ }
665+ } else {
666+ context->Global()->Set( v8::String::New( args[n]->name ), v8::String::New(args[n]->val_str(str)->c_str() ) );
667+ }
668+ }
669+ }
670+ }
671+ //Need to fetch the global element back from context, global doesn't work anymore
672+ context->Global()->Set( v8::String::New("arguments"), a );
673+
674+
675+
676+ // Compile the source code.
677+ v8::TryCatch try_catch;
678+ v8::Handle<v8::Value> result;
679+ // Create a v8 string containing the JavaScript source code.
680+ // Convert from drizzled::String to char* string to v8::String.
681+ v8::Handle<v8::String> source = v8::String::New(source_str->c_str());
682+ v8::Handle<v8::Script> script = v8::Script::Compile(source);
683+ if ( script.IsEmpty() ) {
684+ emit_drizzle_error(&try_catch);
685+ return NULL;
686+ } else {
687+ result = script->Run();
688+ if ( result.IsEmpty() ) {
689+ assert( try_catch.HasCaught() );
690+ emit_drizzle_error( &try_catch );
691+ // Dispose of Persistent objects before returning. (Is it needed?)
692+ context->Exit();
693+ context.Dispose();
694+ return NULL;
695+ } else {
696+ assert( !try_catch.HasCaught() );
697+ if ( result->IsUndefined() ) {
698+ // Nothing wrong here, but we return Undefined as NULL.
699+ // Dispose of Persistent objects before returning. (Is it needed?)
700+ context->Exit();
701+ context.Dispose();
702+ return NULL;
703+ }
704+ }
705+ }
706+
707+ // Run the script to get the result.
708+ //v8::Handle<v8::Value> foo = script->Run();
709+ v8::Handle<v8::String> rstring = result->ToString();
710+
711+ // Convert the result to a drizzled::String and print it.
712+ // Allocate space to the drizzled::String
713+ str->free(); //TODO: Check the source for alloc(), but apparently I don't need this line?
714+ str->alloc( rstring->Utf8Length() );
715+ // Now copy string from v8 heap to drizzled heap
716+ rstring->WriteUtf8( str->ptr() );
717+ // drizzled::String doesn't actually set string length properly in alloc(), so set it now
718+ str->length( rstring->Utf8Length() );
719+
720+ context->Exit();
721+ context.Dispose();
722+
723+ // There was no error and value returned is not undefined, so it's not null.
724+ null_value= false;
725+ return str;
726+}
727+
728+
729+
730+
731+plugin::Create_function<JsFunction> *js_function = NULL;
732+
733+static int initialize( module::Context &context )
734+{
735+ js_function = new plugin::Create_function<JsFunction>("js");
736+ context.add( js_function );
737+ // Initialize V8
738+ v8::V8::Initialize();
739+ return 0;
740+}
741+
742+
743+/* Functions that are part of the JavaScript API ***************************/
744+
745+/**
746+ * @brief Binds as db.js.version() inside JavaScript.
747+ * @return Version number of v8 engine
748+ */
749+v8::Handle<v8::Value> V8Version( const v8::Arguments& ) {
750+ return v8::String::New( v8::V8::GetVersion() );
751+}
752+
753+/**
754+ * @brief Binds as db.js.engine() inside JavaScript.
755+ * @return The string "v8"
756+ */
757+v8::Handle<v8::Value> JsEngine( const v8::Arguments& ) {
758+ return v8::String::New( JS_ENGINE );
759+}
760+
761+} // namespace js
762+
763+} // namespace drizzle_plugin
764+
765+DRIZZLE_DECLARE_PLUGIN
766+{
767+ DRIZZLE_VERSION_ID,
768+ "js",
769+ "0.9",
770+ "Henrik Ingo",
771+ "Execute JavaScript code with supplied arguments",
772+ PLUGIN_LICENSE_GPL,
773+ drizzle_plugin::js::initialize, /* Plugin Init */
774+ NULL, /* depends */
775+ NULL /* config options */
776+}
777+DRIZZLE_DECLARE_PLUGIN_END;
778+
779\ No newline at end of file
780
781=== added file 'plugin/js/plugin.ini'
782--- plugin/js/plugin.ini 1970-01-01 00:00:00 +0000
783+++ plugin/js/plugin.ini 2011-10-01 10:16:24 +0000
784@@ -0,0 +1,10 @@
785+[plugin]
786+name=js
787+version=0.9
788+author=Henrik Ingo
789+license=PLUGIN_LICENSE_GPL
790+title=Execute JavaScript Code
791+description=Execute JavaScript code with supplied parameters
792+load_by_default=yes
793+build_conditional="$ac_cv_libv8" = "yes"
794+ldflags=${LTLIBV8}
795
796=== added directory 'plugin/js/tests'
797=== added directory 'plugin/js/tests/r'
798=== added file 'plugin/js/tests/r/js.result'
799--- plugin/js/tests/r/js.result 1970-01-01 00:00:00 +0000
800+++ plugin/js/tests/r/js.result 2011-10-01 10:16:24 +0000
801@@ -0,0 +1,35 @@
802+SELECT JS("var foo = 'Hello'; foo + ', World';");
803+JS("var foo = 'Hello'; foo + ', World';")
804+Hello, World
805+SELECT JS("var foo = 'Hello'; foo + ', ' + arguments[0];", "World");
806+JS("var foo = 'Hello'; foo + ', ' + arguments[0];", "World")
807+Hello, World
808+SELECT JS("var foo = 'Hello'; foo + ', ' + bar;", "World" AS 'bar');
809+JS("var foo = 'Hello'; foo + ', ' + bar;", "World" AS 'bar')
810+Hello, World
811+CREATE TABLE jstest (id INT PRIMARY KEY auto_increment, i INT, d DOUBLE, t TIMESTAMP, dt DATETIME);
812+INSERT INTO jstest VALUES (1, -5, 7.5, '2001-02-16 20:38:40', '2011-09-24 22:26:31');
813+SELECT JS("arguments[0] + 1", i) FROM jstest WHERE id=1;
814+JS("arguments[0] + 1", i)
815+-4
816+SELECT JS("arguments[0] + 1.1", d) FROM jstest WHERE id=1;
817+JS("arguments[0] + 1.1", d)
818+8.6
819+SELECT JS("var d = arguments[0]; d.getUTCFullYear() + ' - ' + d.getUTCHours() + ' - ' + + d.getUTCSeconds();", t) FROM jstest WHERE id=1;
820+JS("var d = arguments[0]; d.getUTCFullYear() + ' - ' + d.getUTCHours() + ' - ' + + d.getUTCSeconds();", t)
821+2001 - 20 - 40
822+SELECT JS("var d = arguments[0]; d.getUTCDate() + ' - ' + d.getUTCHours() + ' - ' + d.getUTCMinutes();", dt) FROM jstest WHERE id=1;
823+JS("var d = arguments[0]; d.getUTCDate() + ' - ' + d.getUTCHours() + ' - ' + d.getUTCMinutes();", dt)
824+24 - 22 - 26
825+SELECT JS("var num = arguments[0] + arguments[1]; arguments[2] + num;", i, d, "The sum is: ") FROM jstest WHERE id=1;
826+JS("var num = arguments[0] + arguments[1]; arguments[2] + num;", i, d, "The sum is: ")
827+The sum is: 2.5
828+SELECT JS('var jsondoc = JSON.parse(arguments[0]); JSON.stringify(jsondoc["name"]["firstname"]);', '{ "name" : {"firstname" : "Henrik", "lastname" : "Ingo"} }');
829+JS('var jsondoc = JSON.parse(arguments[0]); JSON.stringify(jsondoc["name"]["firstname"]);', '{ "name" : {"firstname" : "Henrik", "lastname" : "Ingo"} }')
830+"Henrik"
831+DROP TABLE jstest;
832+SELECT JS("this is not javascript");
833+ERROR HY000: Script error: At line 1: SyntaxError: Unexpected identifier (Do SHOW ERRORS for more information.)
834+SELECT JS("var foo = 'Another'; foo + ' thread';");
835+JS("var foo = 'Another'; foo + ' thread';")
836+Another thread
837
838=== added directory 'plugin/js/tests/t'
839=== added file 'plugin/js/tests/t/js.test'
840--- plugin/js/tests/t/js.test 1970-01-01 00:00:00 +0000
841+++ plugin/js/tests/t/js.test 2011-10-01 10:16:24 +0000
842@@ -0,0 +1,35 @@
843+# Basic Hello world test plust testing arguments with and without name
844+SELECT JS("var foo = 'Hello'; foo + ', World';");
845+SELECT JS("var foo = 'Hello'; foo + ', ' + arguments[0];", "World");
846+SELECT JS("var foo = 'Hello'; foo + ', ' + bar;", "World" AS 'bar');
847+
848+# Test all data types are passed correctly as arguments (string was handled above)
849+
850+CREATE TABLE jstest (id INT PRIMARY KEY auto_increment, i INT, d DOUBLE, t TIMESTAMP, dt DATETIME);
851+
852+INSERT INTO jstest VALUES (1, -5, 7.5, '2001-02-16 20:38:40', '2011-09-24 22:26:31');
853+SELECT JS("arguments[0] + 1", i) FROM jstest WHERE id=1;
854+SELECT JS("arguments[0] + 1.1", d) FROM jstest WHERE id=1;
855+SELECT JS("var d = arguments[0]; d.getUTCFullYear() + ' - ' + d.getUTCHours() + ' - ' + + d.getUTCSeconds();", t) FROM jstest WHERE id=1;
856+SELECT JS("var d = arguments[0]; d.getUTCDate() + ' - ' + d.getUTCHours() + ' - ' + d.getUTCMinutes();", dt) FROM jstest WHERE id=1;
857+
858+# Test combinations
859+SELECT JS("var num = arguments[0] + arguments[1]; arguments[2] + num;", i, d, "The sum is: ") FROM jstest WHERE id=1;
860+
861+# And the JSON test, why all this was created in the first place
862+SELECT JS('var jsondoc = JSON.parse(arguments[0]); JSON.stringify(jsondoc["name"]["firstname"]);', '{ "name" : {"firstname" : "Henrik", "lastname" : "Ingo"} }');
863+
864+DROP TABLE jstest;
865+
866+# Make deliberate error
867+--error ER_SCRIPT
868+SELECT JS("this is not javascript");
869+
870+# Why does this crash drizzletest?
871+# SHOW ERRORS;
872+
873+# Make another connection and make sure we are handling multi-threaded mode correctly
874+# (V8 is single threaded only by default.)
875+connect (con2,localhost,test,jstest,mysql);
876+SELECT JS("var foo = 'Another'; foo + ' thread';");
877+disconnect con2;