actn-db 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b41ff0c4461fa9cde659fe55e7ac9b2cd974350d
4
+ data.tar.gz: 94e83df6646636f8ec9eb822ccbe3675189a6538
5
+ SHA512:
6
+ metadata.gz: e1972c75f05e55e51c5ccabb2ab5a29bf4e748f206c675cae168b798d75a7ca41ea14d06ec906ad0496249c5762c3fc1494aef2af1793f21129333caced63bdc
7
+ data.tar.gz: 1d6c1ceaf715844dd3dbd893df4bb273362307dbe0432c50173820ac1a1d6711c28c6e256b558a0af92bb23a424818ed8347c1cfdf938466f3ee3b3b87ee5dbd
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in actn-db.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Onur Uyar
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Actn::DB
2
+
3
+ Handy PLV8 Tools
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'actn-db'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install actn-db
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it ( http://github.com/<my-github-username>/actn-db/fork )
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/**/test*.rb']
7
+ end
8
+
9
+ task :default => :test
10
+
11
+ ENV['DATABASE_URL'] ||= "postgres://localhost:5432/actn_#{ENV['RACK_ENV'] ||= "development"}"
12
+ load "actn/db/tasks/db.rake"
data/actn-db.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'actn/db/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "actn-db"
8
+ spec.version = Actn::DB::VERSION
9
+ spec.authors = ["Onur Uyar"]
10
+ spec.email = ["me@onuruyar.com"]
11
+ spec.summary = %q{Actn.io DB}
12
+ spec.homepage = "https://github.com/hackberry-gh/actn-db"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.5"
21
+ spec.add_development_dependency "rake"
22
+ spec.add_development_dependency "minitest"
23
+ spec.add_development_dependency "minitest-reporters"
24
+
25
+ spec.add_dependency "coffee-script"
26
+ spec.add_dependency "em-pg-client"
27
+ spec.add_dependency "activemodel"
28
+ spec.add_dependency "oj"
29
+ spec.add_dependency "bcrypt"
30
+ end
data/db/1_db.sql ADDED
@@ -0,0 +1,54 @@
1
+ CREATE SCHEMA core;
2
+ SET search_path TO core,public;
3
+
4
+ SELECT plv8_startup();
5
+
6
+ SELECT __create_table('core','models');
7
+ SELECT __create_index('core','models', '{"cols": {"name": "text"},"unique": true}');
8
+
9
+
10
+ CREATE or REPLACE FUNCTION model_callbacks() RETURNS trigger AS
11
+ $$
12
+ table_name = (NEW?.data?.name or OLD?.data?.name)?.tableize()
13
+ table_schema = (NEW?.data?.table_schema or OLD?.data?.table_schema) or "public"
14
+
15
+ return if table_schema is "core"
16
+
17
+ # plv8.elog(NOTICE,"MODEL CALLBACKS",table_schema,JSON.stringify(NEW?.data or OLD?.data))
18
+
19
+ mapper = (ind) -> _.keys(ind.cols)
20
+ differ = (_ind) ->
21
+ (ind) ->
22
+ _.isEmpty( _.difference( _.keys(ind.cols), _.flatten( _.map( _ind.data?.indexes, mapper ) ) ) )
23
+
24
+ switch TG_OP
25
+ when "INSERT"
26
+ plv8.execute "SELECT __create_table($1,$2)",[table_schema , table_name]
27
+
28
+ plv8.execute "SELECT __create_index($1,$2,$3)", [table_schema, table_name, {cols: {path: "text" }}]
29
+
30
+ for indopts in NEW?.data?.indexes or []
31
+ plv8.execute "SELECT __create_index($1,$2,$3)", [table_schema, table_name, indopts]
32
+
33
+ when "UPDATE"
34
+
35
+ diff = _.reject( OLD?.data?.indexes, differ(NEW) )
36
+
37
+ for indopts in diff
38
+ plv8.execute "SELECT __drop_index($1,$2,$3)", [table_schema, table_name, indopts]
39
+
40
+ diff = _.reject( NEW?.data?.indexes, differ(OLD) )
41
+
42
+ for indopts in diff
43
+ plv8.execute "SELECT __create_index($1,$2,$3)", [table_schema, table_name, indopts]
44
+
45
+ when "DELETE"
46
+ for indopts in Old?.data?.indexes or []
47
+ plv8.execute "SELECT __drop_index($1,$2,$3)", [table_schema, table_name, indopts]
48
+ plv8.execute "SELECT __drop_table($1,$2)",[table_schema , table_name]
49
+
50
+ $$ LANGUAGE plcoffee STABLE STRICT;
51
+
52
+ CREATE TRIGGER core_models_callback_trigger
53
+ AFTER INSERT OR UPDATE OR DELETE ON core.models
54
+ FOR EACH ROW EXECUTE PROCEDURE model_callbacks();
@@ -0,0 +1,472 @@
1
+ -- PLV8 Functions
2
+
3
+ CREATE or REPLACE FUNCTION __json(_data json, _key text) RETURNS JSON AS $$
4
+ ret = actn.valueAt(_data,_key)
5
+ return null unless ret?
6
+ return JSON.stringify(ret)
7
+
8
+ $$ LANGUAGE plcoffee STABLE STRICT;
9
+
10
+
11
+
12
+
13
+
14
+ CREATE or REPLACE FUNCTION __string(_data json, _key text) RETURNS TEXT AS $$
15
+ ret = actn.valueAt(_data,_key)
16
+ return null unless ret?
17
+ return ret.toString()
18
+
19
+ $$ LANGUAGE plcoffee IMMUTABLE STRICT;
20
+
21
+
22
+
23
+
24
+
25
+ CREATE or REPLACE FUNCTION __integer(_data json, _key text) RETURNS INT AS $$
26
+ ret = actn.valueAt(_data,_key)
27
+ return null unless ret?
28
+ return parseInt(ret)
29
+
30
+ $$ LANGUAGE plcoffee IMMUTABLE STRICT;
31
+
32
+
33
+
34
+
35
+
36
+ CREATE or REPLACE FUNCTION __integer_array(_data json, _key text) RETURNS INT[] AS $$
37
+ ret = actn.valueAt(_data,_key)
38
+ return null unless ret?
39
+ return (if ret instanceof Array then ret else [ret])
40
+
41
+ $$ LANGUAGE plcoffee IMMUTABLE STRICT;
42
+
43
+
44
+
45
+
46
+
47
+ CREATE or REPLACE FUNCTION __float(_data json, _key text) RETURNS DOUBLE PRECISION AS $$
48
+ ret = actn.valueAt(_data,_key)
49
+ return null unless ret?
50
+ return parseFloat(ret)
51
+
52
+ $$ LANGUAGE plcoffee IMMUTABLE STRICT;
53
+
54
+
55
+
56
+
57
+
58
+ CREATE or REPLACE FUNCTION __bool(_data json, _key text) RETURNS BOOLEAN AS $$
59
+ ret = actn.valueAt(_data,_key)
60
+ return null unless ret?
61
+ return !!ret
62
+
63
+ $$ LANGUAGE plcoffee IMMUTABLE STRICT;
64
+
65
+
66
+
67
+
68
+
69
+ CREATE or REPLACE FUNCTION __timestamp(_data json, _key text) RETURNS TIMESTAMP AS $$
70
+ ret = actn.valueAt(_data,_key)
71
+ return null unless ret?
72
+ return new Date(ret)
73
+
74
+ $$ LANGUAGE plcoffee IMMUTABLE STRICT;
75
+
76
+
77
+
78
+
79
+
80
+ CREATE or REPLACE FUNCTION __patch(_data json, _value json, _sync boolean) RETURNS JSON AS $$
81
+
82
+ data = _data
83
+ changes = _value
84
+ isObject = false
85
+
86
+ sync = if _sync? then _sync else true
87
+
88
+ defaults = _.pick( data, _.keys( JSON.parse( plv8.find_function('__defaults')() ) ) )
89
+
90
+ for k of changes
91
+ if data.hasOwnProperty(k)
92
+ isObject = typeof (data[k]) is "object" and typeof (changes[k]) is "object"
93
+ data[k] = if isObject and sync then _.extend(data[k], changes[k]) else changes[k]
94
+ else
95
+ data[k] = changes[k]
96
+
97
+ unless sync
98
+ for k of data
99
+ delete data[k] unless changes[k]?
100
+
101
+ _.extend(data, defaults)
102
+
103
+ return JSON.stringify(data)
104
+
105
+ $$ LANGUAGE plcoffee STABLE STRICT;
106
+
107
+
108
+
109
+
110
+
111
+ CREATE or REPLACE FUNCTION __select(_data json, _fields text) RETURNS JSON AS $$
112
+
113
+ data = _data
114
+ fields = _fields
115
+ ret = _.pick(data,fields.split(","))
116
+
117
+ return JSON.stringify(ret)
118
+
119
+ $$ LANGUAGE plcoffee STABLE STRICT;
120
+
121
+
122
+
123
+
124
+
125
+ CREATE or REPLACE FUNCTION __push(_data json, _key text, _value json) RETURNS JSON AS $$
126
+
127
+ data = _data
128
+ value = _value
129
+ keys = _key.split(".")
130
+ len = keys.length
131
+ last_field = data
132
+ field = data
133
+ i = 0
134
+
135
+ while i < len
136
+ last_field = field
137
+ field = field[keys[i]] if field
138
+ ++i
139
+ if field
140
+ field.push value
141
+ else
142
+ value = [value] unless value instanceof Array
143
+ last_field[keys.pop()] = value
144
+
145
+ return JSON.stringify(data)
146
+
147
+ $$ LANGUAGE plcoffee STABLE STRICT;
148
+
149
+
150
+
151
+
152
+
153
+ CREATE or REPLACE FUNCTION __uuid() RETURNS JSON AS $$
154
+
155
+ ary = plv8.execute 'SELECT uuid_generate_v4() as uuid;'
156
+ return JSON.stringify(ary[0])
157
+
158
+ $$ LANGUAGE plcoffee STABLE STRICT;
159
+
160
+
161
+
162
+
163
+
164
+
165
+ CREATE or REPLACE FUNCTION __defaults() RETURNS JSON AS $$
166
+
167
+ uuid = JSON.parse(plv8.find_function('__uuid')())
168
+ timestamp = new Date()
169
+ return JSON.stringify({uuid: uuid.uuid, created_at: timestamp, updated_at: timestamp})
170
+
171
+ $$ LANGUAGE plcoffee STABLE STRICT;
172
+
173
+
174
+
175
+
176
+
177
+ CREATE or REPLACE FUNCTION __create_table(schema_name text, table_name text) RETURNS JSON AS $$
178
+
179
+ plv8.execute """
180
+ CREATE TABLE #{schema_name}.#{table_name} (
181
+ id serial NOT NULL,
182
+ data json DEFAULT __uuid() NOT NULL,
183
+ CONSTRAINT #{schema_name}_#{table_name}_pkey PRIMARY KEY (id));
184
+
185
+ CREATE UNIQUE INDEX indx_#{schema_name}_#{table_name}_unique_uuid ON #{schema_name}.#{table_name} (__string(data,'uuid'));
186
+ """
187
+ return JSON.stringify(table_name)
188
+
189
+ $$ LANGUAGE plcoffee STABLE STRICT;
190
+
191
+
192
+
193
+
194
+
195
+ CREATE or REPLACE FUNCTION __drop_table(schema_name text, table_name text) RETURNS JSON AS $$
196
+
197
+ plv8.execute "DROP TABLE IF EXISTS #{schema_name}.#{table_name} CASCADE;"
198
+ return JSON.stringify(table_name)
199
+
200
+ $$ LANGUAGE plcoffee STABLE STRICT;
201
+
202
+
203
+
204
+
205
+
206
+ CREATE or REPLACE FUNCTION __create_index(schema_name text, table_name text, optns json) RETURNS JSON AS $$
207
+
208
+ index_name = "indx_#{schema_name}_#{table_name}"
209
+ for name, type of optns.cols
210
+ index_name += "_#{name}"
211
+
212
+ sql = ["CREATE"]
213
+ sql.push "UNIQUE" if optns.unique
214
+ sql.push "INDEX"
215
+ sql.push "CONCURRENTLY" if optns.concurrently
216
+ sql.push "#{index_name} on #{schema_name}.#{table_name}"
217
+ sql.push "("
218
+ cols = []
219
+ for name, type of optns.cols
220
+ meth = "__#{if type is 'text' then 'string' else type}"
221
+ cols.push "#{meth}(data,'#{name}'::#{type})"
222
+ sql.push cols.join(",")
223
+ sql.push ")"
224
+
225
+ sql = sql.join(" ")
226
+
227
+ plv8.execute(sql)
228
+
229
+ return JSON.stringify(index_name)
230
+
231
+ $$ LANGUAGE plcoffee STABLE STRICT;
232
+
233
+
234
+
235
+
236
+
237
+ CREATE or REPLACE FUNCTION __drop_index(schema_name text, table_name text, optns json) RETURNS JSON AS $$
238
+
239
+ index_name = "indx_#{schema_name}_#{table_name}"
240
+ for name, type of optns.cols
241
+ index_name += "_#{name}"
242
+
243
+ plv8.execute("DROP INDEX IF EXISTS #{index_name}")
244
+
245
+ return JSON.stringify(index_name)
246
+
247
+ $$ LANGUAGE plcoffee STABLE STRICT;
248
+
249
+
250
+
251
+
252
+
253
+
254
+ -- ##
255
+ -- # Select data
256
+ -- # SELECT query(_schema_name, _table_name, {where: {uuid: "12345"}});
257
+
258
+ CREATE or REPLACE FUNCTION __query(_schema_name text, _table_name text, _query json) RETURNS json AS $$
259
+
260
+ search_path = if _schema_name is "public" then _schema_name else "#{_schema_name}, public"
261
+
262
+ builder = new actn.Builder(_schema_name, _table_name, search_path, _query)
263
+
264
+ [sql,params] = builder.build_select()
265
+
266
+ rows = plv8.execute(sql,params)
267
+
268
+ builder = null
269
+
270
+ if _query?.select?.indexOf('COUNT') > -1
271
+ result = rows
272
+ else
273
+ result = _.pluck(rows,'data')
274
+
275
+
276
+ return JSON.stringify(result)
277
+
278
+ $$ LANGUAGE plcoffee STABLE;
279
+
280
+
281
+
282
+
283
+
284
+ -- ##
285
+ -- # Insert ot update row through validation!
286
+ -- # SELECT upsert(validate('User', '{"name":"foo"}'));
287
+
288
+ CREATE or REPLACE FUNCTION __upsert(_schema_name text, _table_name text, _data json) RETURNS json AS $$
289
+
290
+ # plv8.elog(NOTICE,"UPSERT",JSON.stringify(_data))
291
+
292
+ return JSON.stringify(_data) if _data.errors?
293
+
294
+ data = _data
295
+
296
+ search_path = if _schema_name is "public" then _schema_name else "#{_schema_name},public"
297
+
298
+ if data.uuid?
299
+
300
+ query = { where: { uuid: data.uuid } }
301
+
302
+ builder = new actn.Builder(_schema_name, _table_name, search_path, query )
303
+
304
+ [sql,params] = builder.build_update(data)
305
+
306
+ else
307
+
308
+ builder = new actn.Builder(_schema_name, _table_name, search_path, {})
309
+
310
+ [sql,params] = builder.build_insert(data)
311
+
312
+
313
+ # plan = plv8.prepare(sql, ['json','bool','text'])
314
+
315
+ # plv8.elog(NOTICE,sql,JSON.stringify(params))
316
+
317
+ rows = plv8.execute(sql, params)
318
+
319
+ result = _.pluck(rows,'data')
320
+
321
+ result = result[0] if result.length is 1
322
+
323
+ builder = null
324
+
325
+ return JSON.stringify(result)
326
+
327
+ $$ LANGUAGE plcoffee STABLE STRICT;
328
+
329
+
330
+
331
+
332
+
333
+ -- ##
334
+ -- # Delete single row by uuid
335
+ -- # SELECT remove('users',uuid-1234567);
336
+
337
+ CREATE or REPLACE FUNCTION __update(_schema_name text, _table_name text, _data json, _cond json) RETURNS json AS $$
338
+
339
+ return JSON.stringify(_data) if _data.errors?
340
+
341
+ search_path = if _schema_name is "public" then _schema_name else "#{_schema_name},public"
342
+
343
+ builder = new actn.Builder(_schema_name, _table_name, search_path, {where: _cond})
344
+
345
+ [sql,params] = builder.build_update(_data)
346
+
347
+ rows = plv8.execute(sql,params)
348
+ result = _.pluck(rows,'data')
349
+ result = result[0] if result.length is 1
350
+
351
+ builder = null
352
+
353
+ return JSON.stringify(result)
354
+
355
+ $$ LANGUAGE plcoffee STABLE STRICT;
356
+
357
+
358
+
359
+
360
+
361
+ -- ##
362
+ -- # Delete single row by uuid
363
+ -- # SELECT remove('users',uuid-1234567);
364
+
365
+ CREATE or REPLACE FUNCTION __delete(_schema_name text, _table_name text, _cond json) RETURNS json AS $$
366
+
367
+ search_path = if _schema_name is "public" then _schema_name else "#{_schema_name},public"
368
+
369
+ builder = new actn.Builder(_schema_name, _table_name, search_path, {where: _cond})
370
+
371
+ [sql,params] = builder.build_delete()
372
+
373
+ # plv8.elog(NOTICE,"DELETE",sql,params)
374
+
375
+ rows = plv8.execute(sql,params)
376
+ result = _.pluck(rows,'data')
377
+ result = result[0] if result.length is 1
378
+
379
+ builder = null
380
+
381
+ return JSON.stringify(result)
382
+
383
+ $$ LANGUAGE plcoffee STABLE STRICT;
384
+
385
+
386
+
387
+
388
+
389
+
390
+
391
+ -- ##
392
+ -- # Validate data by json schema
393
+ -- # SELECT validate(model_name, data);
394
+
395
+ CREATE or REPLACE FUNCTION __validate(_name text, _data json) RETURNS json AS $$
396
+
397
+ data = _data
398
+
399
+ # plv8.elog(NOTICE,"__VALIDATE",_name,JSON.stringify(_data))
400
+
401
+ return data unless model = plv8.find_function('__find_model')(_name)
402
+
403
+ model = JSON.parse(model)
404
+
405
+ # plv8.elog(NOTICE,"__VALIDATE MODEL",_name,JSON.stringify(model))
406
+
407
+ if model?.schema?
408
+
409
+ errors = actn.jjv.validate(model.schema,data)
410
+
411
+ plv8.elog(NOTICE,"VALVAL",JSON.stringify(model.schema))
412
+
413
+ if data.uuid? and model.schema.readonly_attributes?
414
+
415
+ data = _.omit(data,model.schema.readonly_attributes)
416
+
417
+ # plv8.elog(NOTICE,"VALIDATE READONLY",JSON.stringify(data),JSON.stringify(model.schema.readonly_attributes))
418
+
419
+
420
+ else if model.schema.unique_attributes?
421
+
422
+ _schema = if _name is "Model" then "core" else "public"
423
+ _table = model.name.tableize()
424
+ __query = plv8.find_function("__query")
425
+
426
+ for uniq_attr in model.schema.unique_attributes or []
427
+ if data[uniq_attr]?
428
+ where = {}
429
+ where[uniq_attr] = data[uniq_attr]
430
+ # plv8.elog(NOTICE,"VALIDATE WHERE",JSON.stringify({where: where}))
431
+ found = JSON.parse(__query(_schema,_table,{where: where}))
432
+ # plv8.elog(NOTICE,"VALIDATE FOUND",JSON.stringify(found))
433
+ unless _.isEmpty(found)
434
+ errors ?= {validation: {}}
435
+ errors['validation'][uniq_attr] ?= {}
436
+ errors['validation'][uniq_attr]["has already been taken"] = true
437
+
438
+ data = {errors: errors} if errors?
439
+
440
+ # plv8.elog(NOTICE,"__VALIDATE DATA",_name,JSON.stringify(data))
441
+
442
+ return data
443
+
444
+ $$ LANGUAGE plcoffee STABLE STRICT;
445
+
446
+
447
+
448
+
449
+
450
+ -- ##
451
+ -- # finds model with given name
452
+ -- # SELECT __find_model(model_name);
453
+
454
+ CREATE or REPLACE FUNCTION __find_model(_name text) RETURNS json AS $$
455
+
456
+ rows = plv8.execute("""SET search_path TO core,public;
457
+ SELECT data FROM core.models
458
+ WHERE __string(data,'name'::text) = $1::text""", [_name])
459
+
460
+ return unless rows?
461
+
462
+ result = _.pluck(rows,'data')[0]
463
+
464
+ return JSON.stringify(result)
465
+
466
+ $$ LANGUAGE plcoffee STABLE STRICT;
467
+
468
+
469
+
470
+
471
+
472
+