actn-db 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+