Age Owner Branch data TLA Line data Source code
1 : : /*
2 : : * the PLyCursor class
3 : : *
4 : : * src/pl/plpython/plpy_cursorobject.c
5 : : */
6 : :
7 : : #include "postgres.h"
8 : :
9 : : #include <limits.h>
10 : :
11 : : #include "catalog/pg_type.h"
12 : : #include "mb/pg_wchar.h"
13 : : #include "plpy_cursorobject.h"
14 : : #include "plpy_elog.h"
15 : : #include "plpy_main.h"
16 : : #include "plpy_planobject.h"
17 : : #include "plpy_resultobject.h"
18 : : #include "plpy_spi.h"
19 : : #include "plpy_util.h"
20 : : #include "utils/memutils.h"
21 : :
22 : : static PyObject *PLy_cursor_query(const char *query);
23 : : static void PLy_cursor_dealloc(PLyCursorObject *self);
24 : : static PyObject *PLy_cursor_iternext(PyObject *self);
25 : : static PyObject *PLy_cursor_fetch(PyObject *self, PyObject *args);
26 : : static PyObject *PLy_cursor_close(PyObject *self, PyObject *unused);
27 : :
28 : : static const char PLy_cursor_doc[] = "Wrapper around a PostgreSQL cursor";
29 : :
30 : : static PyMethodDef PLy_cursor_methods[] = {
31 : : {"fetch", PLy_cursor_fetch, METH_VARARGS, NULL},
32 : : {"close", PLy_cursor_close, METH_NOARGS, NULL},
33 : : {NULL, NULL, 0, NULL}
34 : : };
35 : :
36 : : static PyType_Slot PLyCursor_slots[] =
37 : : {
38 : : {
39 : : Py_tp_dealloc, PLy_cursor_dealloc
40 : : },
41 : : {
42 : : Py_tp_doc, (char *) PLy_cursor_doc
43 : : },
44 : : {
45 : : Py_tp_iter, PyObject_SelfIter
46 : : },
47 : : {
48 : : Py_tp_iternext, PLy_cursor_iternext
49 : : },
50 : : {
51 : : Py_tp_methods, PLy_cursor_methods
52 : : },
53 : : {
54 : : 0, NULL
55 : : }
56 : : };
57 : :
58 : : static PyType_Spec PLyCursor_spec =
59 : : {
60 : : .name = "PLyCursor",
61 : : .basicsize = sizeof(PLyCursorObject),
62 : : .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
63 : : .slots = PLyCursor_slots,
64 : : };
65 : :
66 : : static PyTypeObject *PLy_CursorType;
67 : :
68 : : void
5308 peter_e@gmx.net 69 :CBC 23 : PLy_cursor_init_type(void)
70 : : {
475 peter@eisentraut.org 71 : 23 : PLy_CursorType = (PyTypeObject *) PyType_FromSpec(&PLyCursor_spec);
72 [ - + ]: 23 : if (!PLy_CursorType)
5308 peter_e@gmx.net 73 [ # # ]:UBC 0 : elog(ERROR, "could not initialize PLy_CursorType");
5308 peter_e@gmx.net 74 :CBC 23 : }
75 : :
76 : : PyObject *
77 : 19 : PLy_cursor(PyObject *self, PyObject *args)
78 : : {
79 : : char *query;
80 : : PyObject *plan;
81 : 19 : PyObject *planargs = NULL;
82 : :
83 [ + + ]: 19 : if (PyArg_ParseTuple(args, "s", &query))
84 : 14 : return PLy_cursor_query(query);
85 : :
86 : 5 : PyErr_Clear();
87 : :
88 [ + - ]: 5 : if (PyArg_ParseTuple(args, "O|O", &plan, &planargs))
89 : 5 : return PLy_cursor_plan(plan, planargs);
90 : :
5308 peter_e@gmx.net 91 :UBC 0 : PLy_exception_set(PLy_exc_error, "plpy.cursor expected a query or a plan");
92 : 0 : return NULL;
93 : : }
94 : :
95 : :
96 : : static PyObject *
5308 peter_e@gmx.net 97 :CBC 14 : PLy_cursor_query(const char *query)
98 : : {
99 : : PLyCursorObject *cursor;
3148 tgl@sss.pgh.pa.us 100 : 14 : PLyExecutionContext *exec_ctx = PLy_current_execution_context();
101 : : volatile MemoryContext oldcontext;
102 : : volatile ResourceOwner oldowner;
103 : :
475 peter@eisentraut.org 104 [ - + ]: 14 : if ((cursor = PyObject_New(PLyCursorObject, PLy_CursorType)) == NULL)
5308 peter_e@gmx.net 105 :UBC 0 : return NULL;
106 : : #if PY_VERSION_HEX < 0x03080000
107 : : /* Workaround for Python issue 35810; no longer necessary in Python 3.8 */
108 : : Py_INCREF(PLy_CursorType);
109 : : #endif
5308 peter_e@gmx.net 110 :CBC 14 : cursor->portalname = NULL;
111 : 14 : cursor->closed = false;
3890 tgl@sss.pgh.pa.us 112 : 14 : cursor->mcxt = AllocSetContextCreate(TopMemoryContext,
113 : : "PL/Python cursor context",
114 : : ALLOCSET_DEFAULT_SIZES);
115 : :
116 : : /* Initialize for converting result tuples to Python */
3148 117 : 14 : PLy_input_setup_func(&cursor->result, cursor->mcxt,
118 : : RECORDOID, -1,
119 : 14 : exec_ctx->curr_proc);
120 : :
5308 peter_e@gmx.net 121 : 14 : oldcontext = CurrentMemoryContext;
122 : 14 : oldowner = CurrentResourceOwner;
123 : :
124 : 14 : PLy_spi_subtransaction_begin(oldcontext, oldowner);
125 : :
126 [ + - ]: 14 : PG_TRY();
127 : : {
128 : : SPIPlanPtr plan;
129 : : Portal portal;
130 : :
131 : 14 : pg_verifymbstr(query, strlen(query), false);
132 : :
133 : 14 : plan = SPI_prepare(query, 0, NULL);
134 [ - + ]: 14 : if (plan == NULL)
5308 peter_e@gmx.net 135 [ # # ]:UBC 0 : elog(ERROR, "SPI_prepare failed: %s",
136 : : SPI_result_code_string(SPI_result));
137 : :
5308 peter_e@gmx.net 138 :CBC 28 : portal = SPI_cursor_open(NULL, plan, NULL, NULL,
5222 tgl@sss.pgh.pa.us 139 : 14 : exec_ctx->curr_proc->fn_readonly);
5308 peter_e@gmx.net 140 : 14 : SPI_freeplan(plan);
141 : :
142 [ - + ]: 14 : if (portal == NULL)
5285 peter_e@gmx.net 143 [ # # ]:UBC 0 : elog(ERROR, "SPI_cursor_open() failed: %s",
144 : : SPI_result_code_string(SPI_result));
145 : :
3890 tgl@sss.pgh.pa.us 146 :CBC 14 : cursor->portalname = MemoryContextStrdup(cursor->mcxt, portal->name);
147 : :
3122 peter_e@gmx.net 148 : 14 : PinPortal(portal);
149 : :
5308 150 : 14 : PLy_spi_subtransaction_commit(oldcontext, oldowner);
151 : : }
5308 peter_e@gmx.net 152 :UBC 0 : PG_CATCH();
153 : : {
154 : 0 : PLy_spi_subtransaction_abort(oldcontext, oldowner);
155 : 0 : return NULL;
156 : : }
5308 peter_e@gmx.net 157 [ - + ]:CBC 14 : PG_END_TRY();
158 : :
159 [ - + ]: 14 : Assert(cursor->portalname != NULL);
160 : 14 : return (PyObject *) cursor;
161 : : }
162 : :
163 : : PyObject *
164 : 6 : PLy_cursor_plan(PyObject *ob, PyObject *args)
165 : : {
166 : : PLyCursorObject *cursor;
167 : : volatile int nargs;
168 : : PLyPlanObject *plan;
3148 tgl@sss.pgh.pa.us 169 : 6 : PLyExecutionContext *exec_ctx = PLy_current_execution_context();
170 : : volatile MemoryContext oldcontext;
171 : : volatile ResourceOwner oldowner;
172 : :
5308 peter_e@gmx.net 173 [ + + ]: 6 : if (args)
174 : : {
1576 andres@anarazel.de 175 [ + - - + ]: 4 : if (!PySequence_Check(args) || PyUnicode_Check(args))
176 : : {
5308 peter_e@gmx.net 177 :UBC 0 : PLy_exception_set(PyExc_TypeError, "plpy.cursor takes a sequence as its second argument");
178 : 0 : return NULL;
179 : : }
5308 peter_e@gmx.net 180 :CBC 4 : nargs = PySequence_Length(args);
181 : : }
182 : : else
183 : 2 : nargs = 0;
184 : :
185 : 6 : plan = (PLyPlanObject *) ob;
186 : :
187 [ + + ]: 6 : if (nargs != plan->nargs)
188 : : {
189 : : char *sv;
190 : 1 : PyObject *so = PyObject_Str(args);
191 : :
192 [ - + ]: 1 : if (!so)
5308 peter_e@gmx.net 193 :UBC 0 : PLy_elog(ERROR, "could not execute plan");
1576 andres@anarazel.de 194 :CBC 1 : sv = PLyUnicode_AsString(so);
5308 peter_e@gmx.net 195 : 1 : PLy_exception_set_plural(PyExc_TypeError,
196 : : "Expected sequence of %d argument, got %d: %s",
197 : : "Expected sequence of %d arguments, got %d: %s",
198 : 1 : plan->nargs,
199 : : plan->nargs, nargs, sv);
200 : : Py_DECREF(so);
201 : :
202 : 1 : return NULL;
203 : : }
204 : :
475 peter@eisentraut.org 205 [ - + ]: 5 : if ((cursor = PyObject_New(PLyCursorObject, PLy_CursorType)) == NULL)
5308 peter_e@gmx.net 206 :UBC 0 : return NULL;
207 : : #if PY_VERSION_HEX < 0x03080000
208 : : /* Workaround for Python issue 35810; no longer necessary in Python 3.8 */
209 : : Py_INCREF(PLy_CursorType);
210 : : #endif
5308 peter_e@gmx.net 211 :CBC 5 : cursor->portalname = NULL;
212 : 5 : cursor->closed = false;
3890 tgl@sss.pgh.pa.us 213 : 5 : cursor->mcxt = AllocSetContextCreate(TopMemoryContext,
214 : : "PL/Python cursor context",
215 : : ALLOCSET_DEFAULT_SIZES);
216 : :
217 : : /* Initialize for converting result tuples to Python */
3148 218 : 5 : PLy_input_setup_func(&cursor->result, cursor->mcxt,
219 : : RECORDOID, -1,
220 : 5 : exec_ctx->curr_proc);
221 : :
5308 peter_e@gmx.net 222 : 5 : oldcontext = CurrentMemoryContext;
223 : 5 : oldowner = CurrentResourceOwner;
224 : :
225 : 5 : PLy_spi_subtransaction_begin(oldcontext, oldowner);
226 : :
227 [ + + ]: 5 : PG_TRY();
228 : : {
229 : : Portal portal;
230 : : MemoryContext tmpcontext;
231 : : Datum *volatile values;
232 : : char *volatile nulls;
233 : : volatile int j;
234 : :
235 : : /*
236 : : * Converted arguments and associated cruft will be in this context,
237 : : * which is local to our subtransaction.
238 : : */
535 tgl@sss.pgh.pa.us 239 : 5 : tmpcontext = AllocSetContextCreate(CurTransactionContext,
240 : : "PL/Python temporary context",
241 : : ALLOCSET_SMALL_SIZES);
242 : 5 : MemoryContextSwitchTo(tmpcontext);
243 : :
5308 peter_e@gmx.net 244 [ + + ]: 5 : if (nargs > 0)
245 : : {
535 tgl@sss.pgh.pa.us 246 : 3 : values = (Datum *) palloc(nargs * sizeof(Datum));
247 : 3 : nulls = (char *) palloc(nargs * sizeof(char));
248 : : }
249 : : else
250 : : {
251 : 2 : values = NULL;
5308 peter_e@gmx.net 252 : 2 : nulls = NULL;
253 : : }
254 : :
255 [ + + ]: 7 : for (j = 0; j < nargs; j++)
256 : : {
3148 tgl@sss.pgh.pa.us 257 : 3 : PLyObToDatum *arg = &plan->args[j];
258 : : PyObject *elem;
259 : :
5308 peter_e@gmx.net 260 : 3 : elem = PySequence_GetItem(args, j);
261 : :
262 : : /* PySequence_GetItem() can return NULL, with an exception set */
1 rguo@postgresql.org 263 [ + + ]: 3 : if (elem == NULL)
264 : 1 : PLy_elog(ERROR, "could not get element %d from sequence", j);
265 : :
1362 drowley@postgresql.o 266 [ + - ]: 2 : PG_TRY(2);
267 : : {
268 : : bool isnull;
269 : :
535 tgl@sss.pgh.pa.us 270 : 2 : values[j] = PLy_output_convert(arg, elem, &isnull);
3148 271 [ - + ]: 2 : nulls[j] = isnull ? 'n' : ' ';
272 : : }
1362 drowley@postgresql.o 273 : 2 : PG_FINALLY(2);
274 : : {
275 : : Py_DECREF(elem);
276 : : }
277 [ - + ]: 2 : PG_END_TRY(2);
278 : : }
279 : :
535 tgl@sss.pgh.pa.us 280 : 4 : MemoryContextSwitchTo(oldcontext);
281 : :
282 : 8 : portal = SPI_cursor_open(NULL, plan->plan, values, nulls,
5222 283 : 4 : exec_ctx->curr_proc->fn_readonly);
5308 peter_e@gmx.net 284 [ - + ]: 4 : if (portal == NULL)
5285 peter_e@gmx.net 285 [ # # ]:UBC 0 : elog(ERROR, "SPI_cursor_open() failed: %s",
286 : : SPI_result_code_string(SPI_result));
287 : :
3890 tgl@sss.pgh.pa.us 288 :CBC 4 : cursor->portalname = MemoryContextStrdup(cursor->mcxt, portal->name);
289 : :
3122 peter_e@gmx.net 290 : 4 : PinPortal(portal);
291 : :
535 tgl@sss.pgh.pa.us 292 : 4 : MemoryContextDelete(tmpcontext);
5308 peter_e@gmx.net 293 : 4 : PLy_spi_subtransaction_commit(oldcontext, oldowner);
294 : : }
295 : 1 : PG_CATCH();
296 : : {
297 : : Py_DECREF(cursor);
298 : : /* Subtransaction abort will remove the tmpcontext */
299 : 1 : PLy_spi_subtransaction_abort(oldcontext, oldowner);
300 : 1 : return NULL;
301 : : }
302 [ - + ]: 4 : PG_END_TRY();
303 : :
304 [ - + ]: 4 : Assert(cursor->portalname != NULL);
305 : 4 : return (PyObject *) cursor;
306 : : }
307 : :
308 : : static void
475 peter@eisentraut.org 309 : 19 : PLy_cursor_dealloc(PLyCursorObject *self)
310 : : {
311 : : #if PY_VERSION_HEX >= 0x03080000
312 : 19 : PyTypeObject *tp = Py_TYPE(self);
313 : : #endif
314 : : Portal portal;
315 : :
316 [ + + ]: 19 : if (!self->closed)
317 : : {
318 : 16 : portal = GetPortalByName(self->portalname);
319 : :
5308 peter_e@gmx.net 320 [ + + ]: 16 : if (PortalIsValid(portal))
321 : : {
3122 322 : 12 : UnpinPortal(portal);
5308 323 : 12 : SPI_cursor_close(portal);
324 : : }
475 peter@eisentraut.org 325 : 16 : self->closed = true;
326 : : }
327 [ + - ]: 19 : if (self->mcxt)
328 : : {
329 : 19 : MemoryContextDelete(self->mcxt);
330 : 19 : self->mcxt = NULL;
331 : : }
332 : :
333 : 19 : PyObject_Free(self);
334 : : #if PY_VERSION_HEX >= 0x03080000
335 : : /* This was not needed before Python 3.8 (Python issue 35810) */
336 : : Py_DECREF(tp);
337 : : #endif
5308 peter_e@gmx.net 338 : 19 : }
339 : :
340 : : static PyObject *
341 : 36 : PLy_cursor_iternext(PyObject *self)
342 : : {
343 : : PLyCursorObject *cursor;
344 : : PyObject *ret;
3148 tgl@sss.pgh.pa.us 345 : 36 : PLyExecutionContext *exec_ctx = PLy_current_execution_context();
346 : : volatile MemoryContext oldcontext;
347 : : volatile ResourceOwner oldowner;
348 : : Portal portal;
349 : :
5308 peter_e@gmx.net 350 : 36 : cursor = (PLyCursorObject *) self;
351 : :
352 [ + + ]: 36 : if (cursor->closed)
353 : : {
354 : 1 : PLy_exception_set(PyExc_ValueError, "iterating a closed cursor");
355 : 1 : return NULL;
356 : : }
357 : :
358 : 35 : portal = GetPortalByName(cursor->portalname);
359 [ - + ]: 35 : if (!PortalIsValid(portal))
360 : : {
5308 peter_e@gmx.net 361 :UBC 0 : PLy_exception_set(PyExc_ValueError,
362 : : "iterating a cursor in an aborted subtransaction");
363 : 0 : return NULL;
364 : : }
365 : :
5308 peter_e@gmx.net 366 :CBC 35 : oldcontext = CurrentMemoryContext;
367 : 35 : oldowner = CurrentResourceOwner;
368 : :
369 : 35 : PLy_spi_subtransaction_begin(oldcontext, oldowner);
370 : :
371 [ + + ]: 35 : PG_TRY();
372 : : {
373 : 35 : SPI_cursor_fetch(portal, true, 1);
374 [ + + ]: 34 : if (SPI_processed == 0)
375 : : {
376 : 8 : PyErr_SetNone(PyExc_StopIteration);
377 : 8 : ret = NULL;
378 : : }
379 : : else
380 : : {
3148 tgl@sss.pgh.pa.us 381 : 26 : PLy_input_setup_tuple(&cursor->result, SPI_tuptable->tupdesc,
382 : 26 : exec_ctx->curr_proc);
383 : :
384 : 26 : ret = PLy_input_from_tuple(&cursor->result, SPI_tuptable->vals[0],
2649 peter@eisentraut.org 385 : 26 : SPI_tuptable->tupdesc, true);
386 : : }
387 : :
5308 peter_e@gmx.net 388 : 34 : SPI_freetuptable(SPI_tuptable);
389 : :
390 : 34 : PLy_spi_subtransaction_commit(oldcontext, oldowner);
391 : : }
392 : 1 : PG_CATCH();
393 : : {
394 : 1 : PLy_spi_subtransaction_abort(oldcontext, oldowner);
395 : 1 : return NULL;
396 : : }
397 [ - + ]: 34 : PG_END_TRY();
398 : :
399 : 34 : return ret;
400 : : }
401 : :
402 : : static PyObject *
403 : 13 : PLy_cursor_fetch(PyObject *self, PyObject *args)
404 : : {
405 : : PLyCursorObject *cursor;
406 : : int count;
407 : : PLyResultObject *ret;
3148 tgl@sss.pgh.pa.us 408 : 13 : PLyExecutionContext *exec_ctx = PLy_current_execution_context();
409 : : volatile MemoryContext oldcontext;
410 : : volatile ResourceOwner oldowner;
411 : : Portal portal;
412 : :
3533 peter_e@gmx.net 413 [ - + ]: 13 : if (!PyArg_ParseTuple(args, "i:fetch", &count))
5308 peter_e@gmx.net 414 :UBC 0 : return NULL;
415 : :
5308 peter_e@gmx.net 416 :CBC 13 : cursor = (PLyCursorObject *) self;
417 : :
418 [ + + ]: 13 : if (cursor->closed)
419 : : {
420 : 1 : PLy_exception_set(PyExc_ValueError, "fetch from a closed cursor");
421 : 1 : return NULL;
422 : : }
423 : :
424 : 12 : portal = GetPortalByName(cursor->portalname);
425 [ + + ]: 12 : if (!PortalIsValid(portal))
426 : : {
427 : 2 : PLy_exception_set(PyExc_ValueError,
428 : : "iterating a cursor in an aborted subtransaction");
429 : 2 : return NULL;
430 : : }
431 : :
432 : 10 : ret = (PLyResultObject *) PLy_result_new();
433 [ - + ]: 10 : if (ret == NULL)
5308 peter_e@gmx.net 434 :UBC 0 : return NULL;
435 : :
5308 peter_e@gmx.net 436 :CBC 10 : oldcontext = CurrentMemoryContext;
437 : 10 : oldowner = CurrentResourceOwner;
438 : :
439 : 10 : PLy_spi_subtransaction_begin(oldcontext, oldowner);
440 : :
441 [ + - ]: 10 : PG_TRY();
442 : : {
443 : 10 : SPI_cursor_fetch(portal, true, count);
444 : :
445 : 10 : Py_DECREF(ret->status);
1576 andres@anarazel.de 446 : 10 : ret->status = PyLong_FromLong(SPI_OK_FETCH);
447 : :
5308 peter_e@gmx.net 448 : 10 : Py_DECREF(ret->nrows);
3083 449 : 10 : ret->nrows = PyLong_FromUnsignedLongLong(SPI_processed);
450 : :
5308 451 [ + + ]: 10 : if (SPI_processed != 0)
452 : : {
453 : : uint64 i;
454 : :
455 : : /*
456 : : * PyList_New() and PyList_SetItem() use Py_ssize_t for list size
457 : : * and list indices; so we cannot support a result larger than
458 : : * PY_SSIZE_T_MAX.
459 : : */
3762 tgl@sss.pgh.pa.us 460 [ - + ]: 7 : if (SPI_processed > (uint64) PY_SSIZE_T_MAX)
3762 tgl@sss.pgh.pa.us 461 [ # # ]:UBC 0 : ereport(ERROR,
462 : : (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
463 : : errmsg("query result has too many rows to fit in a Python list")));
464 : :
5308 peter_e@gmx.net 465 :CBC 7 : Py_DECREF(ret->rows);
466 : 7 : ret->rows = PyList_New(SPI_processed);
3164 467 [ - + ]: 7 : if (!ret->rows)
468 : : {
469 : : Py_DECREF(ret);
3164 peter_e@gmx.net 470 :UBC 0 : ret = NULL;
471 : : }
472 : : else
473 : : {
3164 peter_e@gmx.net 474 :CBC 7 : PLy_input_setup_tuple(&cursor->result, SPI_tuptable->tupdesc,
475 : 7 : exec_ctx->curr_proc);
476 : :
477 [ + + ]: 44 : for (i = 0; i < SPI_processed; i++)
478 : : {
479 : 74 : PyObject *row = PLy_input_from_tuple(&cursor->result,
480 : 37 : SPI_tuptable->vals[i],
2649 peter@eisentraut.org 481 : 37 : SPI_tuptable->tupdesc,
482 : : true);
483 : :
3164 peter_e@gmx.net 484 : 37 : PyList_SetItem(ret->rows, i, row);
485 : : }
486 : : }
487 : : }
488 : :
5308 489 : 10 : SPI_freetuptable(SPI_tuptable);
490 : :
491 : 10 : PLy_spi_subtransaction_commit(oldcontext, oldowner);
492 : : }
5308 peter_e@gmx.net 493 :UBC 0 : PG_CATCH();
494 : : {
495 : 0 : PLy_spi_subtransaction_abort(oldcontext, oldowner);
496 : 0 : return NULL;
497 : : }
5308 peter_e@gmx.net 498 [ - + ]:CBC 10 : PG_END_TRY();
499 : :
500 : 10 : return (PyObject *) ret;
501 : : }
502 : :
503 : : static PyObject *
504 : 5 : PLy_cursor_close(PyObject *self, PyObject *unused)
505 : : {
506 : 5 : PLyCursorObject *cursor = (PLyCursorObject *) self;
507 : :
508 [ + + ]: 5 : if (!cursor->closed)
509 : : {
5133 bruce@momjian.us 510 : 4 : Portal portal = GetPortalByName(cursor->portalname);
511 : :
5308 peter_e@gmx.net 512 [ + + ]: 4 : if (!PortalIsValid(portal))
513 : : {
514 : 1 : PLy_exception_set(PyExc_ValueError,
515 : : "closing a cursor in an aborted subtransaction");
516 : 1 : return NULL;
517 : : }
518 : :
3122 519 : 3 : UnpinPortal(portal);
5308 520 : 3 : SPI_cursor_close(portal);
521 : 3 : cursor->closed = true;
522 : : }
523 : :
3196 524 : 4 : Py_RETURN_NONE;
525 : : }
|