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

Proposed by Henrik Ingo on 2011-09-23
Status: Merged
Approved by: Brian Aker on 2011-09-26
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 2011-09-23 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 on 2011-09-24
2385. By Henrik Ingo on 2011-09-24

Added tests for JS().

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 on 2011-10-01
2386. By Henrik Ingo on 2011-09-29

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 on 2011-10-01

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 on 2011-10-01

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
=== modified file 'configure.ac'
--- configure.ac 2011-08-16 01:07:54 +0000
+++ configure.ac 2011-10-01 10:16:24 +0000
@@ -155,7 +155,7 @@
155PANDORA_DRIZZLE_BUILD155PANDORA_DRIZZLE_BUILD
156PANDORA_HAVE_BOOST_TEST156PANDORA_HAVE_BOOST_TEST
157PANDORA_HAVE_LIBSQLITE3157PANDORA_HAVE_LIBSQLITE3
158158PANDORA_HAVE_LIBV8
159159
160160
161#########################################################################161#########################################################################
162162
=== modified file 'drizzled/error.cc'
--- drizzled/error.cc 2011-08-16 11:47:29 +0000
+++ drizzled/error.cc 2011-10-01 10:16:24 +0000
@@ -639,6 +639,9 @@
639 ADD_ERROR_MESSAGE(ER_USE_DATA_DICTIONARY, N_("Engine status is now stored in the data_dictionary tables, please use these instead."));639 ADD_ERROR_MESSAGE(ER_USE_DATA_DICTIONARY, N_("Engine status is now stored in the data_dictionary tables, please use these instead."));
640 ADD_ERROR_MESSAGE(ER_TRANSACTION_ALREADY_STARTED, N_("There is already a transaction in progress"));640 ADD_ERROR_MESSAGE(ER_TRANSACTION_ALREADY_STARTED, N_("There is already a transaction in progress"));
641 ADD_ERROR_MESSAGE(ER_NO_LOCK_HELD, N_("No lock is held by this connection."));641 ADD_ERROR_MESSAGE(ER_NO_LOCK_HELD, N_("No lock is held by this connection."));
642
643 // Errors in scripts, such as JavaScript
644 ADD_ERROR_MESSAGE(ER_SCRIPT, N_("Script error: %s"));
642}645}
643646
644} /* namespace drizzled */647} /* namespace drizzled */
645648
=== modified file 'drizzled/error_t.h'
--- drizzled/error_t.h 2011-08-16 11:47:29 +0000
+++ drizzled/error_t.h 2011-10-01 10:16:24 +0000
@@ -866,7 +866,8 @@
866 ER_USE_DATA_DICTIONARY,866 ER_USE_DATA_DICTIONARY,
867 ER_TRANSACTION_ALREADY_STARTED,867 ER_TRANSACTION_ALREADY_STARTED,
868 ER_CARTESIAN_JOIN_ATTEMPTED,868 ER_CARTESIAN_JOIN_ATTEMPTED,
869 ER_NO_LOCK_HELD869 ER_NO_LOCK_HELD,
870 ER_SCRIPT /* Error executing script: (such as JavaScript) */
870};871};
871872
872873
873874
=== modified file 'drizzled/temporal.h'
--- drizzled/temporal.h 2011-03-29 12:45:08 +0000
+++ drizzled/temporal.h 2011-10-01 10:16:24 +0000
@@ -76,7 +76,7 @@
76/**76/**
77 * Base class for all temporal data classes.77 * Base class for all temporal data classes.
78 */78 */
79class Temporal79class DRIZZLED_API Temporal
80{80{
81protected:81protected:
82 enum calendar _calendar;82 enum calendar _calendar;
@@ -188,7 +188,7 @@
188 * Class representing temporal components in a valid188 * Class representing temporal components in a valid
189 * SQL date range, with no time component189 * SQL date range, with no time component
190 */190 */
191class Date: public Temporal191class DRIZZLED_API Date: public Temporal
192{192{
193public:193public:
194 Date() :Temporal() {}194 Date() :Temporal() {}
@@ -435,7 +435,7 @@
435 * Class representing temporal components having only435 * Class representing temporal components having only
436 * a time component, with no date structure436 * a time component, with no date structure
437 */437 */
438class Time: public Temporal438class DRIZZLED_API Time: public Temporal
439{439{
440public:440public:
441 Time() :Temporal() {}441 Time() :Temporal() {}
@@ -567,7 +567,7 @@
567 * Class representing temporal components in a valid567 * Class representing temporal components in a valid
568 * SQL datetime range, including a time component568 * SQL datetime range, including a time component
569 */569 */
570class DateTime: public Date570class DRIZZLED_API DateTime: public Date
571{571{
572public:572public:
573 DateTime() :Date() {}573 DateTime() :Date() {}
@@ -680,7 +680,7 @@
680/**680/**
681 * Class representing temporal components in the UNIX epoch681 * Class representing temporal components in the UNIX epoch
682 */682 */
683class Timestamp: public DateTime683class DRIZZLED_API Timestamp: public DateTime
684{684{
685public:685public:
686 Timestamp() :DateTime() {}686 Timestamp() :DateTime() {}
@@ -746,7 +746,7 @@
746 * Class representing temporal components in the UNIX epoch746 * Class representing temporal components in the UNIX epoch
747 * with an additional microsecond component.747 * with an additional microsecond component.
748 */748 */
749class MicroTimestamp: public Timestamp749class DRIZZLED_API MicroTimestamp: public Timestamp
750{750{
751public:751public:
752 MicroTimestamp() :Timestamp() {}752 MicroTimestamp() :Timestamp() {}
@@ -789,7 +789,7 @@
789 * Class representing temporal components in the UNIX epoch789 * Class representing temporal components in the UNIX epoch
790 * with an additional nanosecond component.790 * with an additional nanosecond component.
791 */791 */
792class NanoTimestamp: public Timestamp792class DRIZZLED_API NanoTimestamp: public Timestamp
793{793{
794public:794public:
795 NanoTimestamp() :Timestamp() {}795 NanoTimestamp() :Timestamp() {}
796796
=== added file 'm4/pandora_have_libv8.m4'
--- m4/pandora_have_libv8.m4 1970-01-01 00:00:00 +0000
+++ m4/pandora_have_libv8.m4 2011-10-01 10:16:24 +0000
@@ -0,0 +1,53 @@
1dnl Copyright (C) 2009 Sun Microsystems, Inc.
2dnl This file is free software; Sun Microsystems, Inc.
3dnl gives unlimited permission to copy and/or distribute it,
4dnl with or without modifications, as long as this notice is preserved.
5
6#--------------------------------------------------------------------
7# Check for libv8
8#--------------------------------------------------------------------
9
10
11AC_DEFUN([_PANDORA_SEARCH_LIBV8],[
12 AC_REQUIRE([AC_LIB_PREFIX])
13
14 # v8 is written in C++, need to use g++ for test link below
15 AC_LANG_CPLUSPLUS
16
17 AC_LIB_HAVE_LINKFLAGS(v8, pthread,
18 [
19 #include <v8.h>
20 ],[
21 v8::HandleScope handle_scope;
22 ])
23
24 AM_CONDITIONAL(HAVE_LIBV8, [test "x${ac_cv_libv8}" = "xyes"])
25])
26
27AC_DEFUN([_PANDORA_HAVE_LIBV8],[
28 AC_ARG_ENABLE([libv8],
29 [AS_HELP_STRING([--disable-libv8],
30 [Build with libv8 support @<:@default=on@:>@])],
31 [ac_enable_libv8="$enableval"],
32 [ac_enable_libv8="yes"])
33
34 _PANDORA_SEARCH_LIBV8
35])
36
37
38AC_DEFUN([PANDORA_HAVE_LIBV8],[
39 AC_REQUIRE([_PANDORA_HAVE_LIBV8])
40])
41
42AC_DEFUN([_PANDORA_REQUIRE_LIBV8],[
43 ac_enable_libv8="yes"
44 _PANDORA_SEARCH_LIBV8
45
46 AS_IF([test x$ac_cv_libv8 = xno],[
47 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.])
48 ])
49])
50
51AC_DEFUN([PANDORA_REQUIRE_LIBV8],[
52 AC_REQUIRE([_PANDORA_REQUIRE_LIBV8])
53])
054
=== added directory 'plugin/js'
=== added directory 'plugin/js/docs'
=== added file 'plugin/js/docs/index.rst'
--- plugin/js/docs/index.rst 1970-01-01 00:00:00 +0000
+++ plugin/js/docs/index.rst 2011-10-01 10:16:24 +0000
@@ -0,0 +1,230 @@
1JS
2===========
3
4.. code-block:: mysql
5
6 JS(javascript_code [, arg1 [AS arg_name]] [, ...])
7
8``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.
9
10
11.. _js_loading:
12
13Loading
14-------
15
16This plugin is loaded by default.
17
18If you want to prevent the loading of this plugin, start :program:`drizzled` with::
19
20 --plugin-remove=js
21
22.. _js_examples:
23
24Examples
25--------
26
27The 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.
28
29.. code-block:: mysql
30
31 SELECT JS('var d = new Date(); "Drizzle started running JavaScript at: " + d;');
32
33Will output
34
35+----------------------------------------------------------------------------------+
36| JS('var d = new Date(); "Drizzle started running JavaScript at: " + d;') |
37+==================================================================================+
38| Drizzle started running JavaScript at: Mon Aug 29 2011 00:23:31 GMT+0300 (EEST) |
39+----------------------------------------------------------------------------------+
40
41
42Additional arguments are passed to the JavaScript environment and are available in the ``arguments[]`` array.
43
44.. code-block:: mysql
45
46 SELECT JS("arguments[0] + arguments[1] + arguments[2];", 1, 2, 3) AS 'JS(...)';
47
48Will output
49
50+--------------+
51| JS(...) |
52+==============+
53| 6 |
54+--------------+
55
56
57
58If the optional ``AS arg_name`` is used, the same argument value is made available as a global variable with that name.
59
60.. code-block:: mysql
61
62 SELECT JS("first + second + third;", 1 AS 'first', 2.0 AS 'second', 3.5 AS 'third') AS 'JS(...)';
63
64Will output
65
66+--------------+
67| JS(...) |
68+==============+
69| 6.5 |
70+--------------+
71
72.. _json_parse:
73
74Using JS() to parse JSON documents
75-----------------------------------
76
77JavaScript 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:
78
79.. code-block:: mysql
80
81 SELECT JS("var jsondoc = JSON.parse(arguments[0]); jsondoc['name']['firstname'];",
82 '{ "name" : {"firstname" : "Henrik", "lastname" : "Ingo"} }') AS 'JS(...)';
83
84Will output
85
86+--------------+
87| JS(...) |
88+==============+
89| Henrik |
90+--------------+
91
92
93To return a JSON document from JavaScript, use ``JSON.stringify()``:
94
95.. code-block:: mysql
96
97 SELECT JS("var jsondoc = JSON.parse(arguments[0]);
98 JSON.stringify(jsondoc['name']);",
99 '{ "name" : {"firstname" : "Henrik", "lastname" : "Ingo"} }') AS 'JS(...)';
100
101
102Will output
103
104+------------------------------------------+
105| JS(...) |
106+==========================================+
107| {"firstname":"Henrik","lastname":"Ingo"} |
108+------------------------------------------+
109
110Note 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.
111
112.. _js_queries:
113
114Using JS in queries, passing columns as arguments
115-------------------------------------------------
116
117Naturally, 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:
118
119.. code-block:: mysql
120
121 CREATE TABLE t (k INT PRIMARY KEY auto_increment, v TEXT);
122 INSERT INTO t (v) VALUES ('{ "person" : { "firstname" : "Roland", "lastname" : "Bouman" } }');
123 INSERT INTO t (v) VALUES ('{ "person" : { "firstname" : "Henrik", "lastname" : "Ingo" } }');
124 INSERT INTO t (v) VALUES ('{ "person" : { "firstname" : "Brian", "lastname" : "Aker" } }');
125 SELECT JS('var person = JSON.parse(jsondoc); person["person"]["firstname"];',
126 v as jsondoc) AS 'JS(...)'
127 FROM t WHERE k=2;
128
129
130Will output
131
132+--------------+
133| JS(...) |
134+==============+
135| Henrik |
136+--------------+
137
138
139And
140
141.. code-block:: mysql
142
143 SELECT k, JS('var person = JSON.parse(jsondoc); person["person"]["firstname"];',
144 v as jsondoc) AS 'firstname',
145 JS('var person = JSON.parse(jsondoc); person["person"]["lastname"];',
146 v as jsondoc) AS 'lastname'
147 FROM t;
148
149Will break your unstructured JSON data back into a relational table:
150
151+---+-----------+----------+
152| k | firstname | lastname |
153+===+===========+==========+
154| 1 | Roland | Bouman |
155+---+-----------+----------+
156| 2 | Henrik | Ingo |
157+---+-----------+----------+
158| 3 | Brian | Aker |
159+---+-----------+----------+
160
161.. _js_stored_procedure_surrogate:
162
163Using JS as surrogate for stored procedures:
164--------------------------------------------
165
166Especially 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:
167
168.. code-block:: mysql
169
170 CREATE TABLE sp (name VARCHAR(255) PRIMARY KEY, script TEXT);
171 INSERT INTO sp (name, script) VALUES ('get_person_property', 'var person = JSON.parse(jsondoc); person["person"][property];');
172 SELECT k, JS( (SELECT script FROM sp WHERE name='get_person_property'),
173 v as jsondoc, 'firstname' as 'property') AS 'firstname',
174 JS( (SELECT script FROM sp WHERE name='get_person_property'),
175 v as jsondoc, 'lastname' as 'property') AS 'lastname'
176 FROM t;
177
178
179Will output the same result as above:
180
181+---+-----------+----------+
182| k | firstname | lastname |
183+===+===========+==========+
184| 1 | Roland | Bouman |
185+---+-----------+----------+
186| 2 | Henrik | Ingo |
187+---+-----------+----------+
188| 3 | Brian | Aker |
189+---+-----------+----------+
190
191.. _js_future_work:
192
193Limitations and future work
194---------------------------
195
196The 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.
197
198Values 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).
199
200The 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.
201
202.. _js_authors:
203
204Authors
205-------
206
207Henrik Ingo
208
209Thanks 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.
210
211.. _js_version:
212
213Version
214-------
215
216This documentation applies to **js 0.9**.
217
218To see which version of the plugin a Drizzle server is running, execute:
219
220.. code-block:: mysql
221
222 SELECT MODULE_VERSION FROM DATA_DICTIONARY.MODULES WHERE MODULE_NAME='js'
223
224
225Changelog
226---------
227
228v0.9
229^^^^
230* First release. Complete JS() functionality, but no APIs back to Drizzle are exposed yet and several performance optimizations were left for later release.
0231
=== added file 'plugin/js/js.cc'
--- plugin/js/js.cc 1970-01-01 00:00:00 +0000
+++ plugin/js/js.cc 2011-10-01 10:16:24 +0000
@@ -0,0 +1,371 @@
1/* -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*-
2 * vim:expandtab:shiftwidth=2:tabstop=2:smarttab:
3 *
4 * Copyright (C) 2011, Henrik Ingo.
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 2 of the License.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 */
19
20#include <config.h>
21#include <stdio.h>
22
23#include <drizzled/error.h>
24#include <drizzled/plugin/function.h>
25#include <drizzled/function/str/strfunc.h>
26#include <drizzled/temporal.h>
27
28#include <v8.h>
29#define JS_ENGINE "v8"
30
31using namespace std;
32using namespace drizzled;
33
34
35namespace drizzle_plugin {
36namespace js {
37
38v8::Handle<v8::Value> V8Version(const v8::Arguments& args);
39v8::Handle<v8::Value> JsEngine(const v8::Arguments& args);
40const char* v8_to_char(const v8::String::Utf8Value& value);
41void emit_drizzle_error(v8::TryCatch* try_catch);
42
43
44// TODO: So this is a function that returns strings?
45// What is the class for functions that return mixed types?
46// Or is this as it should be, apparently js('1') + js('2') does the right thing already.
47
48class JsFunction : public Item_str_func
49{
50public:
51 String *val_str(String *);
52
53 const char *func_name() const
54 {
55 return "js";
56 }
57
58 void fix_length_and_dec()
59 {
60 maybe_null= 1;
61 max_length= MAX_BLOB_WIDTH;
62 }
63
64 bool check_argument_count(int n)
65 {
66 return (n >= 1);
67 }
68};
69
70/**
71 * @brief Extracts a C string from a V8 Utf8Value
72 *
73 * Idea copied from v8 sources, samples/shell.cc. Makes code easier to read than
74 * (char *)(*utf8value)
75 */
76const char* v8_to_char(const v8::String::Utf8Value& value) {
77 return *value ? *value : "<javascript v8 string conversion failed>";
78}
79
80/**
81 * @brief Take v8 exception and emit Drizzle error to client
82 *
83 * This is adapted from ReportException() in v8 samples/shell.cc.
84 */
85void emit_drizzle_error(v8::TryCatch* try_catch)
86{
87 v8::HandleScope handle_scope;
88 v8::String::Utf8Value exception(try_catch->Exception());
89 const char* exception_string = v8_to_char(exception);
90 v8::Handle<v8::Message> message = try_catch->Message();
91 if (message.IsEmpty()) {
92 // V8 didn't provide any extra information about this error; just
93 // print the exception.
94 my_error(ER_SCRIPT, MYF(0), exception_string);
95 } else {
96 char buf[2048];
97 int linenum = message->GetLineNumber();
98 sprintf(buf, "At line %i: %.1900s (Do SHOW ERRORS for more information.)", linenum, exception_string);
99 my_error(ER_SCRIPT, MYF(0), buf);
100 // Print line of source code and where error happened.
101 v8::String::Utf8Value sourceline(message->GetSourceLine());
102 const char* sourceline_string = v8_to_char(sourceline);
103 sprintf(buf, "Line %i: %.160s", linenum, sourceline_string);
104 my_error(ER_SCRIPT, MYF(0), buf);
105 int start = message->GetStartColumn();
106 sprintf(buf, "Check your script starting at: '%.50s'", &sourceline_string[start]);
107 my_error(ER_SCRIPT, MYF(0), buf);
108 v8::String::Utf8Value stack_trace(try_catch->StackTrace());
109 if (stack_trace.length() > 0) {
110 const char* stack_trace_string = v8_to_char(stack_trace);
111 my_error(ER_SCRIPT, MYF(0), stack_trace_string);
112 }
113 }
114}
115
116/**
117 * @brief Implements js() - execute JavaScript code
118 *
119 * @todo row_result types are not yet handled, what are they anyway?
120 * @todo Lot's of performance optimizations postponed for later version:
121 * * When available, use v8::Isolate instead of v8::Locker for multithreading
122 * (or a mix of both).
123 * * As part of this work, refactor v8 stuff into separate
124 * function, proxy, factory or something...
125 * * Save the compiled script so it can be used again if same script is run
126 * many times
127 * * Some of the v8 stuff should be done in initialize()
128 *
129 * @note DECIMAL_RESULT type is now a double in JavaScript. This could lose
130 * precision. But to send them as strings would also be awkward (+ operator will
131 * do unexpected things). In any case, we'd need some biginteger (bigdecimal?)
132 * kind of library to do anything with higher precision values anyway. If you
133 * want to keep the precision, you can cast your decimal values to strings
134 * explicitly when passing them as arguments.
135 *
136 * @param res Pointer to the drizzled::String object that will contain the result
137 * @return a drizzled::String containing the value returned by executed JavaScript code (value of last executed statement)
138 */
139String *JsFunction::val_str( String *str )
140{
141 assert( fixed == 1 );
142 // If we return from any of the error conditions during method, then
143 // return value of the drizzle function is null.
144 null_value= true;
145
146 String *source_str=NULL;
147 source_str = args[0]->val_str( str );
148
149 // Need to use Locker in multi-threaded app. v8 is unlocked by the destructor
150 // when locker goes out of scope.
151 // TODO: Newer versions of v8 provide an Isolate class where you can get a
152 // separate instance of v8 (ie one per thread). v8 2.5.9.9 in Ubuntu 11.04 does
153 // not yet offer it.
154 v8::Locker locker;
155 // Pass code and arguments into v8...
156 v8::HandleScope handle_scope;
157 // Create a template for the global object and populate a drizzle object.
158 v8::Handle<v8::ObjectTemplate> global = v8::ObjectTemplate::New();
159 // Drizzle will contain API's to drizzle variables, functions and tables
160 v8::Handle<v8::ObjectTemplate> db = v8::ObjectTemplate::New();
161 v8::Handle<v8::ObjectTemplate> js = v8::ObjectTemplate::New();
162 // Bind the 'version' function
163 global->Set( v8::String::New("db"), db );
164 db->Set( v8::String::New("js"), js );
165 js->Set( v8::String::New("version"), v8::FunctionTemplate::New(V8Version) );
166 js->Set( v8::String::New("engine"), v8::FunctionTemplate::New(JsEngine) );
167
168 // Now bind the arguments into argv[]
169 // v8::Array can only be created when context is already entered (otherwise v8 segfaults!)
170 v8::Persistent<v8::Context> context = v8::Context::New( NULL, global );
171 if ( context.IsEmpty() ) {
172 char buf[100];
173 sprintf(buf, "Error in js() while creating JavaScript context in %s.", JS_ENGINE);
174 my_error(ER_SCRIPT, MYF(0), buf);
175 return NULL;
176 }
177 context->Enter();
178
179 v8::Handle<v8::Array> a = v8::Array::New(arg_count-1);
180 for( uint64_t n = 1; n < arg_count; n++ )
181 {
182 // Need to do this differently for ints, doubles and strings
183 // TODO: There is also ROW_RESULT. Is that relevant here? What does it look like? I could pass rows as an array or object.
184 if( args[n]->result_type() == INT_RESULT ){
185 // TODO: Turns out Drizzle doesn't do unsigned. So this code path can never happen? (I can't test it at least...)
186 if( args[n]->is_unsigned() ) {
187 a->Set( n-1, v8::Integer::NewFromUnsigned( (uint32_t) args[n]->val_uint() ) );
188 } else {
189 a->Set( n-1, v8::Integer::New((int32_t)args[n]->val_int() ) );
190 }
191 } else if ( args[n]->result_type() == REAL_RESULT || args[n]->result_type() == DECIMAL_RESULT ) {
192 a->Set( n-1, v8::Number::New(args[n]->val_real() ) );
193 } else if ( true || args[n]->result_type() == STRING_RESULT ) {
194 if ( args[n]->is_datetime() ) {
195 // DATE/TIME values are also STRING_RESULT, make them a Date type in v8
196 // Now we need to get the unix timestamp integer, surprisingly tricky...
197 // 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.
198 type::Time ltime;
199 Timestamp temporal;
200 args[n]->get_date(ltime, 0);
201 temporal.set_years(ltime.year);
202 temporal.set_months(ltime.month);
203 temporal.set_days(ltime.day);
204 temporal.set_hours(ltime.hour);
205 temporal.set_minutes(ltime.minute);
206 temporal.set_seconds(ltime.second);
207 temporal.set_epoch_seconds();
208 if (temporal.is_valid())
209 {
210 time_t tmp;
211 temporal.to_time_t(tmp);
212 // Pay attention, Ecmascript defines a date as *milliseconds* since unix epoch
213 // Also, on platforms where time_t is 32 bit, we need explicit cast to 64 bit integer
214 a->Set( n-1, v8::Date::New(((uint64_t)tmp)*1000) );
215 } else {
216 a->Set( n-1, v8::String::New(args[n]->val_str(str)->c_str() ) );
217 }
218 } else {
219 // Default to creating string values in JavaScript
220 a->Set( n-1, v8::String::New(args[n]->val_str(str)->c_str() ) );
221 }
222 }
223 // If user has given a name to the arguments, pass these as global variables
224 if( ! args[n]->is_autogenerated_name ) {
225 if( args[n]->result_type() == INT_RESULT ){
226 if( args[n]->is_unsigned() ) {
227 context->Global()->Set( v8::String::New( args[n]->name ), v8::Integer::NewFromUnsigned( (uint32_t) args[n]->val_uint() ) );
228 } else {
229 context->Global()->Set( v8::String::New( args[n]->name ), v8::Integer::New((int32_t)args[n]->val_int() ) );
230 }
231 } else if ( args[n]->result_type() == REAL_RESULT || args[n]->result_type() == DECIMAL_RESULT ) {
232 context->Global()->Set( v8::String::New( args[n]->name ), v8::Number::New(args[n]->val_real() ) );
233 } else if ( true || args[n]->result_type() == STRING_RESULT ) {
234 if ( args[n]->is_datetime() ) {
235 // DATE/TIME values are also STRING_RESULT, make them a Date type in v8
236 // Now we need to get the unix timestamp integer, surprisingly tricky...
237 // 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.
238 type::Time ltime;
239 Timestamp temporal;
240 args[n]->get_date(ltime, 0);
241 temporal.set_years(ltime.year);
242 temporal.set_months(ltime.month);
243 temporal.set_days(ltime.day);
244 temporal.set_hours(ltime.hour);
245 temporal.set_minutes(ltime.minute);
246 temporal.set_seconds(ltime.second);
247 temporal.set_epoch_seconds();
248 if (temporal.is_valid())
249 {
250 time_t tmp;
251 temporal.to_time_t(tmp);
252 // Pay attention, Ecmascript defines a date as *milliseconds* since unix epoch
253 // Also, on platforms where time_t is 32 bit, we need explicit cast to 64 bit integer
254 context->Global()->Set( v8::String::New( args[n]->name ), v8::Date::New(((uint64_t)tmp)*1000) );
255 } else {
256 context->Global()->Set( v8::String::New( args[n]->name ), v8::String::New(args[n]->val_str(str)->c_str() ) );
257 }
258 } else {
259 context->Global()->Set( v8::String::New( args[n]->name ), v8::String::New(args[n]->val_str(str)->c_str() ) );
260 }
261 }
262 }
263 }
264 //Need to fetch the global element back from context, global doesn't work anymore
265 context->Global()->Set( v8::String::New("arguments"), a );
266
267
268
269 // Compile the source code.
270 v8::TryCatch try_catch;
271 v8::Handle<v8::Value> result;
272 // Create a v8 string containing the JavaScript source code.
273 // Convert from drizzled::String to char* string to v8::String.
274 v8::Handle<v8::String> source = v8::String::New(source_str->c_str());
275 v8::Handle<v8::Script> script = v8::Script::Compile(source);
276 if ( script.IsEmpty() ) {
277 emit_drizzle_error(&try_catch);
278 return NULL;
279 } else {
280 result = script->Run();
281 if ( result.IsEmpty() ) {
282 assert( try_catch.HasCaught() );
283 emit_drizzle_error( &try_catch );
284 // Dispose of Persistent objects before returning. (Is it needed?)
285 context->Exit();
286 context.Dispose();
287 return NULL;
288 } else {
289 assert( !try_catch.HasCaught() );
290 if ( result->IsUndefined() ) {
291 // Nothing wrong here, but we return Undefined as NULL.
292 // Dispose of Persistent objects before returning. (Is it needed?)
293 context->Exit();
294 context.Dispose();
295 return NULL;
296 }
297 }
298 }
299
300 // Run the script to get the result.
301 //v8::Handle<v8::Value> foo = script->Run();
302 v8::Handle<v8::String> rstring = result->ToString();
303
304 // Convert the result to a drizzled::String and print it.
305 // Allocate space to the drizzled::String
306 str->free(); //TODO: Check the source for alloc(), but apparently I don't need this line?
307 str->alloc( rstring->Utf8Length() );
308 // Now copy string from v8 heap to drizzled heap
309 rstring->WriteUtf8( str->ptr() );
310 // drizzled::String doesn't actually set string length properly in alloc(), so set it now
311 str->length( rstring->Utf8Length() );
312
313 context->Exit();
314 context.Dispose();
315
316 // There was no error and value returned is not undefined, so it's not null.
317 null_value= false;
318 return str;
319}
320
321
322
323
324plugin::Create_function<JsFunction> *js_function = NULL;
325
326static int initialize( module::Context &context )
327{
328 js_function = new plugin::Create_function<JsFunction>("js");
329 context.add( js_function );
330 // Initialize V8
331 v8::V8::Initialize();
332 return 0;
333}
334
335
336/* Functions that are part of the JavaScript API ***************************/
337
338/**
339 * @brief Binds as db.js.version() inside JavaScript.
340 * @return Version number of v8 engine
341 */
342v8::Handle<v8::Value> V8Version( const v8::Arguments& ) {
343 return v8::String::New( v8::V8::GetVersion() );
344}
345
346/**
347 * @brief Binds as db.js.engine() inside JavaScript.
348 * @return The string "v8"
349 */
350v8::Handle<v8::Value> JsEngine( const v8::Arguments& ) {
351 return v8::String::New( JS_ENGINE );
352}
353
354} // namespace js
355
356} // namespace drizzle_plugin
357
358DRIZZLE_DECLARE_PLUGIN
359{
360 DRIZZLE_VERSION_ID,
361 "js",
362 "0.9",
363 "Henrik Ingo",
364 "Execute JavaScript code with supplied arguments",
365 PLUGIN_LICENSE_GPL,
366 drizzle_plugin::js::initialize, /* Plugin Init */
367 NULL, /* depends */
368 NULL /* config options */
369}
370DRIZZLE_DECLARE_PLUGIN_END;
371
0\ No newline at end of file372\ No newline at end of file
1373
=== added file 'plugin/js/plugin.ini'
--- plugin/js/plugin.ini 1970-01-01 00:00:00 +0000
+++ plugin/js/plugin.ini 2011-10-01 10:16:24 +0000
@@ -0,0 +1,10 @@
1[plugin]
2name=js
3version=0.9
4author=Henrik Ingo
5license=PLUGIN_LICENSE_GPL
6title=Execute JavaScript Code
7description=Execute JavaScript code with supplied parameters
8load_by_default=yes
9build_conditional="$ac_cv_libv8" = "yes"
10ldflags=${LTLIBV8}
011
=== added directory 'plugin/js/tests'
=== added directory 'plugin/js/tests/r'
=== added file 'plugin/js/tests/r/js.result'
--- plugin/js/tests/r/js.result 1970-01-01 00:00:00 +0000
+++ plugin/js/tests/r/js.result 2011-10-01 10:16:24 +0000
@@ -0,0 +1,35 @@
1SELECT JS("var foo = 'Hello'; foo + ', World';");
2JS("var foo = 'Hello'; foo + ', World';")
3Hello, World
4SELECT JS("var foo = 'Hello'; foo + ', ' + arguments[0];", "World");
5JS("var foo = 'Hello'; foo + ', ' + arguments[0];", "World")
6Hello, World
7SELECT JS("var foo = 'Hello'; foo + ', ' + bar;", "World" AS 'bar');
8JS("var foo = 'Hello'; foo + ', ' + bar;", "World" AS 'bar')
9Hello, World
10CREATE TABLE jstest (id INT PRIMARY KEY auto_increment, i INT, d DOUBLE, t TIMESTAMP, dt DATETIME);
11INSERT INTO jstest VALUES (1, -5, 7.5, '2001-02-16 20:38:40', '2011-09-24 22:26:31');
12SELECT JS("arguments[0] + 1", i) FROM jstest WHERE id=1;
13JS("arguments[0] + 1", i)
14-4
15SELECT JS("arguments[0] + 1.1", d) FROM jstest WHERE id=1;
16JS("arguments[0] + 1.1", d)
178.6
18SELECT JS("var d = arguments[0]; d.getUTCFullYear() + ' - ' + d.getUTCHours() + ' - ' + + d.getUTCSeconds();", t) FROM jstest WHERE id=1;
19JS("var d = arguments[0]; d.getUTCFullYear() + ' - ' + d.getUTCHours() + ' - ' + + d.getUTCSeconds();", t)
202001 - 20 - 40
21SELECT JS("var d = arguments[0]; d.getUTCDate() + ' - ' + d.getUTCHours() + ' - ' + d.getUTCMinutes();", dt) FROM jstest WHERE id=1;
22JS("var d = arguments[0]; d.getUTCDate() + ' - ' + d.getUTCHours() + ' - ' + d.getUTCMinutes();", dt)
2324 - 22 - 26
24SELECT JS("var num = arguments[0] + arguments[1]; arguments[2] + num;", i, d, "The sum is: ") FROM jstest WHERE id=1;
25JS("var num = arguments[0] + arguments[1]; arguments[2] + num;", i, d, "The sum is: ")
26The sum is: 2.5
27SELECT JS('var jsondoc = JSON.parse(arguments[0]); JSON.stringify(jsondoc["name"]["firstname"]);', '{ "name" : {"firstname" : "Henrik", "lastname" : "Ingo"} }');
28JS('var jsondoc = JSON.parse(arguments[0]); JSON.stringify(jsondoc["name"]["firstname"]);', '{ "name" : {"firstname" : "Henrik", "lastname" : "Ingo"} }')
29"Henrik"
30DROP TABLE jstest;
31SELECT JS("this is not javascript");
32ERROR HY000: Script error: At line 1: SyntaxError: Unexpected identifier (Do SHOW ERRORS for more information.)
33SELECT JS("var foo = 'Another'; foo + ' thread';");
34JS("var foo = 'Another'; foo + ' thread';")
35Another thread
036
=== added directory 'plugin/js/tests/t'
=== added file 'plugin/js/tests/t/js.test'
--- plugin/js/tests/t/js.test 1970-01-01 00:00:00 +0000
+++ plugin/js/tests/t/js.test 2011-10-01 10:16:24 +0000
@@ -0,0 +1,35 @@
1# Basic Hello world test plust testing arguments with and without name
2SELECT JS("var foo = 'Hello'; foo + ', World';");
3SELECT JS("var foo = 'Hello'; foo + ', ' + arguments[0];", "World");
4SELECT JS("var foo = 'Hello'; foo + ', ' + bar;", "World" AS 'bar');
5
6# Test all data types are passed correctly as arguments (string was handled above)
7
8CREATE TABLE jstest (id INT PRIMARY KEY auto_increment, i INT, d DOUBLE, t TIMESTAMP, dt DATETIME);
9
10INSERT INTO jstest VALUES (1, -5, 7.5, '2001-02-16 20:38:40', '2011-09-24 22:26:31');
11SELECT JS("arguments[0] + 1", i) FROM jstest WHERE id=1;
12SELECT JS("arguments[0] + 1.1", d) FROM jstest WHERE id=1;
13SELECT JS("var d = arguments[0]; d.getUTCFullYear() + ' - ' + d.getUTCHours() + ' - ' + + d.getUTCSeconds();", t) FROM jstest WHERE id=1;
14SELECT JS("var d = arguments[0]; d.getUTCDate() + ' - ' + d.getUTCHours() + ' - ' + d.getUTCMinutes();", dt) FROM jstest WHERE id=1;
15
16# Test combinations
17SELECT JS("var num = arguments[0] + arguments[1]; arguments[2] + num;", i, d, "The sum is: ") FROM jstest WHERE id=1;
18
19# And the JSON test, why all this was created in the first place
20SELECT JS('var jsondoc = JSON.parse(arguments[0]); JSON.stringify(jsondoc["name"]["firstname"]);', '{ "name" : {"firstname" : "Henrik", "lastname" : "Ingo"} }');
21
22DROP TABLE jstest;
23
24# Make deliberate error
25--error ER_SCRIPT
26SELECT JS("this is not javascript");
27
28# Why does this crash drizzletest?
29# SHOW ERRORS;
30
31# Make another connection and make sure we are handling multi-threaded mode correctly
32# (V8 is single threaded only by default.)
33connect (con2,localhost,test,jstest,mysql);
34SELECT JS("var foo = 'Another'; foo + ' thread';");
35disconnect con2;