globegit-postgresql-plruby 0.5.4

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.
Files changed (122) hide show
  1. data/Changes +121 -0
  2. data/README.markdown +155 -0
  3. data/Rakefile +48 -0
  4. data/docs/plruby.rb +1931 -0
  5. data/ex_trans.sql +33 -0
  6. data/extconf.rb +267 -0
  7. data/plruby.html +1454 -0
  8. data/plruby.rd +1571 -0
  9. data/postgresql-plruby.gemspec +56 -0
  10. data/src/conversions.h +5 -0
  11. data/src/conversions/basic/conversions.h +25 -0
  12. data/src/conversions/basic/extconf.rb +8 -0
  13. data/src/conversions/basic/plruby_basic.c +357 -0
  14. data/src/conversions/bitstring/bitstring.sql +75 -0
  15. data/src/conversions/bitstring/conversions.h +15 -0
  16. data/src/conversions/bitstring/extconf.rb +8 -0
  17. data/src/conversions/bitstring/plruby_bitstring.c +579 -0
  18. data/src/conversions/convcommon.h +129 -0
  19. data/src/conversions/datetime/conversions.h +13 -0
  20. data/src/conversions/datetime/extconf.rb +8 -0
  21. data/src/conversions/datetime/plruby_datetime.c +269 -0
  22. data/src/conversions/geometry/conversions.h +37 -0
  23. data/src/conversions/geometry/extconf.rb +8 -0
  24. data/src/conversions/geometry/geometry.sql +196 -0
  25. data/src/conversions/geometry/plruby_geometry.c +2494 -0
  26. data/src/conversions/network/conversions.h +21 -0
  27. data/src/conversions/network/extconf.rb +8 -0
  28. data/src/conversions/network/network.sql +63 -0
  29. data/src/conversions/network/plruby_network.c +537 -0
  30. data/src/package.h +20 -0
  31. data/src/plpl.c +1708 -0
  32. data/src/plplan.c +893 -0
  33. data/src/plruby.c +1676 -0
  34. data/src/plruby.h +324 -0
  35. data/src/pltrans.c +388 -0
  36. data/test/conv_bitstring/b.rb +45 -0
  37. data/test/conv_bitstring/runtest +26 -0
  38. data/test/conv_bitstring/test.expected.73 +148 -0
  39. data/test/conv_bitstring/test.expected.74 +148 -0
  40. data/test/conv_bitstring/test.expected.80 +148 -0
  41. data/test/conv_bitstring/test.expected.81 +148 -0
  42. data/test/conv_bitstring/test.expected.82 +148 -0
  43. data/test/conv_bitstring/test.expected.83 +148 -0
  44. data/test/conv_bitstring/test.expected.84 +148 -0
  45. data/test/conv_bitstring/test.out +148 -0
  46. data/test/conv_bitstring/test_mklang.sql +8 -0
  47. data/test/conv_bitstring/test_queries.sql +63 -0
  48. data/test/conv_bitstring/test_queries.sql.in +63 -0
  49. data/test/conv_geometry/b.rb +45 -0
  50. data/test/conv_geometry/runtest +26 -0
  51. data/test/conv_geometry/test.expected.73 +265 -0
  52. data/test/conv_geometry/test.expected.74 +265 -0
  53. data/test/conv_geometry/test.expected.80 +265 -0
  54. data/test/conv_geometry/test.expected.81 +265 -0
  55. data/test/conv_geometry/test.expected.82 +265 -0
  56. data/test/conv_geometry/test.expected.83 +265 -0
  57. data/test/conv_geometry/test.expected.84 +265 -0
  58. data/test/conv_geometry/test.out +265 -0
  59. data/test/conv_geometry/test_mklang.sql +8 -0
  60. data/test/conv_geometry/test_queries.sql +194 -0
  61. data/test/conv_geometry/test_queries.sql.in +194 -0
  62. data/test/conv_network/b.rb +45 -0
  63. data/test/conv_network/runtest +26 -0
  64. data/test/conv_network/test.expected.73 +213 -0
  65. data/test/conv_network/test.expected.74 +237 -0
  66. data/test/conv_network/test.expected.80 +237 -0
  67. data/test/conv_network/test.expected.81 +237 -0
  68. data/test/conv_network/test.expected.82 +237 -0
  69. data/test/conv_network/test.expected.83 +237 -0
  70. data/test/conv_network/test.expected.84 +237 -0
  71. data/test/conv_network/test.out +237 -0
  72. data/test/conv_network/test_mklang.sql +8 -0
  73. data/test/conv_network/test_queries.sql +60 -0
  74. data/test/conv_network/test_queries.sql.in +60 -0
  75. data/test/plp/b.rb +34 -0
  76. data/test/plp/runtest +29 -0
  77. data/test/plp/test.expected.73 +472 -0
  78. data/test/plp/test.expected.74 +472 -0
  79. data/test/plp/test.expected.75 +472 -0
  80. data/test/plp/test.expected.80 +472 -0
  81. data/test/plp/test.expected.81 +472 -0
  82. data/test/plp/test.expected.82 +472 -0
  83. data/test/plp/test.expected.83 +472 -0
  84. data/test/plp/test.expected.84 +472 -0
  85. data/test/plp/test.out +472 -0
  86. data/test/plp/test_mklang.sql +8 -0
  87. data/test/plp/test_queries.sql +273 -0
  88. data/test/plp/test_setup.sql +931 -0
  89. data/test/plp/test_setup.sql.in +931 -0
  90. data/test/plt/b.rb +34 -0
  91. data/test/plt/runtest +29 -0
  92. data/test/plt/test.expected.73 +178 -0
  93. data/test/plt/test.expected.74 +178 -0
  94. data/test/plt/test.expected.75 +178 -0
  95. data/test/plt/test.expected.80 +178 -0
  96. data/test/plt/test.expected.81 +178 -0
  97. data/test/plt/test.expected.82 +178 -0
  98. data/test/plt/test.expected.83 +164 -0
  99. data/test/plt/test.expected.84 +168 -0
  100. data/test/plt/test.out +168 -0
  101. data/test/plt/test_mklang.sql +8 -0
  102. data/test/plt/test_queries.sql +72 -0
  103. data/test/plt/test_setup.sql +252 -0
  104. data/test/plt/test_setup.sql.in +252 -0
  105. data/test/range/b.rb +45 -0
  106. data/test/range/runtest +26 -0
  107. data/test/range/test.expected.73 +396 -0
  108. data/test/range/test.expected.73.in +396 -0
  109. data/test/range/test.expected.74 +396 -0
  110. data/test/range/test.expected.74.in +396 -0
  111. data/test/range/test.expected.75 +396 -0
  112. data/test/range/test.expected.75.in +396 -0
  113. data/test/range/test.expected.80 +396 -0
  114. data/test/range/test.expected.81 +397 -0
  115. data/test/range/test.expected.82 +397 -0
  116. data/test/range/test.expected.83 +397 -0
  117. data/test/range/test.expected.84 +399 -0
  118. data/test/range/test.out +399 -0
  119. data/test/range/test_mklang.sql +8 -0
  120. data/test/range/test_queries.sql +249 -0
  121. data/test/range/test_queries.sql.in +249 -0
  122. metadata +207 -0
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'rbconfig'
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = 'globegit-postgresql-plruby'
6
+ spec.version = '0.5.4'
7
+ spec.authors = ['Akinori MUSHA', 'Guy Decoux']
8
+ spec.license = 'Ruby'
9
+ spec.email = 'akinori@musha.org'
10
+ spec.homepage = 'https://github.com/knu/postgresql-plruby'
11
+ spec.summary = 'Enable Ruby for use as a procedural language within PostgreSQL'
12
+ spec.test_files = Dir['test/test*']
13
+ spec.extensions = ['extconf.rb']
14
+ spec.files = Dir['**/*'].reject{ |f| f.include?('git') || f.include?('tmp') }
15
+
16
+ spec.rubyforge_project = 'plruby'
17
+
18
+ spec.extra_rdoc_files = [
19
+ 'README.markdown',
20
+ 'Changes'
21
+ ] + Dir['ext/*.c']
22
+
23
+ spec.description = <<-EOF
24
+ PL/Ruby is a loadable procedural language for the PostgreSQL database
25
+ system that enables the Ruby language to create functions and trigger
26
+ procedures.
27
+ EOF
28
+
29
+ plruby_bin = 'plruby.' + Config::CONFIG['DLEXT']
30
+ plruby_dir = File.join('postgresql-plruby-' + spec.version.to_s, 'src')
31
+ path_to_binary = File.join(Gem.dir, 'gems', plruby_dir, plruby_bin)
32
+
33
+ possible_paths = Gem.path.map{ |path|
34
+ File.join(path, 'gems', plruby_dir, plruby_bin)
35
+ }
36
+
37
+ spec.post_install_message = <<-EOF
38
+
39
+ Now run the following commands from within a postgresql shell in order
40
+ to create the plruby language on in database server:
41
+
42
+ create function plruby_call_handler() returns language_handler
43
+ as '#{path_to_binary}'
44
+ language 'C';
45
+
46
+ create trusted language 'plruby'
47
+ handler plruby_call_handler
48
+ lancompiler 'PL/Ruby';
49
+
50
+ NOTE: Your actual path to #{plruby_bin} may be different. Possible
51
+ paths to the plruby binary are:
52
+
53
+ #{possible_paths.join("\n ")}
54
+
55
+ EOF
56
+ end
@@ -0,0 +1,5 @@
1
+ #include "conversions/basic/conversions.h"
2
+ #include "conversions/bitstring/conversions.h"
3
+ #include "conversions/datetime/conversions.h"
4
+ #include "conversions/geometry/conversions.h"
5
+ #include "conversions/network/conversions.h"
@@ -0,0 +1,25 @@
1
+ {
2
+ extern void plruby_require(char *);
3
+
4
+ plruby_require("plruby/plruby_basic");
5
+ rb_hash_aset(plruby_classes, INT2NUM(OIDOID), rb_cFixnum);
6
+ rb_hash_aset(plruby_classes, INT2NUM(INT2OID), rb_cFixnum);
7
+ rb_hash_aset(plruby_classes, INT2NUM(INT4OID), rb_cFixnum);
8
+ rb_hash_aset(plruby_classes, INT2NUM(INT8OID), rb_cFixnum);
9
+
10
+ rb_hash_aset(plruby_classes, INT2NUM(FLOAT4OID), rb_cFloat);
11
+ rb_hash_aset(plruby_classes, INT2NUM(FLOAT8OID), rb_cFloat);
12
+ rb_hash_aset(plruby_classes, INT2NUM(CASHOID), rb_cFloat);
13
+ rb_hash_aset(plruby_classes, INT2NUM(NUMERICOID), rb_cFloat);
14
+
15
+ rb_hash_aset(plruby_classes, INT2NUM(TIMESTAMPOID), rb_cTime);
16
+ rb_hash_aset(plruby_classes, INT2NUM(TIMESTAMPTZOID), rb_cTime);
17
+ rb_hash_aset(plruby_classes, INT2NUM(ABSTIMEOID), rb_cTime);
18
+ rb_hash_aset(plruby_classes, INT2NUM(DATEOID), rb_cTime);
19
+ rb_hash_aset(plruby_classes, INT2NUM(RELTIMEOID), rb_cTime);
20
+ rb_hash_aset(plruby_classes, INT2NUM(INTERVALOID), rb_cTime);
21
+ rb_hash_aset(plruby_classes, INT2NUM(TIMETZOID), rb_cTime);
22
+ rb_hash_aset(plruby_classes, INT2NUM(TIMEOID), rb_cTime);
23
+
24
+ rb_hash_aset(plruby_classes, ULONG2NUM(BYTEAOID), rb_cString);
25
+ }
@@ -0,0 +1,8 @@
1
+ require 'mkmf'
2
+
3
+ if CONFIG["LIBRUBYARG"] == "$(LIBRUBYARG_SHARED)" &&
4
+ !enable_config("plruby-shared")
5
+ $LIBRUBYARG = ""
6
+ end
7
+ have_library('ruby18', 'ruby_init')
8
+ create_makefile('plruby/plruby_basic')
@@ -0,0 +1,357 @@
1
+ #include "convcommon.h"
2
+
3
+ #include <utils/cash.h>
4
+ #include <utils/date.h>
5
+ #include <utils/nabstime.h>
6
+ #include <utils/pg_locale.h>
7
+ #include <utils/timestamp.h>
8
+ #include <math.h>
9
+
10
+ static double cash_divisor;
11
+ static Timestamp epoch;
12
+ static ID id_at, id_to_f, id_to_i, id_usec;
13
+
14
+ static VALUE
15
+ pl_fixnum_s_datum(VALUE obj, VALUE a)
16
+ {
17
+ Oid typoid;
18
+ Datum value;
19
+
20
+ value = plruby_datum_get(a, &typoid);
21
+ switch (typoid) {
22
+ case OIDOID:
23
+ return UINT2NUM(DatumGetObjectId(value));
24
+
25
+ case INT2OID:
26
+ return INT2NUM(DatumGetInt16(value));
27
+
28
+ case INT4OID:
29
+ return INT2NUM(DatumGetInt32(value));
30
+
31
+ case INT8OID:
32
+ return LL2NUM(DatumGetInt64(value));
33
+
34
+ default:
35
+ rb_raise(rb_eArgError, "unknown OID type %d", typoid);
36
+ }
37
+ }
38
+
39
+ static VALUE
40
+ pl_fixnum_to_datum(VALUE obj, VALUE a)
41
+ {
42
+ Datum d;
43
+
44
+ switch (plruby_datum_oid(a, NULL)) {
45
+ case OIDOID:
46
+ d = ObjectIdGetDatum(NUM2UINT(obj));
47
+ break;
48
+
49
+ case INT2OID:
50
+ d = Int16GetDatum(NUM2INT(obj));
51
+ break;
52
+
53
+ case INT4OID:
54
+ d = Int32GetDatum(NUM2INT(obj));
55
+ break;
56
+
57
+ case INT8OID:
58
+ d = Int64GetDatum(NUM2LL(obj));
59
+ break;
60
+
61
+ default:
62
+ return Qnil;
63
+ }
64
+ return plruby_datum_set(a, d);
65
+ }
66
+
67
+ static VALUE
68
+ pl_float_s_datum(VALUE obj, VALUE a)
69
+ {
70
+ Oid typoid;
71
+ Datum value;
72
+ double result;
73
+
74
+ value = plruby_datum_get(a, &typoid);
75
+ switch (typoid) {
76
+ case FLOAT4OID:
77
+ result = DatumGetFloat4(value);
78
+ break;
79
+
80
+ case FLOAT8OID:
81
+ result = DatumGetFloat8(value);
82
+ break;
83
+
84
+ case CASHOID:
85
+ result = (double) *(Cash *) DatumGetPointer(value) / cash_divisor;
86
+ break;
87
+
88
+ case NUMERICOID:
89
+ result = DatumGetFloat8(plruby_dfc1(numeric_float8, value));
90
+ break;
91
+
92
+ default:
93
+ rb_raise(rb_eArgError, "unknown OID type %d", typoid);
94
+ }
95
+ return rb_float_new(result);
96
+ }
97
+
98
+ extern double round();
99
+
100
+ static VALUE
101
+ pl_float_to_datum(VALUE obj, VALUE a)
102
+ {
103
+ double value;
104
+ Datum d;
105
+
106
+ value = RFLOAT_VALUE(obj);
107
+ switch (plruby_datum_oid(a, NULL)) {
108
+ case FLOAT4OID:
109
+ d = Float4GetDatum((float4)value);
110
+ break;
111
+
112
+ case FLOAT8OID:
113
+ d = Float8GetDatum((float8)value);
114
+ break;
115
+
116
+ case CASHOID:
117
+ {
118
+ Cash *cash = (Cash *) palloc(sizeof(Cash));
119
+ *cash = (Cash) round(value * cash_divisor);
120
+ d = PointerGetDatum(cash);
121
+ break;
122
+ }
123
+
124
+ case NUMERICOID:
125
+ d = plruby_dfc1(float8_numeric, Float8GetDatum((float8)value));
126
+ break;
127
+
128
+ default:
129
+ return Qnil;
130
+ }
131
+ return plruby_datum_set(a, d);
132
+ }
133
+
134
+ static VALUE
135
+ pl_str_s_datum(VALUE klass, VALUE a)
136
+ {
137
+ bytea *data;
138
+ Oid typoid;
139
+ Datum value;
140
+
141
+ value = plruby_datum_get(a, &typoid);
142
+ if (typoid != BYTEAOID) {
143
+ return Qnil;
144
+ }
145
+ data = DatumGetByteaP(value);
146
+ return rb_str_new(VARDATA(data), VARSIZE(data) - VARHDRSZ);
147
+ }
148
+
149
+ static VALUE
150
+ pl_str_to_datum(VALUE obj, VALUE a)
151
+ {
152
+ bytea *data;
153
+ size_t len;
154
+
155
+ /* Converts BYTEA only. */
156
+ if (plruby_datum_oid(a, NULL) != BYTEAOID)
157
+ return Qnil;
158
+
159
+ len = RSTRING_LEN(obj);
160
+ data = palloc(VARHDRSZ + len);
161
+ memcpy(VARDATA(data), RSTRING_PTR(obj), len);
162
+ #ifdef SET_VARSIZE
163
+ SET_VARSIZE(data, VARHDRSZ + len);
164
+ #else
165
+ VARATT_SIZEP(data) = VARHDRSZ + len;
166
+ #endif
167
+ return plruby_datum_set(a, PointerGetDatum(data));
168
+ }
169
+
170
+ static VALUE
171
+ pl_time_s_datum(VALUE klass, VALUE a)
172
+ {
173
+ Timestamp ts;
174
+ Oid typoid;
175
+ Datum value;
176
+
177
+ /*
178
+ * INTERVAL and RELTIME are converted to Float (number of seconds).
179
+ * For INTERVALs containing nonzero month/year component, duration of one
180
+ * month is assumed to be 30*24*60*60 seconds. A special type has to be
181
+ * created for this, because of the months/year components and also because
182
+ * long or short enough numbers do not convert back right (exponential
183
+ * notation of INTERVALs is not accepted by Postgres).
184
+ *
185
+ * TIMESTAMP, TIMESTAMP WITH TIME ZONE, ABSTIME, DATE are converted to klass
186
+ * (Time), naturally.
187
+ *
188
+ * TIME and TIME WITH TIME ZONE are also converted to klass (Time), as in
189
+ * the (totally broken anyway) 0.4.3 implementation. The result is that
190
+ * specific time since Unix epoch. That makes little sense (the reverse
191
+ * conversion of the result breaks anyway), but some at least. A special
192
+ * type has to be created for this.
193
+ */
194
+ value = plruby_datum_get(a, &typoid);
195
+ switch (typoid) {
196
+ /* Time interval types. */
197
+
198
+ case RELTIMEOID:
199
+ value = plruby_dfc1(reltime_interval, value);
200
+ /* ... */
201
+ case INTERVALOID:
202
+ {
203
+ Interval *iv = DatumGetIntervalP(value);
204
+
205
+ return rb_float_new((double) iv->month * 30*24*60*60 +
206
+ iv->time
207
+ #ifdef HAVE_INT64_TIMESTAMP
208
+ / 1E6
209
+ #endif
210
+ );
211
+ }
212
+
213
+ /*
214
+ * Time of day types.
215
+ *
216
+ * No separate conversion code is written, abusing the coincidence of C
217
+ * types used for TimeADT and Timestamp (int64 or double, depending on
218
+ * HAVE_INT64_TIMESTAMP). The proper implementation would use a special
219
+ * type anyway, see above.
220
+ */
221
+
222
+ case TIMETZOID:
223
+ {
224
+ TimeTzADT *timetz = DatumGetTimeTzADTP(value);
225
+
226
+ /* Shift according to the timezone. */
227
+ ts = timetz->time + (Timestamp) timetz->zone
228
+ #ifdef HAVE_INT64_TIMESTAMP
229
+ * 1000000
230
+ #endif
231
+ ;
232
+ }
233
+ goto convert;
234
+
235
+ case TIMEOID:
236
+ ts = (Timestamp) DatumGetTimeADT(value);
237
+ goto convert;
238
+
239
+ /* The rest of types end up as a Timestamp in `value'. */
240
+
241
+ case ABSTIMEOID:
242
+ value = plruby_dfc1(abstime_timestamptz, value);
243
+ break;
244
+
245
+ case DATEOID:
246
+ value = plruby_dfc1(date_timestamptz, value);
247
+ break;
248
+
249
+ case TIMESTAMPOID:
250
+ case TIMESTAMPTZOID:
251
+ break;
252
+
253
+ default:
254
+ rb_raise(rb_eTypeError, "%s: incompatible type OID %u",
255
+ rb_class2name(klass), typoid);
256
+ }
257
+
258
+ ts = DatumGetTimestamp(value) - epoch;
259
+
260
+ convert:
261
+ return rb_funcall(klass, id_at,
262
+ #ifndef HAVE_INT64_TIMESTAMP
263
+ 1, rb_float_new(ts)
264
+ #else
265
+ 2, LONG2NUM(ts / 1000000), ULONG2NUM(ts % 1000000)
266
+ #endif
267
+ );
268
+ }
269
+
270
+ static VALUE
271
+ pl_time_to_datum(VALUE obj, VALUE a)
272
+ {
273
+ PGFunction conv;
274
+ Datum d;
275
+ int typoid;
276
+
277
+ typoid = plruby_datum_oid(a, NULL);
278
+ switch (typoid) {
279
+ case ABSTIMEOID:
280
+ case DATEOID:
281
+ case TIMEOID:
282
+ case TIMESTAMPOID:
283
+ case TIMESTAMPTZOID:
284
+ case TIMETZOID:
285
+ break;
286
+
287
+ default:
288
+ return Qnil;
289
+ }
290
+
291
+ /* Convert Time to TimestampTz first. */
292
+ #ifndef HAVE_INT64_TIMESTAMP
293
+ d = TimestampTzGetDatum(epoch + NUM2DBL(rb_funcall(obj, id_to_f, 0)));
294
+ #else
295
+ d = TimestampTzGetDatum(epoch + (TimestampTz)
296
+ NUM2LONG(rb_funcall(obj, id_to_i, 0)) * 1000000 +
297
+ NUM2ULONG(rb_funcall(obj, id_usec, 0)));
298
+ #endif
299
+
300
+ conv = NULL;
301
+ switch (typoid) {
302
+ case ABSTIMEOID:
303
+ conv = timestamptz_abstime;
304
+ break;
305
+
306
+ case DATEOID:
307
+ conv = timestamptz_date;
308
+ break;
309
+
310
+ case TIMEOID:
311
+ conv = timestamptz_time;
312
+ break;
313
+
314
+ case TIMESTAMPOID:
315
+ conv = timestamptz_timestamp;
316
+ break;
317
+
318
+ case TIMESTAMPTZOID:
319
+ break;
320
+
321
+ case TIMETZOID:
322
+ conv = timestamptz_timetz;
323
+ break;
324
+ }
325
+
326
+ if (conv == NULL) {
327
+ return Qnil;
328
+ }
329
+ d = plruby_dfc1(conv, d);
330
+ return plruby_datum_set(a, d);
331
+ }
332
+
333
+ void Init_plruby_basic()
334
+ {
335
+ int fpoint;
336
+ struct lconv *lconvert = PGLC_localeconv();
337
+
338
+ fpoint = lconvert->frac_digits;
339
+ if (fpoint < 0 || fpoint > 10) {
340
+ fpoint = 2;
341
+ }
342
+ cash_divisor = pow(10.0, fpoint);
343
+ epoch = SetEpochTimestamp();
344
+ id_at = rb_intern("at");
345
+ id_to_f = rb_intern("to_f");
346
+ id_to_i = rb_intern("to_i");
347
+ id_usec = rb_intern("usec");
348
+
349
+ rb_define_singleton_method(rb_cFixnum, "from_datum", pl_fixnum_s_datum, 1);
350
+ rb_define_method(rb_cFixnum, "to_datum", pl_fixnum_to_datum, 1);
351
+ rb_define_singleton_method(rb_cFloat, "from_datum", pl_float_s_datum, 1);
352
+ rb_define_method(rb_cFloat, "to_datum", pl_float_to_datum, 1);
353
+ rb_define_singleton_method(rb_cString, "from_datum", pl_str_s_datum, 1);
354
+ rb_define_method(rb_cString, "to_datum", pl_str_to_datum, 1);
355
+ rb_define_singleton_method(rb_cTime, "from_datum", pl_time_s_datum, 1);
356
+ rb_define_method(rb_cTime, "to_datum", pl_time_to_datum, 1);
357
+ }