proposal - function string_to_table
Hi
I propose new function string_to_table. This function is significantly
faster (and simpler) variant of regexp_split_to_array function. There was
same process years ago when we implemented string_agg as faster variant of
array_to_string(array_agg()). string_to_table is faster variant (and little
bit more intuitive alternative of unnest(string_to_array()).
string_to_table is about 15% faster than unnest(string_to_array()) and
about 40% faster than regexp_split_to_array.
Initial patch is attached
Notes, comments?
Regards
Pavel
Attachments:
string_to_table.patchtext/x-patch; charset=US-ASCII; name=string_to_table.patchDownload
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 7a270eb0ab..4046646267 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -14160,6 +14160,9 @@ SELECT NULLIF(value, '(none)') ...
<indexterm>
<primary>string_to_array</primary>
</indexterm>
+ <indexterm>
+ <primary>string_to_table</primary>
+ </indexterm>
<indexterm>
<primary>unnest</primary>
</indexterm>
@@ -14362,6 +14365,22 @@ SELECT NULLIF(value, '(none)') ...
<entry><literal>string_to_array('xx~^~yy~^~zz', '~^~', 'yy')</literal></entry>
<entry><literal>{xx,NULL,zz}</literal></entry>
</row>
+
+
+ <row>
+ <entry>
+ <literal>
+ <function>string_to_table</function>(<type>text</type>, <type>text</type> <optional>, <type>text</type></optional>)
+ </literal>
+ </entry>
+ <entry><type>setof text</type></entry>
+ <entry>splits string into table using supplied delimiter and
+ optional null string</entry>
+ <entry><literal>string_to_table('xx~^~yy~^~zz', '~^~', 'yy')</literal></entry>
+ <entry><literallayout class="monospaced">xx
+yy
+zz</literallayout>(3 rows)</entry>
+ </row>
<row>
<entry>
<literal>
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 2eaabd6231..5d0826e668 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -26,6 +26,7 @@
#include "lib/hyperloglog.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
+#include "nodes/execnodes.h"
#include "parser/scansup.h"
#include "port/pg_bswap.h"
#include "regex/regex.h"
@@ -35,6 +36,7 @@
#include "utils/memutils.h"
#include "utils/pg_locale.h"
#include "utils/sortsupport.h"
+#include "utils/tuplestore.h"
#include "utils/varlena.h"
@@ -92,6 +94,16 @@ typedef struct
pg_locale_t locale;
} VarStringSortSupport;
+/*
+ * Holds target metadata used for split string to array or to table.
+ */
+typedef struct
+{
+ ArrayBuildState *astate;
+ Tuplestorestate *tupstore;
+ TupleDesc tupdesc;
+} SplitStringTargetData;
+
/*
* This should be large enough that most strings will fit, but small enough
* that we feel comfortable putting it on the stack
@@ -139,7 +151,7 @@ static bytea *bytea_substring(Datum str,
bool length_not_specified);
static bytea *bytea_overlay(bytea *t1, bytea *t2, int sp, int sl);
static void appendStringInfoText(StringInfo str, const text *t);
-static Datum text_to_array_internal(PG_FUNCTION_ARGS);
+static bool text_to_array_internal(FunctionCallInfo fcinfo, SplitStringTargetData *tstate);
static text *array_to_text_internal(FunctionCallInfo fcinfo, ArrayType *v,
const char *fldsep, const char *null_string);
static StringInfo makeStringAggState(FunctionCallInfo fcinfo);
@@ -4679,7 +4691,19 @@ text_isequal(text *txt1, text *txt2, Oid collid)
Datum
text_to_array(PG_FUNCTION_ARGS)
{
- return text_to_array_internal(fcinfo);
+ SplitStringTargetData tstate;
+
+ /* reset tstate */
+ memset(&tstate, 0, sizeof(tstate));
+
+ if (!text_to_array_internal(fcinfo, &tstate))
+ PG_RETURN_NULL();
+
+ if (!tstate.astate)
+ PG_RETURN_ARRAYTYPE_P(construct_empty_array(TEXTOID));
+
+ PG_RETURN_ARRAYTYPE_P(makeArrayResult(tstate.astate,
+ CurrentMemoryContext));
}
/*
@@ -4693,16 +4717,98 @@ text_to_array(PG_FUNCTION_ARGS)
Datum
text_to_array_null(PG_FUNCTION_ARGS)
{
- return text_to_array_internal(fcinfo);
+ return text_to_array(fcinfo);
+}
+
+/*
+ * text_to_table
+ * Parse input string and returns substrings as a table.
+ */
+Datum
+text_to_table(PG_FUNCTION_ARGS)
+{
+ ReturnSetInfo *rsi = (ReturnSetInfo *) fcinfo->resultinfo;
+ SplitStringTargetData tstate;
+ MemoryContext old_cxt;
+
+ /* check to see if caller supports us returning a tuplestore */
+ if (rsi == NULL || !IsA(rsi, ReturnSetInfo))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("set-valued function called in context that cannot accept a set")));
+
+ if (!(rsi->allowedModes & SFRM_Materialize))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("materialize mode required, but it is not "
+ "allowed in this context")));
+
+ old_cxt = MemoryContextSwitchTo(rsi->econtext->ecxt_per_query_memory);
+
+ tstate.astate = NULL;
+ tstate.tupdesc = CreateTupleDescCopy(rsi->expectedDesc);
+ tstate.tupstore = tuplestore_begin_heap(rsi->allowedModes & SFRM_Materialize_Random,
+ false, work_mem);
+
+ MemoryContextSwitchTo(old_cxt);
+
+ (void) text_to_array_internal(fcinfo, &tstate);
+
+ tuplestore_donestoring(tstate.tupstore);
+
+ rsi->returnMode = SFRM_Materialize;
+ rsi->setResult = tstate.tupstore;
+ rsi->setDesc = tstate.tupdesc;
+
+ return (Datum) 0;
+}
+
+Datum
+text_to_table_null(PG_FUNCTION_ARGS)
+{
+ return text_to_table(fcinfo);
+}
+
+/*
+ * Add text to result set (table or array). Build a table when set is a expected or build
+ * a array
+ */
+static void
+accum_result(SplitStringTargetData *tstate,
+ text *result_text,
+ bool is_null)
+{
+ if (tstate->tupdesc)
+ {
+ HeapTuple tuple;
+ Datum values[1];
+ bool nulls[1];
+
+ values[0] = PointerGetDatum(result_text);
+ nulls[0] = is_null;
+
+ tuple = heap_form_tuple(tstate->tupdesc, values, nulls);
+ tuplestore_puttuple(tstate->tupstore, tuple);
+ }
+ else
+ {
+ tstate->astate = accumArrayResult(tstate->astate,
+ PointerGetDatum(result_text),
+ is_null,
+ TEXTOID,
+ CurrentMemoryContext);
+ }
}
/*
* common code for text_to_array and text_to_array_null functions
*
* These are not strict so we have to test for null inputs explicitly.
+ * Returns false, when result is null, else returns true.
+ *
*/
-static Datum
-text_to_array_internal(PG_FUNCTION_ARGS)
+static bool
+text_to_array_internal(FunctionCallInfo fcinfo, SplitStringTargetData *tstate)
{
text *inputstring;
text *fldsep;
@@ -4712,11 +4818,10 @@ text_to_array_internal(PG_FUNCTION_ARGS)
char *start_ptr;
text *result_text;
bool is_null;
- ArrayBuildState *astate = NULL;
/* when input string is NULL, then result is NULL too */
if (PG_ARGISNULL(0))
- PG_RETURN_NULL();
+ return false;
inputstring = PG_GETARG_TEXT_PP(0);
@@ -4745,7 +4850,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
/* return empty array for empty input string */
if (inputstring_len < 1)
- PG_RETURN_ARRAYTYPE_P(construct_empty_array(TEXTOID));
+ return true;
/*
* empty field separator: return the input string as a one-element
@@ -4753,22 +4858,11 @@ text_to_array_internal(PG_FUNCTION_ARGS)
*/
if (fldsep_len < 1)
{
- Datum elems[1];
- bool nulls[1];
- int dims[1];
- int lbs[1];
-
/* single element can be a NULL too */
is_null = null_string ? text_isequal(inputstring, null_string, PG_GET_COLLATION()) : false;
- elems[0] = PointerGetDatum(inputstring);
- nulls[0] = is_null;
- dims[0] = 1;
- lbs[0] = 1;
- /* XXX: this hardcodes assumptions about the text type */
- PG_RETURN_ARRAYTYPE_P(construct_md_array(elems, nulls,
- 1, dims, lbs,
- TEXTOID, -1, false, TYPALIGN_INT));
+ accum_result(tstate, inputstring, is_null);
+ return true;
}
text_position_setup(inputstring, fldsep, PG_GET_COLLATION(), &state);
@@ -4802,12 +4896,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
is_null = null_string ? text_isequal(result_text, null_string, PG_GET_COLLATION()) : false;
/* stash away this field */
- astate = accumArrayResult(astate,
- PointerGetDatum(result_text),
- is_null,
- TEXTOID,
- CurrentMemoryContext);
-
+ accum_result(tstate, result_text, is_null);
pfree(result_text);
if (!found)
@@ -4844,12 +4933,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
is_null = null_string ? text_isequal(result_text, null_string, PG_GET_COLLATION()) : false;
/* stash away this field */
- astate = accumArrayResult(astate,
- PointerGetDatum(result_text),
- is_null,
- TEXTOID,
- CurrentMemoryContext);
-
+ accum_result(tstate, result_text, is_null);
pfree(result_text);
start_ptr += chunk_len;
@@ -4857,8 +4941,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
}
}
- PG_RETURN_ARRAYTYPE_P(makeArrayResult(astate,
- CurrentMemoryContext));
+ return true;
}
/*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4bce3ad8de..2fc97f830f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3561,6 +3561,14 @@
{ oid => '2768', descr => 'split string by pattern',
proname => 'regexp_split_to_array', prorettype => '_text',
proargtypes => 'text text text', prosrc => 'regexp_split_to_array' },
+{ oid => '2228', descr => 'split delimited text',
+ proname => 'string_to_table', prorows => '1000', proretset => 't',
+ prorettype => 'text', proargtypes => 'text text',
+ prosrc => 'text_to_table' },
+{ oid => '2282', descr => 'split delimited text with null string',
+ proname => 'string_to_table', prorows => '1000', proretset => 't',
+ prorettype => 'text', proargtypes => 'text text text',
+ prosrc => 'text_to_table_null' },
{ oid => '2089', descr => 'convert int4 number to hex',
proname => 'to_hex', prorettype => 'text', proargtypes => 'int4',
prosrc => 'to_hex32' },
diff --git a/src/test/regress/expected/arrays.out b/src/test/regress/expected/arrays.out
index c730563f03..f024b153cc 100644
--- a/src/test/regress/expected/arrays.out
+++ b/src/test/regress/expected/arrays.out
@@ -1755,6 +1755,109 @@ select string_to_array('1,2,3,4,*,6', ',', '*');
{1,2,3,4,NULL,6}
(1 row)
+select string_to_table('1|2|3', '|');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+(3 rows)
+
+select string_to_table('1|2|3|', '|');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+
+(4 rows)
+
+select string_to_table('1||2|3||', '||');
+ string_to_table
+-----------------
+ 1
+ 2|3
+
+(3 rows)
+
+select string_to_table('1|2|3', '');
+ string_to_table
+-----------------
+ 1|2|3
+(1 row)
+
+select string_to_table('', '|');
+ string_to_table
+-----------------
+(0 rows)
+
+select string_to_table('1|2|3', NULL);
+ string_to_table
+-----------------
+(0 rows)
+
+select string_to_table(NULL, '|') IS NULL;
+ ?column?
+----------
+(0 rows)
+
+select string_to_table('abc', '');
+ string_to_table
+-----------------
+ abc
+(1 row)
+
+select string_to_table('abc', '', 'abc');
+ string_to_table
+-----------------
+
+(1 row)
+
+select string_to_table('abc', ',');
+ string_to_table
+-----------------
+ abc
+(1 row)
+
+select string_to_table('abc', ',', 'abc');
+ string_to_table
+-----------------
+
+(1 row)
+
+select string_to_table('1,2,3,4,,6', ',');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+ 4
+
+ 6
+(6 rows)
+
+select string_to_table('1,2,3,4,,6', ',', '');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+ 4
+
+ 6
+(6 rows)
+
+select string_to_table('1,2,3,4,*,6', ',', '*');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+ 4
+
+ 6
+(6 rows)
+
select array_to_string(NULL::int4[], ',') IS NULL;
?column?
----------
diff --git a/src/test/regress/sql/arrays.sql b/src/test/regress/sql/arrays.sql
index 25dd4e2c6d..d57352cf47 100644
--- a/src/test/regress/sql/arrays.sql
+++ b/src/test/regress/sql/arrays.sql
@@ -544,6 +544,21 @@ select string_to_array('1,2,3,4,,6', ',');
select string_to_array('1,2,3,4,,6', ',', '');
select string_to_array('1,2,3,4,*,6', ',', '*');
+select string_to_table('1|2|3', '|');
+select string_to_table('1|2|3|', '|');
+select string_to_table('1||2|3||', '||');
+select string_to_table('1|2|3', '');
+select string_to_table('', '|');
+select string_to_table('1|2|3', NULL);
+select string_to_table(NULL, '|') IS NULL;
+select string_to_table('abc', '');
+select string_to_table('abc', '', 'abc');
+select string_to_table('abc', ',');
+select string_to_table('abc', ',', 'abc');
+select string_to_table('1,2,3,4,,6', ',');
+select string_to_table('1,2,3,4,,6', ',', '');
+select string_to_table('1,2,3,4,*,6', ',', '*');
+
select array_to_string(NULL::int4[], ',') IS NULL;
select array_to_string('{}'::int4[], ',');
select array_to_string(array[1,2,3,4,NULL,6], ',');
On Fri, Apr 17, 2020 at 07:47:15PM +0200, Pavel Stehule wrote:
I propose new function string_to_table. This function is significantly
+1
+/* + * Add text to result set (table or array). Build a table when set is a expected or build + * a array
as expected (??)
*an* array
+select string_to_table('abc', '', 'abc'); + string_to_table +----------------- + +(1 row)
Maybe you should \pset null '(null)' for this
--
Justin
pá 17. 4. 2020 v 23:29 odesílatel Justin Pryzby <pryzby@telsasoft.com>
napsal:
On Fri, Apr 17, 2020 at 07:47:15PM +0200, Pavel Stehule wrote:
I propose new function string_to_table. This function is significantly
+1
+/* + * Add text to result set (table or array). Build a table when set is aexpected or build
+ * a array
as expected (??)
*an* array
I tried to fix this comment
+select string_to_table('abc', '', 'abc'); + string_to_table +----------------- + +(1 row)Maybe you should \pset null '(null)' for this
changing NULL output can break lot of existing tests, but I add second
column with info about null
+select string_to_table('1,2,3,4,*,6', ',', '*'),
string_to_table('1,2,3,4,*,6', ',', '*') IS NULL;
+ string_to_table | ?column?
+-----------------+----------
+ 1 | f
+ 2 | f
+ 3 | f
+ 4 | f
+ | t
+ 6 | f
+(6 rows)
Regards
Pavel
Show quoted text
--
Justin
Attachments:
string_to_table-20200418.patchtext/x-patch; charset=US-ASCII; name=string_to_table-20200418.patchDownload
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 92c1835ae3..ba87e472e1 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -14004,6 +14004,9 @@ SELECT NULLIF(value, '(none)') ...
<indexterm>
<primary>string_to_array</primary>
</indexterm>
+ <indexterm>
+ <primary>string_to_table</primary>
+ </indexterm>
<indexterm>
<primary>unnest</primary>
</indexterm>
@@ -14206,6 +14209,22 @@ SELECT NULLIF(value, '(none)') ...
<entry><literal>string_to_array('xx~^~yy~^~zz', '~^~', 'yy')</literal></entry>
<entry><literal>{xx,NULL,zz}</literal></entry>
</row>
+
+
+ <row>
+ <entry>
+ <literal>
+ <function>string_to_table</function>(<type>text</type>, <type>text</type> <optional>, <type>text</type></optional>)
+ </literal>
+ </entry>
+ <entry><type>setof text</type></entry>
+ <entry>splits string into table using supplied delimiter and
+ optional null string</entry>
+ <entry><literal>string_to_table('xx~^~yy~^~zz', '~^~', 'yy')</literal></entry>
+ <entry><literallayout class="monospaced">xx
+yy
+zz</literallayout>(3 rows)</entry>
+ </row>
<row>
<entry>
<literal>
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 2eaabd6231..650813d8b8 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -26,6 +26,7 @@
#include "lib/hyperloglog.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
+#include "nodes/execnodes.h"
#include "parser/scansup.h"
#include "port/pg_bswap.h"
#include "regex/regex.h"
@@ -35,6 +36,7 @@
#include "utils/memutils.h"
#include "utils/pg_locale.h"
#include "utils/sortsupport.h"
+#include "utils/tuplestore.h"
#include "utils/varlena.h"
@@ -92,6 +94,16 @@ typedef struct
pg_locale_t locale;
} VarStringSortSupport;
+/*
+ * Holds target metadata used for split string to array or to table.
+ */
+typedef struct
+{
+ ArrayBuildState *astate;
+ Tuplestorestate *tupstore;
+ TupleDesc tupdesc;
+} SplitStringTargetData;
+
/*
* This should be large enough that most strings will fit, but small enough
* that we feel comfortable putting it on the stack
@@ -139,7 +151,7 @@ static bytea *bytea_substring(Datum str,
bool length_not_specified);
static bytea *bytea_overlay(bytea *t1, bytea *t2, int sp, int sl);
static void appendStringInfoText(StringInfo str, const text *t);
-static Datum text_to_array_internal(PG_FUNCTION_ARGS);
+static bool text_to_array_internal(FunctionCallInfo fcinfo, SplitStringTargetData *tstate);
static text *array_to_text_internal(FunctionCallInfo fcinfo, ArrayType *v,
const char *fldsep, const char *null_string);
static StringInfo makeStringAggState(FunctionCallInfo fcinfo);
@@ -4679,7 +4691,19 @@ text_isequal(text *txt1, text *txt2, Oid collid)
Datum
text_to_array(PG_FUNCTION_ARGS)
{
- return text_to_array_internal(fcinfo);
+ SplitStringTargetData tstate;
+
+ /* reset tstate */
+ memset(&tstate, 0, sizeof(tstate));
+
+ if (!text_to_array_internal(fcinfo, &tstate))
+ PG_RETURN_NULL();
+
+ if (!tstate.astate)
+ PG_RETURN_ARRAYTYPE_P(construct_empty_array(TEXTOID));
+
+ PG_RETURN_ARRAYTYPE_P(makeArrayResult(tstate.astate,
+ CurrentMemoryContext));
}
/*
@@ -4693,16 +4717,98 @@ text_to_array(PG_FUNCTION_ARGS)
Datum
text_to_array_null(PG_FUNCTION_ARGS)
{
- return text_to_array_internal(fcinfo);
+ return text_to_array(fcinfo);
+}
+
+/*
+ * text_to_table
+ * Parse input string and returns substrings as a table.
+ */
+Datum
+text_to_table(PG_FUNCTION_ARGS)
+{
+ ReturnSetInfo *rsi = (ReturnSetInfo *) fcinfo->resultinfo;
+ SplitStringTargetData tstate;
+ MemoryContext old_cxt;
+
+ /* check to see if caller supports us returning a tuplestore */
+ if (rsi == NULL || !IsA(rsi, ReturnSetInfo))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("set-valued function called in context that cannot accept a set")));
+
+ if (!(rsi->allowedModes & SFRM_Materialize))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("materialize mode required, but it is not "
+ "allowed in this context")));
+
+ old_cxt = MemoryContextSwitchTo(rsi->econtext->ecxt_per_query_memory);
+
+ tstate.astate = NULL;
+ tstate.tupdesc = CreateTupleDescCopy(rsi->expectedDesc);
+ tstate.tupstore = tuplestore_begin_heap(rsi->allowedModes & SFRM_Materialize_Random,
+ false, work_mem);
+
+ MemoryContextSwitchTo(old_cxt);
+
+ (void) text_to_array_internal(fcinfo, &tstate);
+
+ tuplestore_donestoring(tstate.tupstore);
+
+ rsi->returnMode = SFRM_Materialize;
+ rsi->setResult = tstate.tupstore;
+ rsi->setDesc = tstate.tupdesc;
+
+ return (Datum) 0;
+}
+
+Datum
+text_to_table_null(PG_FUNCTION_ARGS)
+{
+ return text_to_table(fcinfo);
+}
+
+/*
+ * Add text to result set (table or array). When a result set is expected,
+ * then we fill a tuplestore, else we prepare an array.
+ */
+static void
+accum_result(SplitStringTargetData *tstate,
+ text *result_text,
+ bool is_null)
+{
+ if (tstate->tupdesc)
+ {
+ HeapTuple tuple;
+ Datum values[1];
+ bool nulls[1];
+
+ values[0] = PointerGetDatum(result_text);
+ nulls[0] = is_null;
+
+ tuple = heap_form_tuple(tstate->tupdesc, values, nulls);
+ tuplestore_puttuple(tstate->tupstore, tuple);
+ }
+ else
+ {
+ tstate->astate = accumArrayResult(tstate->astate,
+ PointerGetDatum(result_text),
+ is_null,
+ TEXTOID,
+ CurrentMemoryContext);
+ }
}
/*
* common code for text_to_array and text_to_array_null functions
*
* These are not strict so we have to test for null inputs explicitly.
+ * Returns false, when result is null, else returns true.
+ *
*/
-static Datum
-text_to_array_internal(PG_FUNCTION_ARGS)
+static bool
+text_to_array_internal(FunctionCallInfo fcinfo, SplitStringTargetData *tstate)
{
text *inputstring;
text *fldsep;
@@ -4712,11 +4818,10 @@ text_to_array_internal(PG_FUNCTION_ARGS)
char *start_ptr;
text *result_text;
bool is_null;
- ArrayBuildState *astate = NULL;
/* when input string is NULL, then result is NULL too */
if (PG_ARGISNULL(0))
- PG_RETURN_NULL();
+ return false;
inputstring = PG_GETARG_TEXT_PP(0);
@@ -4745,7 +4850,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
/* return empty array for empty input string */
if (inputstring_len < 1)
- PG_RETURN_ARRAYTYPE_P(construct_empty_array(TEXTOID));
+ return true;
/*
* empty field separator: return the input string as a one-element
@@ -4753,22 +4858,11 @@ text_to_array_internal(PG_FUNCTION_ARGS)
*/
if (fldsep_len < 1)
{
- Datum elems[1];
- bool nulls[1];
- int dims[1];
- int lbs[1];
-
/* single element can be a NULL too */
is_null = null_string ? text_isequal(inputstring, null_string, PG_GET_COLLATION()) : false;
- elems[0] = PointerGetDatum(inputstring);
- nulls[0] = is_null;
- dims[0] = 1;
- lbs[0] = 1;
- /* XXX: this hardcodes assumptions about the text type */
- PG_RETURN_ARRAYTYPE_P(construct_md_array(elems, nulls,
- 1, dims, lbs,
- TEXTOID, -1, false, TYPALIGN_INT));
+ accum_result(tstate, inputstring, is_null);
+ return true;
}
text_position_setup(inputstring, fldsep, PG_GET_COLLATION(), &state);
@@ -4802,12 +4896,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
is_null = null_string ? text_isequal(result_text, null_string, PG_GET_COLLATION()) : false;
/* stash away this field */
- astate = accumArrayResult(astate,
- PointerGetDatum(result_text),
- is_null,
- TEXTOID,
- CurrentMemoryContext);
-
+ accum_result(tstate, result_text, is_null);
pfree(result_text);
if (!found)
@@ -4844,12 +4933,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
is_null = null_string ? text_isequal(result_text, null_string, PG_GET_COLLATION()) : false;
/* stash away this field */
- astate = accumArrayResult(astate,
- PointerGetDatum(result_text),
- is_null,
- TEXTOID,
- CurrentMemoryContext);
-
+ accum_result(tstate, result_text, is_null);
pfree(result_text);
start_ptr += chunk_len;
@@ -4857,8 +4941,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
}
}
- PG_RETURN_ARRAYTYPE_P(makeArrayResult(astate,
- CurrentMemoryContext));
+ return true;
}
/*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4bce3ad8de..2fc97f830f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3561,6 +3561,14 @@
{ oid => '2768', descr => 'split string by pattern',
proname => 'regexp_split_to_array', prorettype => '_text',
proargtypes => 'text text text', prosrc => 'regexp_split_to_array' },
+{ oid => '2228', descr => 'split delimited text',
+ proname => 'string_to_table', prorows => '1000', proretset => 't',
+ prorettype => 'text', proargtypes => 'text text',
+ prosrc => 'text_to_table' },
+{ oid => '2282', descr => 'split delimited text with null string',
+ proname => 'string_to_table', prorows => '1000', proretset => 't',
+ prorettype => 'text', proargtypes => 'text text text',
+ prosrc => 'text_to_table_null' },
{ oid => '2089', descr => 'convert int4 number to hex',
proname => 'to_hex', prorettype => 'text', proargtypes => 'int4',
prosrc => 'to_hex32' },
diff --git a/src/test/regress/expected/arrays.out b/src/test/regress/expected/arrays.out
index c730563f03..7689f4698f 100644
--- a/src/test/regress/expected/arrays.out
+++ b/src/test/regress/expected/arrays.out
@@ -1755,6 +1755,109 @@ select string_to_array('1,2,3,4,*,6', ',', '*');
{1,2,3,4,NULL,6}
(1 row)
+select string_to_table('1|2|3', '|');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+(3 rows)
+
+select string_to_table('1|2|3|', '|');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+
+(4 rows)
+
+select string_to_table('1||2|3||', '||');
+ string_to_table
+-----------------
+ 1
+ 2|3
+
+(3 rows)
+
+select string_to_table('1|2|3', '');
+ string_to_table
+-----------------
+ 1|2|3
+(1 row)
+
+select string_to_table('', '|');
+ string_to_table
+-----------------
+(0 rows)
+
+select string_to_table('1|2|3', NULL);
+ string_to_table
+-----------------
+(0 rows)
+
+select string_to_table(NULL, '|') IS NULL;
+ ?column?
+----------
+(0 rows)
+
+select string_to_table('abc', '');
+ string_to_table
+-----------------
+ abc
+(1 row)
+
+select string_to_table('abc', '', 'abc');
+ string_to_table
+-----------------
+
+(1 row)
+
+select string_to_table('abc', ',');
+ string_to_table
+-----------------
+ abc
+(1 row)
+
+select string_to_table('abc', ',', 'abc');
+ string_to_table
+-----------------
+
+(1 row)
+
+select string_to_table('1,2,3,4,,6', ',');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+ 4
+
+ 6
+(6 rows)
+
+select string_to_table('1,2,3,4,,6', ',', ''), string_to_table('1,2,3,4,,6', ',', '') IS NULL;
+ string_to_table | ?column?
+-----------------+----------
+ 1 | f
+ 2 | f
+ 3 | f
+ 4 | f
+ | t
+ 6 | f
+(6 rows)
+
+select string_to_table('1,2,3,4,*,6', ',', '*'), string_to_table('1,2,3,4,*,6', ',', '*') IS NULL;
+ string_to_table | ?column?
+-----------------+----------
+ 1 | f
+ 2 | f
+ 3 | f
+ 4 | f
+ | t
+ 6 | f
+(6 rows)
+
select array_to_string(NULL::int4[], ',') IS NULL;
?column?
----------
diff --git a/src/test/regress/sql/arrays.sql b/src/test/regress/sql/arrays.sql
index 25dd4e2c6d..5c10156a71 100644
--- a/src/test/regress/sql/arrays.sql
+++ b/src/test/regress/sql/arrays.sql
@@ -544,6 +544,21 @@ select string_to_array('1,2,3,4,,6', ',');
select string_to_array('1,2,3,4,,6', ',', '');
select string_to_array('1,2,3,4,*,6', ',', '*');
+select string_to_table('1|2|3', '|');
+select string_to_table('1|2|3|', '|');
+select string_to_table('1||2|3||', '||');
+select string_to_table('1|2|3', '');
+select string_to_table('', '|');
+select string_to_table('1|2|3', NULL);
+select string_to_table(NULL, '|') IS NULL;
+select string_to_table('abc', '');
+select string_to_table('abc', '', 'abc');
+select string_to_table('abc', ',');
+select string_to_table('abc', ',', 'abc');
+select string_to_table('1,2,3,4,,6', ',');
+select string_to_table('1,2,3,4,,6', ',', ''), string_to_table('1,2,3,4,,6', ',', '') IS NULL;
+select string_to_table('1,2,3,4,*,6', ',', '*'), string_to_table('1,2,3,4,*,6', ',', '*') IS NULL;
+
select array_to_string(NULL::int4[], ',') IS NULL;
select array_to_string('{}'::int4[], ',');
select array_to_string(array[1,2,3,4,NULL,6], ',');
+{ oid => '2228', descr => 'split delimited text',
+ proname => 'string_to_table', prorows => '1000', proretset => 't',
+ prorettype => 'text', proargtypes => 'text text',
+ prosrc => 'text_to_table' },
+{ oid => '2282', descr => 'split delimited text with null string',
+ proname => 'string_to_table', prorows => '1000', proretset => 't',
+ prorettype => 'text', proargtypes => 'text text text',
+ prosrc => 'text_to_table_null' },
I go through the patch, and everything looks good to me. But I do not know
why it needs a 'text_to_table_null()', it's ok to put a 'text_to_table' there, I think.
Regards,
Highgo Software (Canada/China/Pakistan)
URL : www.highgo.ca
EMAIL: mailto:movead(dot)li(at)highgo(dot)ca
Hi
čt 4. 6. 2020 v 11:49 odesílatel movead.li@highgo.ca <movead.li@highgo.ca>
napsal:
+{ oid => '2228', descr => 'split delimited text', + proname => 'string_to_table', prorows => '1000', proretset => 't', + prorettype => 'text', proargtypes => 'text text', + prosrc => 'text_to_table' }, +{ oid => '2282', descr => 'split delimited text with null string', + proname => 'string_to_table', prorows => '1000', proretset => 't', + prorettype => 'text', proargtypes => 'text text text', + prosrc => 'text_to_table_null' },I go through the patch, and everything looks good to me. But I do not know
why it needs a 'text_to_table_null()', it's ok to put a 'text_to_table'
there, I think.
It is a convention in Postgres - every SQL unique signature has its own
unique internal C function.
I am sending a refreshed patch.
Regards
Pavel
Show quoted text
------------------------------
Regards,
Highgo Software (Canada/China/Pakistan)
URL : www.highgo.ca
EMAIL: mailto:movead(dot)li(at)highgo(dot)ca
Attachments:
string_to_table-20200605.patchtext/x-patch; charset=US-ASCII; name=string_to_table-20200605.patchDownload
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 7c06afd3ea..974499ff2e 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -3437,6 +3437,27 @@ repeat('Pg', 4) <returnvalue>PgPgPgPg</returnvalue>
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>string_to_table</primary>
+ </indexterm>
+ <function>string_to_table</function> ( <parameter>string</parameter> <type>text</type>, <parameter>delimiter</parameter> <type>text</type> <optional>, <parameter>nullstr</parameter> <type>text</type> </optional> )
+ <returnvalue>set of text</returnvalue>
+ </para>
+ <para>
+ splits string into table using supplied delimiter and
+ optional null string.
+ </para>
+ <para>
+ <literal>string_to_table('xx~^~yy~^~zz', '~^~', 'yy')</literal>
+ <returnvalue></returnvalue>
+<programlisting>xx
+yy,
+zz</programlisting>
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
@@ -16717,21 +16738,6 @@ strict $.track.segments[*].location
</para></entry>
</row>
- <row>
- <entry role="func_table_entry"><para role="func_signature">
- <replaceable>string</replaceable> <literal>starts with</literal> <replaceable>string</replaceable>
- <returnvalue>boolean</returnvalue>
- </para>
- <para>
- Tests whether the second operand is an initial substring of the first
- operand.
- </para>
- <para>
- <literal>jsonb_path_query('["John Smith", "Mary Stone", "Bob Johnson"]', '$[*] ? (@ starts with "John")')</literal>
- <returnvalue>"John Smith"</returnvalue>
- </para></entry>
- </row>
-
<row>
<entry role="func_table_entry"><para role="func_signature">
<literal>exists</literal> <literal>(</literal> <replaceable>path_expression</replaceable> <literal>)</literal>
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 2eaabd6231..650813d8b8 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -26,6 +26,7 @@
#include "lib/hyperloglog.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
+#include "nodes/execnodes.h"
#include "parser/scansup.h"
#include "port/pg_bswap.h"
#include "regex/regex.h"
@@ -35,6 +36,7 @@
#include "utils/memutils.h"
#include "utils/pg_locale.h"
#include "utils/sortsupport.h"
+#include "utils/tuplestore.h"
#include "utils/varlena.h"
@@ -92,6 +94,16 @@ typedef struct
pg_locale_t locale;
} VarStringSortSupport;
+/*
+ * Holds target metadata used for split string to array or to table.
+ */
+typedef struct
+{
+ ArrayBuildState *astate;
+ Tuplestorestate *tupstore;
+ TupleDesc tupdesc;
+} SplitStringTargetData;
+
/*
* This should be large enough that most strings will fit, but small enough
* that we feel comfortable putting it on the stack
@@ -139,7 +151,7 @@ static bytea *bytea_substring(Datum str,
bool length_not_specified);
static bytea *bytea_overlay(bytea *t1, bytea *t2, int sp, int sl);
static void appendStringInfoText(StringInfo str, const text *t);
-static Datum text_to_array_internal(PG_FUNCTION_ARGS);
+static bool text_to_array_internal(FunctionCallInfo fcinfo, SplitStringTargetData *tstate);
static text *array_to_text_internal(FunctionCallInfo fcinfo, ArrayType *v,
const char *fldsep, const char *null_string);
static StringInfo makeStringAggState(FunctionCallInfo fcinfo);
@@ -4679,7 +4691,19 @@ text_isequal(text *txt1, text *txt2, Oid collid)
Datum
text_to_array(PG_FUNCTION_ARGS)
{
- return text_to_array_internal(fcinfo);
+ SplitStringTargetData tstate;
+
+ /* reset tstate */
+ memset(&tstate, 0, sizeof(tstate));
+
+ if (!text_to_array_internal(fcinfo, &tstate))
+ PG_RETURN_NULL();
+
+ if (!tstate.astate)
+ PG_RETURN_ARRAYTYPE_P(construct_empty_array(TEXTOID));
+
+ PG_RETURN_ARRAYTYPE_P(makeArrayResult(tstate.astate,
+ CurrentMemoryContext));
}
/*
@@ -4693,16 +4717,98 @@ text_to_array(PG_FUNCTION_ARGS)
Datum
text_to_array_null(PG_FUNCTION_ARGS)
{
- return text_to_array_internal(fcinfo);
+ return text_to_array(fcinfo);
+}
+
+/*
+ * text_to_table
+ * Parse input string and returns substrings as a table.
+ */
+Datum
+text_to_table(PG_FUNCTION_ARGS)
+{
+ ReturnSetInfo *rsi = (ReturnSetInfo *) fcinfo->resultinfo;
+ SplitStringTargetData tstate;
+ MemoryContext old_cxt;
+
+ /* check to see if caller supports us returning a tuplestore */
+ if (rsi == NULL || !IsA(rsi, ReturnSetInfo))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("set-valued function called in context that cannot accept a set")));
+
+ if (!(rsi->allowedModes & SFRM_Materialize))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("materialize mode required, but it is not "
+ "allowed in this context")));
+
+ old_cxt = MemoryContextSwitchTo(rsi->econtext->ecxt_per_query_memory);
+
+ tstate.astate = NULL;
+ tstate.tupdesc = CreateTupleDescCopy(rsi->expectedDesc);
+ tstate.tupstore = tuplestore_begin_heap(rsi->allowedModes & SFRM_Materialize_Random,
+ false, work_mem);
+
+ MemoryContextSwitchTo(old_cxt);
+
+ (void) text_to_array_internal(fcinfo, &tstate);
+
+ tuplestore_donestoring(tstate.tupstore);
+
+ rsi->returnMode = SFRM_Materialize;
+ rsi->setResult = tstate.tupstore;
+ rsi->setDesc = tstate.tupdesc;
+
+ return (Datum) 0;
+}
+
+Datum
+text_to_table_null(PG_FUNCTION_ARGS)
+{
+ return text_to_table(fcinfo);
+}
+
+/*
+ * Add text to result set (table or array). When a result set is expected,
+ * then we fill a tuplestore, else we prepare an array.
+ */
+static void
+accum_result(SplitStringTargetData *tstate,
+ text *result_text,
+ bool is_null)
+{
+ if (tstate->tupdesc)
+ {
+ HeapTuple tuple;
+ Datum values[1];
+ bool nulls[1];
+
+ values[0] = PointerGetDatum(result_text);
+ nulls[0] = is_null;
+
+ tuple = heap_form_tuple(tstate->tupdesc, values, nulls);
+ tuplestore_puttuple(tstate->tupstore, tuple);
+ }
+ else
+ {
+ tstate->astate = accumArrayResult(tstate->astate,
+ PointerGetDatum(result_text),
+ is_null,
+ TEXTOID,
+ CurrentMemoryContext);
+ }
}
/*
* common code for text_to_array and text_to_array_null functions
*
* These are not strict so we have to test for null inputs explicitly.
+ * Returns false, when result is null, else returns true.
+ *
*/
-static Datum
-text_to_array_internal(PG_FUNCTION_ARGS)
+static bool
+text_to_array_internal(FunctionCallInfo fcinfo, SplitStringTargetData *tstate)
{
text *inputstring;
text *fldsep;
@@ -4712,11 +4818,10 @@ text_to_array_internal(PG_FUNCTION_ARGS)
char *start_ptr;
text *result_text;
bool is_null;
- ArrayBuildState *astate = NULL;
/* when input string is NULL, then result is NULL too */
if (PG_ARGISNULL(0))
- PG_RETURN_NULL();
+ return false;
inputstring = PG_GETARG_TEXT_PP(0);
@@ -4745,7 +4850,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
/* return empty array for empty input string */
if (inputstring_len < 1)
- PG_RETURN_ARRAYTYPE_P(construct_empty_array(TEXTOID));
+ return true;
/*
* empty field separator: return the input string as a one-element
@@ -4753,22 +4858,11 @@ text_to_array_internal(PG_FUNCTION_ARGS)
*/
if (fldsep_len < 1)
{
- Datum elems[1];
- bool nulls[1];
- int dims[1];
- int lbs[1];
-
/* single element can be a NULL too */
is_null = null_string ? text_isequal(inputstring, null_string, PG_GET_COLLATION()) : false;
- elems[0] = PointerGetDatum(inputstring);
- nulls[0] = is_null;
- dims[0] = 1;
- lbs[0] = 1;
- /* XXX: this hardcodes assumptions about the text type */
- PG_RETURN_ARRAYTYPE_P(construct_md_array(elems, nulls,
- 1, dims, lbs,
- TEXTOID, -1, false, TYPALIGN_INT));
+ accum_result(tstate, inputstring, is_null);
+ return true;
}
text_position_setup(inputstring, fldsep, PG_GET_COLLATION(), &state);
@@ -4802,12 +4896,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
is_null = null_string ? text_isequal(result_text, null_string, PG_GET_COLLATION()) : false;
/* stash away this field */
- astate = accumArrayResult(astate,
- PointerGetDatum(result_text),
- is_null,
- TEXTOID,
- CurrentMemoryContext);
-
+ accum_result(tstate, result_text, is_null);
pfree(result_text);
if (!found)
@@ -4844,12 +4933,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
is_null = null_string ? text_isequal(result_text, null_string, PG_GET_COLLATION()) : false;
/* stash away this field */
- astate = accumArrayResult(astate,
- PointerGetDatum(result_text),
- is_null,
- TEXTOID,
- CurrentMemoryContext);
-
+ accum_result(tstate, result_text, is_null);
pfree(result_text);
start_ptr += chunk_len;
@@ -4857,8 +4941,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
}
}
- PG_RETURN_ARRAYTYPE_P(makeArrayResult(astate,
- CurrentMemoryContext));
+ return true;
}
/*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 61f2c2f5b4..fd47fd4c90 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3561,6 +3561,14 @@
{ oid => '2768', descr => 'split string by pattern',
proname => 'regexp_split_to_array', prorettype => '_text',
proargtypes => 'text text text', prosrc => 'regexp_split_to_array' },
+{ oid => '2228', descr => 'split delimited text',
+ proname => 'string_to_table', prorows => '1000', proretset => 't',
+ prorettype => 'text', proargtypes => 'text text',
+ prosrc => 'text_to_table' },
+{ oid => '2282', descr => 'split delimited text with null string',
+ proname => 'string_to_table', prorows => '1000', proretset => 't',
+ prorettype => 'text', proargtypes => 'text text text',
+ prosrc => 'text_to_table_null' },
{ oid => '2089', descr => 'convert int4 number to hex',
proname => 'to_hex', prorettype => 'text', proargtypes => 'int4',
prosrc => 'to_hex32' },
diff --git a/src/test/regress/expected/arrays.out b/src/test/regress/expected/arrays.out
index c730563f03..4496104235 100644
--- a/src/test/regress/expected/arrays.out
+++ b/src/test/regress/expected/arrays.out
@@ -1755,6 +1755,142 @@ select string_to_array('1,2,3,4,*,6', ',', '*');
{1,2,3,4,NULL,6}
(1 row)
+select string_to_table('1|2|3', '|');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+(3 rows)
+
+select string_to_table('1|2|3|', '|');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+
+(4 rows)
+
+select string_to_table('1||2|3||', '||');
+ string_to_table
+-----------------
+ 1
+ 2|3
+
+(3 rows)
+
+select string_to_table('1|2|3', '');
+ string_to_table
+-----------------
+ 1|2|3
+(1 row)
+
+select string_to_table('', '|');
+ string_to_table
+-----------------
+(0 rows)
+
+select string_to_table('1|2|3', NULL);
+ string_to_table
+-----------------
+(0 rows)
+
+select string_to_table(NULL, '|') IS NULL;
+ ?column?
+----------
+(0 rows)
+
+select string_to_table('abc', '');
+ string_to_table
+-----------------
+ abc
+(1 row)
+
+select string_to_table('abc', '', 'abc');
+ string_to_table
+-----------------
+
+(1 row)
+
+select string_to_table('abc', ',');
+ string_to_table
+-----------------
+ abc
+(1 row)
+
+select string_to_table('abc', ',', 'abc');
+ string_to_table
+-----------------
+
+(1 row)
+
+select string_to_table('1,2,3,4,,6', ',');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+ 4
+
+ 6
+(6 rows)
+
+select coalesce(v, 'NULL') FROM string_to_table('1,2,3,4,,6', ',') g(v);
+ coalesce
+----------
+ 1
+ 2
+ 3
+ 4
+
+ 6
+(6 rows)
+
+select string_to_table('1,2,3,4,,6', ',', '');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+ 4
+
+ 6
+(6 rows)
+
+select coalesce(v, '***') FROM string_to_table('1,2,3,4,,6', ',', '') g(v);
+ coalesce
+----------
+ 1
+ 2
+ 3
+ 4
+ ***
+ 6
+(6 rows)
+
+select string_to_table('1,2,3,4,*,6', ',', '*');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+ 4
+
+ 6
+(6 rows)
+
+select coalesce(v, '***') FROM string_to_table('1,2,3,4,*,6', ',', '*') g(v);
+ coalesce
+----------
+ 1
+ 2
+ 3
+ 4
+ ***
+ 6
+(6 rows)
+
select array_to_string(NULL::int4[], ',') IS NULL;
?column?
----------
diff --git a/src/test/regress/sql/arrays.sql b/src/test/regress/sql/arrays.sql
index 25dd4e2c6d..d4d5982b80 100644
--- a/src/test/regress/sql/arrays.sql
+++ b/src/test/regress/sql/arrays.sql
@@ -544,6 +544,24 @@ select string_to_array('1,2,3,4,,6', ',');
select string_to_array('1,2,3,4,,6', ',', '');
select string_to_array('1,2,3,4,*,6', ',', '*');
+select string_to_table('1|2|3', '|');
+select string_to_table('1|2|3|', '|');
+select string_to_table('1||2|3||', '||');
+select string_to_table('1|2|3', '');
+select string_to_table('', '|');
+select string_to_table('1|2|3', NULL);
+select string_to_table(NULL, '|') IS NULL;
+select string_to_table('abc', '');
+select string_to_table('abc', '', 'abc');
+select string_to_table('abc', ',');
+select string_to_table('abc', ',', 'abc');
+select string_to_table('1,2,3,4,,6', ',');
+select coalesce(v, 'NULL') FROM string_to_table('1,2,3,4,,6', ',') g(v);
+select string_to_table('1,2,3,4,,6', ',', '');
+select coalesce(v, '***') FROM string_to_table('1,2,3,4,,6', ',', '') g(v);
+select string_to_table('1,2,3,4,*,6', ',', '*');
+select coalesce(v, '***') FROM string_to_table('1,2,3,4,*,6', ',', '*') g(v);
+
select array_to_string(NULL::int4[], ',') IS NULL;
select array_to_string('{}'::int4[], ',');
select array_to_string(array[1,2,3,4,NULL,6], ',');
pá 5. 6. 2020 v 13:55 odesílatel Pavel Stehule <pavel.stehule@gmail.com>
napsal:
Hi
čt 4. 6. 2020 v 11:49 odesílatel movead.li@highgo.ca <movead.li@highgo.ca>
napsal:+{ oid => '2228', descr => 'split delimited text', + proname => 'string_to_table', prorows => '1000', proretset => 't', + prorettype => 'text', proargtypes => 'text text', + prosrc => 'text_to_table' }, +{ oid => '2282', descr => 'split delimited text with null string', + proname => 'string_to_table', prorows => '1000', proretset => 't', + prorettype => 'text', proargtypes => 'text text text', + prosrc => 'text_to_table_null' },I go through the patch, and everything looks good to me. But I do not know
why it needs a 'text_to_table_null()', it's ok to put a 'text_to_table'
there, I think.It is a convention in Postgres - every SQL unique signature has its own
unique internal C function.I am sending a refreshed patch.
rebase
Regards
Pavel
Show quoted text
Regards
Pavel
------------------------------
Regards,
Highgo Software (Canada/China/Pakistan)
URL : www.highgo.ca
EMAIL: mailto:movead(dot)li(at)highgo(dot)ca
Attachments:
string_to_table-20200705.patchtext/x-patch; charset=US-ASCII; name=string_to_table-20200705.patchDownload
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index f065856535..e9f10c162f 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -3458,6 +3458,27 @@ repeat('Pg', 4) <returnvalue>PgPgPgPg</returnvalue>
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>string_to_table</primary>
+ </indexterm>
+ <function>string_to_table</function> ( <parameter>string</parameter> <type>text</type>, <parameter>delimiter</parameter> <type>text</type> <optional>, <parameter>nullstr</parameter> <type>text</type> </optional> )
+ <returnvalue>set of text</returnvalue>
+ </para>
+ <para>
+ splits string into table using supplied delimiter and
+ optional null string.
+ </para>
+ <para>
+ <literal>string_to_table('xx~^~yy~^~zz', '~^~', 'yy')</literal>
+ <returnvalue></returnvalue>
+<programlisting>xx
+yy,
+zz</programlisting>
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
@@ -16747,21 +16768,6 @@ strict $.track.segments[*].location
</para></entry>
</row>
- <row>
- <entry role="func_table_entry"><para role="func_signature">
- <replaceable>string</replaceable> <literal>starts with</literal> <replaceable>string</replaceable>
- <returnvalue>boolean</returnvalue>
- </para>
- <para>
- Tests whether the second operand is an initial substring of the first
- operand.
- </para>
- <para>
- <literal>jsonb_path_query('["John Smith", "Mary Stone", "Bob Johnson"]', '$[*] ? (@ starts with "John")')</literal>
- <returnvalue>"John Smith"</returnvalue>
- </para></entry>
- </row>
-
<row>
<entry role="func_table_entry"><para role="func_signature">
<literal>exists</literal> <literal>(</literal> <replaceable>path_expression</replaceable> <literal>)</literal>
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index df10bfb906..87980b0803 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -26,6 +26,7 @@
#include "lib/hyperloglog.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
+#include "nodes/execnodes.h"
#include "parser/scansup.h"
#include "port/pg_bswap.h"
#include "regex/regex.h"
@@ -35,6 +36,7 @@
#include "utils/memutils.h"
#include "utils/pg_locale.h"
#include "utils/sortsupport.h"
+#include "utils/tuplestore.h"
#include "utils/varlena.h"
@@ -92,6 +94,16 @@ typedef struct
pg_locale_t locale;
} VarStringSortSupport;
+/*
+ * Holds target metadata used for split string to array or to table.
+ */
+typedef struct
+{
+ ArrayBuildState *astate;
+ Tuplestorestate *tupstore;
+ TupleDesc tupdesc;
+} SplitStringTargetData;
+
/*
* This should be large enough that most strings will fit, but small enough
* that we feel comfortable putting it on the stack
@@ -139,7 +151,7 @@ static bytea *bytea_substring(Datum str,
bool length_not_specified);
static bytea *bytea_overlay(bytea *t1, bytea *t2, int sp, int sl);
static void appendStringInfoText(StringInfo str, const text *t);
-static Datum text_to_array_internal(PG_FUNCTION_ARGS);
+static bool text_to_array_internal(FunctionCallInfo fcinfo, SplitStringTargetData *tstate);
static text *array_to_text_internal(FunctionCallInfo fcinfo, ArrayType *v,
const char *fldsep, const char *null_string);
static StringInfo makeStringAggState(FunctionCallInfo fcinfo);
@@ -4679,7 +4691,19 @@ text_isequal(text *txt1, text *txt2, Oid collid)
Datum
text_to_array(PG_FUNCTION_ARGS)
{
- return text_to_array_internal(fcinfo);
+ SplitStringTargetData tstate;
+
+ /* reset tstate */
+ memset(&tstate, 0, sizeof(tstate));
+
+ if (!text_to_array_internal(fcinfo, &tstate))
+ PG_RETURN_NULL();
+
+ if (!tstate.astate)
+ PG_RETURN_ARRAYTYPE_P(construct_empty_array(TEXTOID));
+
+ PG_RETURN_ARRAYTYPE_P(makeArrayResult(tstate.astate,
+ CurrentMemoryContext));
}
/*
@@ -4693,16 +4717,98 @@ text_to_array(PG_FUNCTION_ARGS)
Datum
text_to_array_null(PG_FUNCTION_ARGS)
{
- return text_to_array_internal(fcinfo);
+ return text_to_array(fcinfo);
+}
+
+/*
+ * text_to_table
+ * Parse input string and returns substrings as a table.
+ */
+Datum
+text_to_table(PG_FUNCTION_ARGS)
+{
+ ReturnSetInfo *rsi = (ReturnSetInfo *) fcinfo->resultinfo;
+ SplitStringTargetData tstate;
+ MemoryContext old_cxt;
+
+ /* check to see if caller supports us returning a tuplestore */
+ if (rsi == NULL || !IsA(rsi, ReturnSetInfo))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("set-valued function called in context that cannot accept a set")));
+
+ if (!(rsi->allowedModes & SFRM_Materialize))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("materialize mode required, but it is not "
+ "allowed in this context")));
+
+ old_cxt = MemoryContextSwitchTo(rsi->econtext->ecxt_per_query_memory);
+
+ tstate.astate = NULL;
+ tstate.tupdesc = CreateTupleDescCopy(rsi->expectedDesc);
+ tstate.tupstore = tuplestore_begin_heap(rsi->allowedModes & SFRM_Materialize_Random,
+ false, work_mem);
+
+ MemoryContextSwitchTo(old_cxt);
+
+ (void) text_to_array_internal(fcinfo, &tstate);
+
+ tuplestore_donestoring(tstate.tupstore);
+
+ rsi->returnMode = SFRM_Materialize;
+ rsi->setResult = tstate.tupstore;
+ rsi->setDesc = tstate.tupdesc;
+
+ return (Datum) 0;
+}
+
+Datum
+text_to_table_null(PG_FUNCTION_ARGS)
+{
+ return text_to_table(fcinfo);
+}
+
+/*
+ * Add text to result set (table or array). When a result set is expected,
+ * then we fill a tuplestore, else we prepare an array.
+ */
+static void
+accum_result(SplitStringTargetData *tstate,
+ text *result_text,
+ bool is_null)
+{
+ if (tstate->tupdesc)
+ {
+ HeapTuple tuple;
+ Datum values[1];
+ bool nulls[1];
+
+ values[0] = PointerGetDatum(result_text);
+ nulls[0] = is_null;
+
+ tuple = heap_form_tuple(tstate->tupdesc, values, nulls);
+ tuplestore_puttuple(tstate->tupstore, tuple);
+ }
+ else
+ {
+ tstate->astate = accumArrayResult(tstate->astate,
+ PointerGetDatum(result_text),
+ is_null,
+ TEXTOID,
+ CurrentMemoryContext);
+ }
}
/*
* common code for text_to_array and text_to_array_null functions
*
* These are not strict so we have to test for null inputs explicitly.
+ * Returns false, when result is null, else returns true.
+ *
*/
-static Datum
-text_to_array_internal(PG_FUNCTION_ARGS)
+static bool
+text_to_array_internal(FunctionCallInfo fcinfo, SplitStringTargetData *tstate)
{
text *inputstring;
text *fldsep;
@@ -4712,11 +4818,10 @@ text_to_array_internal(PG_FUNCTION_ARGS)
char *start_ptr;
text *result_text;
bool is_null;
- ArrayBuildState *astate = NULL;
/* when input string is NULL, then result is NULL too */
if (PG_ARGISNULL(0))
- PG_RETURN_NULL();
+ return false;
inputstring = PG_GETARG_TEXT_PP(0);
@@ -4745,7 +4850,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
/* return empty array for empty input string */
if (inputstring_len < 1)
- PG_RETURN_ARRAYTYPE_P(construct_empty_array(TEXTOID));
+ return true;
/*
* empty field separator: return the input string as a one-element
@@ -4753,22 +4858,11 @@ text_to_array_internal(PG_FUNCTION_ARGS)
*/
if (fldsep_len < 1)
{
- Datum elems[1];
- bool nulls[1];
- int dims[1];
- int lbs[1];
-
/* single element can be a NULL too */
is_null = null_string ? text_isequal(inputstring, null_string, PG_GET_COLLATION()) : false;
- elems[0] = PointerGetDatum(inputstring);
- nulls[0] = is_null;
- dims[0] = 1;
- lbs[0] = 1;
- /* XXX: this hardcodes assumptions about the text type */
- PG_RETURN_ARRAYTYPE_P(construct_md_array(elems, nulls,
- 1, dims, lbs,
- TEXTOID, -1, false, TYPALIGN_INT));
+ accum_result(tstate, inputstring, is_null);
+ return true;
}
text_position_setup(inputstring, fldsep, PG_GET_COLLATION(), &state);
@@ -4802,12 +4896,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
is_null = null_string ? text_isequal(result_text, null_string, PG_GET_COLLATION()) : false;
/* stash away this field */
- astate = accumArrayResult(astate,
- PointerGetDatum(result_text),
- is_null,
- TEXTOID,
- CurrentMemoryContext);
-
+ accum_result(tstate, result_text, is_null);
pfree(result_text);
if (!found)
@@ -4844,12 +4933,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
is_null = null_string ? text_isequal(result_text, null_string, PG_GET_COLLATION()) : false;
/* stash away this field */
- astate = accumArrayResult(astate,
- PointerGetDatum(result_text),
- is_null,
- TEXTOID,
- CurrentMemoryContext);
-
+ accum_result(tstate, result_text, is_null);
pfree(result_text);
start_ptr += chunk_len;
@@ -4857,8 +4941,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
}
}
- PG_RETURN_ARRAYTYPE_P(makeArrayResult(astate,
- CurrentMemoryContext));
+ return true;
}
/*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 38295aca48..baa2bfa908 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3561,6 +3561,14 @@
{ oid => '2768', descr => 'split string by pattern',
proname => 'regexp_split_to_array', prorettype => '_text',
proargtypes => 'text text text', prosrc => 'regexp_split_to_array' },
+{ oid => '2228', descr => 'split delimited text',
+ proname => 'string_to_table', prorows => '1000', proretset => 't',
+ prorettype => 'text', proargtypes => 'text text',
+ prosrc => 'text_to_table' },
+{ oid => '2282', descr => 'split delimited text with null string',
+ proname => 'string_to_table', prorows => '1000', proretset => 't',
+ prorettype => 'text', proargtypes => 'text text text',
+ prosrc => 'text_to_table_null' },
{ oid => '2089', descr => 'convert int4 number to hex',
proname => 'to_hex', prorettype => 'text', proargtypes => 'int4',
prosrc => 'to_hex32' },
diff --git a/src/test/regress/expected/arrays.out b/src/test/regress/expected/arrays.out
index c730563f03..4496104235 100644
--- a/src/test/regress/expected/arrays.out
+++ b/src/test/regress/expected/arrays.out
@@ -1755,6 +1755,142 @@ select string_to_array('1,2,3,4,*,6', ',', '*');
{1,2,3,4,NULL,6}
(1 row)
+select string_to_table('1|2|3', '|');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+(3 rows)
+
+select string_to_table('1|2|3|', '|');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+
+(4 rows)
+
+select string_to_table('1||2|3||', '||');
+ string_to_table
+-----------------
+ 1
+ 2|3
+
+(3 rows)
+
+select string_to_table('1|2|3', '');
+ string_to_table
+-----------------
+ 1|2|3
+(1 row)
+
+select string_to_table('', '|');
+ string_to_table
+-----------------
+(0 rows)
+
+select string_to_table('1|2|3', NULL);
+ string_to_table
+-----------------
+(0 rows)
+
+select string_to_table(NULL, '|') IS NULL;
+ ?column?
+----------
+(0 rows)
+
+select string_to_table('abc', '');
+ string_to_table
+-----------------
+ abc
+(1 row)
+
+select string_to_table('abc', '', 'abc');
+ string_to_table
+-----------------
+
+(1 row)
+
+select string_to_table('abc', ',');
+ string_to_table
+-----------------
+ abc
+(1 row)
+
+select string_to_table('abc', ',', 'abc');
+ string_to_table
+-----------------
+
+(1 row)
+
+select string_to_table('1,2,3,4,,6', ',');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+ 4
+
+ 6
+(6 rows)
+
+select coalesce(v, 'NULL') FROM string_to_table('1,2,3,4,,6', ',') g(v);
+ coalesce
+----------
+ 1
+ 2
+ 3
+ 4
+
+ 6
+(6 rows)
+
+select string_to_table('1,2,3,4,,6', ',', '');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+ 4
+
+ 6
+(6 rows)
+
+select coalesce(v, '***') FROM string_to_table('1,2,3,4,,6', ',', '') g(v);
+ coalesce
+----------
+ 1
+ 2
+ 3
+ 4
+ ***
+ 6
+(6 rows)
+
+select string_to_table('1,2,3,4,*,6', ',', '*');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+ 4
+
+ 6
+(6 rows)
+
+select coalesce(v, '***') FROM string_to_table('1,2,3,4,*,6', ',', '*') g(v);
+ coalesce
+----------
+ 1
+ 2
+ 3
+ 4
+ ***
+ 6
+(6 rows)
+
select array_to_string(NULL::int4[], ',') IS NULL;
?column?
----------
diff --git a/src/test/regress/sql/arrays.sql b/src/test/regress/sql/arrays.sql
index 25dd4e2c6d..d4d5982b80 100644
--- a/src/test/regress/sql/arrays.sql
+++ b/src/test/regress/sql/arrays.sql
@@ -544,6 +544,24 @@ select string_to_array('1,2,3,4,,6', ',');
select string_to_array('1,2,3,4,,6', ',', '');
select string_to_array('1,2,3,4,*,6', ',', '*');
+select string_to_table('1|2|3', '|');
+select string_to_table('1|2|3|', '|');
+select string_to_table('1||2|3||', '||');
+select string_to_table('1|2|3', '');
+select string_to_table('', '|');
+select string_to_table('1|2|3', NULL);
+select string_to_table(NULL, '|') IS NULL;
+select string_to_table('abc', '');
+select string_to_table('abc', '', 'abc');
+select string_to_table('abc', ',');
+select string_to_table('abc', ',', 'abc');
+select string_to_table('1,2,3,4,,6', ',');
+select coalesce(v, 'NULL') FROM string_to_table('1,2,3,4,,6', ',') g(v);
+select string_to_table('1,2,3,4,,6', ',', '');
+select coalesce(v, '***') FROM string_to_table('1,2,3,4,,6', ',', '') g(v);
+select string_to_table('1,2,3,4,*,6', ',', '*');
+select coalesce(v, '***') FROM string_to_table('1,2,3,4,*,6', ',', '*') g(v);
+
select array_to_string(NULL::int4[], ',') IS NULL;
select array_to_string('{}'::int4[], ',');
select array_to_string(array[1,2,3,4,NULL,6], ',');
ne 5. 7. 2020 v 13:30 odesílatel Pavel Stehule <pavel.stehule@gmail.com>
napsal:
pá 5. 6. 2020 v 13:55 odesílatel Pavel Stehule <pavel.stehule@gmail.com>
napsal:Hi
čt 4. 6. 2020 v 11:49 odesílatel movead.li@highgo.ca <movead.li@highgo.ca>
napsal:+{ oid => '2228', descr => 'split delimited text', + proname => 'string_to_table', prorows => '1000', proretset => 't', + prorettype => 'text', proargtypes => 'text text', + prosrc => 'text_to_table' }, +{ oid => '2282', descr => 'split delimited text with null string', + proname => 'string_to_table', prorows => '1000', proretset => 't', + prorettype => 'text', proargtypes => 'text text text', + prosrc => 'text_to_table_null' },I go through the patch, and everything looks good to me. But I do not
know
why it needs a 'text_to_table_null()', it's ok to put a 'text_to_table'
there, I think.It is a convention in Postgres - every SQL unique signature has its own
unique internal C function.I am sending a refreshed patch.
rebase
two fresh fix
a) remove garbage from patch that breaks doc
b) these functions should not be strict - be consistent with
string_to_array functions
Regards
Pavel
Show quoted text
Regards
Pavel
Regards
Pavel
------------------------------
Regards,
Highgo Software (Canada/China/Pakistan)
URL : www.highgo.ca
EMAIL: mailto:movead(dot)li(at)highgo(dot)ca
Attachments:
string_to_table-20200706-2.patchtext/x-patch; charset=US-ASCII; name=string_to_table-20200706-2.patchDownload
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index f065856535..b686037524 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -3458,6 +3458,27 @@ repeat('Pg', 4) <returnvalue>PgPgPgPg</returnvalue>
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>string_to_table</primary>
+ </indexterm>
+ <function>string_to_table</function> ( <parameter>string</parameter> <type>text</type>, <parameter>delimiter</parameter> <type>text</type> <optional>, <parameter>nullstr</parameter> <type>text</type> </optional> )
+ <returnvalue>set of text</returnvalue>
+ </para>
+ <para>
+ splits string into table using supplied delimiter and
+ optional null string.
+ </para>
+ <para>
+ <literal>string_to_table('xx~^~yy~^~zz', '~^~', 'yy')</literal>
+ <returnvalue></returnvalue>
+<programlisting>xx
+yy,
+zz</programlisting>
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index df10bfb906..87980b0803 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -26,6 +26,7 @@
#include "lib/hyperloglog.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
+#include "nodes/execnodes.h"
#include "parser/scansup.h"
#include "port/pg_bswap.h"
#include "regex/regex.h"
@@ -35,6 +36,7 @@
#include "utils/memutils.h"
#include "utils/pg_locale.h"
#include "utils/sortsupport.h"
+#include "utils/tuplestore.h"
#include "utils/varlena.h"
@@ -92,6 +94,16 @@ typedef struct
pg_locale_t locale;
} VarStringSortSupport;
+/*
+ * Holds target metadata used for split string to array or to table.
+ */
+typedef struct
+{
+ ArrayBuildState *astate;
+ Tuplestorestate *tupstore;
+ TupleDesc tupdesc;
+} SplitStringTargetData;
+
/*
* This should be large enough that most strings will fit, but small enough
* that we feel comfortable putting it on the stack
@@ -139,7 +151,7 @@ static bytea *bytea_substring(Datum str,
bool length_not_specified);
static bytea *bytea_overlay(bytea *t1, bytea *t2, int sp, int sl);
static void appendStringInfoText(StringInfo str, const text *t);
-static Datum text_to_array_internal(PG_FUNCTION_ARGS);
+static bool text_to_array_internal(FunctionCallInfo fcinfo, SplitStringTargetData *tstate);
static text *array_to_text_internal(FunctionCallInfo fcinfo, ArrayType *v,
const char *fldsep, const char *null_string);
static StringInfo makeStringAggState(FunctionCallInfo fcinfo);
@@ -4679,7 +4691,19 @@ text_isequal(text *txt1, text *txt2, Oid collid)
Datum
text_to_array(PG_FUNCTION_ARGS)
{
- return text_to_array_internal(fcinfo);
+ SplitStringTargetData tstate;
+
+ /* reset tstate */
+ memset(&tstate, 0, sizeof(tstate));
+
+ if (!text_to_array_internal(fcinfo, &tstate))
+ PG_RETURN_NULL();
+
+ if (!tstate.astate)
+ PG_RETURN_ARRAYTYPE_P(construct_empty_array(TEXTOID));
+
+ PG_RETURN_ARRAYTYPE_P(makeArrayResult(tstate.astate,
+ CurrentMemoryContext));
}
/*
@@ -4693,16 +4717,98 @@ text_to_array(PG_FUNCTION_ARGS)
Datum
text_to_array_null(PG_FUNCTION_ARGS)
{
- return text_to_array_internal(fcinfo);
+ return text_to_array(fcinfo);
+}
+
+/*
+ * text_to_table
+ * Parse input string and returns substrings as a table.
+ */
+Datum
+text_to_table(PG_FUNCTION_ARGS)
+{
+ ReturnSetInfo *rsi = (ReturnSetInfo *) fcinfo->resultinfo;
+ SplitStringTargetData tstate;
+ MemoryContext old_cxt;
+
+ /* check to see if caller supports us returning a tuplestore */
+ if (rsi == NULL || !IsA(rsi, ReturnSetInfo))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("set-valued function called in context that cannot accept a set")));
+
+ if (!(rsi->allowedModes & SFRM_Materialize))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("materialize mode required, but it is not "
+ "allowed in this context")));
+
+ old_cxt = MemoryContextSwitchTo(rsi->econtext->ecxt_per_query_memory);
+
+ tstate.astate = NULL;
+ tstate.tupdesc = CreateTupleDescCopy(rsi->expectedDesc);
+ tstate.tupstore = tuplestore_begin_heap(rsi->allowedModes & SFRM_Materialize_Random,
+ false, work_mem);
+
+ MemoryContextSwitchTo(old_cxt);
+
+ (void) text_to_array_internal(fcinfo, &tstate);
+
+ tuplestore_donestoring(tstate.tupstore);
+
+ rsi->returnMode = SFRM_Materialize;
+ rsi->setResult = tstate.tupstore;
+ rsi->setDesc = tstate.tupdesc;
+
+ return (Datum) 0;
+}
+
+Datum
+text_to_table_null(PG_FUNCTION_ARGS)
+{
+ return text_to_table(fcinfo);
+}
+
+/*
+ * Add text to result set (table or array). When a result set is expected,
+ * then we fill a tuplestore, else we prepare an array.
+ */
+static void
+accum_result(SplitStringTargetData *tstate,
+ text *result_text,
+ bool is_null)
+{
+ if (tstate->tupdesc)
+ {
+ HeapTuple tuple;
+ Datum values[1];
+ bool nulls[1];
+
+ values[0] = PointerGetDatum(result_text);
+ nulls[0] = is_null;
+
+ tuple = heap_form_tuple(tstate->tupdesc, values, nulls);
+ tuplestore_puttuple(tstate->tupstore, tuple);
+ }
+ else
+ {
+ tstate->astate = accumArrayResult(tstate->astate,
+ PointerGetDatum(result_text),
+ is_null,
+ TEXTOID,
+ CurrentMemoryContext);
+ }
}
/*
* common code for text_to_array and text_to_array_null functions
*
* These are not strict so we have to test for null inputs explicitly.
+ * Returns false, when result is null, else returns true.
+ *
*/
-static Datum
-text_to_array_internal(PG_FUNCTION_ARGS)
+static bool
+text_to_array_internal(FunctionCallInfo fcinfo, SplitStringTargetData *tstate)
{
text *inputstring;
text *fldsep;
@@ -4712,11 +4818,10 @@ text_to_array_internal(PG_FUNCTION_ARGS)
char *start_ptr;
text *result_text;
bool is_null;
- ArrayBuildState *astate = NULL;
/* when input string is NULL, then result is NULL too */
if (PG_ARGISNULL(0))
- PG_RETURN_NULL();
+ return false;
inputstring = PG_GETARG_TEXT_PP(0);
@@ -4745,7 +4850,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
/* return empty array for empty input string */
if (inputstring_len < 1)
- PG_RETURN_ARRAYTYPE_P(construct_empty_array(TEXTOID));
+ return true;
/*
* empty field separator: return the input string as a one-element
@@ -4753,22 +4858,11 @@ text_to_array_internal(PG_FUNCTION_ARGS)
*/
if (fldsep_len < 1)
{
- Datum elems[1];
- bool nulls[1];
- int dims[1];
- int lbs[1];
-
/* single element can be a NULL too */
is_null = null_string ? text_isequal(inputstring, null_string, PG_GET_COLLATION()) : false;
- elems[0] = PointerGetDatum(inputstring);
- nulls[0] = is_null;
- dims[0] = 1;
- lbs[0] = 1;
- /* XXX: this hardcodes assumptions about the text type */
- PG_RETURN_ARRAYTYPE_P(construct_md_array(elems, nulls,
- 1, dims, lbs,
- TEXTOID, -1, false, TYPALIGN_INT));
+ accum_result(tstate, inputstring, is_null);
+ return true;
}
text_position_setup(inputstring, fldsep, PG_GET_COLLATION(), &state);
@@ -4802,12 +4896,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
is_null = null_string ? text_isequal(result_text, null_string, PG_GET_COLLATION()) : false;
/* stash away this field */
- astate = accumArrayResult(astate,
- PointerGetDatum(result_text),
- is_null,
- TEXTOID,
- CurrentMemoryContext);
-
+ accum_result(tstate, result_text, is_null);
pfree(result_text);
if (!found)
@@ -4844,12 +4933,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
is_null = null_string ? text_isequal(result_text, null_string, PG_GET_COLLATION()) : false;
/* stash away this field */
- astate = accumArrayResult(astate,
- PointerGetDatum(result_text),
- is_null,
- TEXTOID,
- CurrentMemoryContext);
-
+ accum_result(tstate, result_text, is_null);
pfree(result_text);
start_ptr += chunk_len;
@@ -4857,8 +4941,7 @@ text_to_array_internal(PG_FUNCTION_ARGS)
}
}
- PG_RETURN_ARRAYTYPE_P(makeArrayResult(astate,
- CurrentMemoryContext));
+ return true;
}
/*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 38295aca48..2e8d1d6330 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3561,6 +3561,14 @@
{ oid => '2768', descr => 'split string by pattern',
proname => 'regexp_split_to_array', prorettype => '_text',
proargtypes => 'text text text', prosrc => 'regexp_split_to_array' },
+{ oid => '2228', descr => 'split delimited text',
+ proname => 'string_to_table', proisstrict => 'f', prorows => '1000',
+ proretset => 't', prorettype => 'text', proargtypes => 'text text',
+ prosrc => 'text_to_table' },
+{ oid => '2282', descr => 'split delimited text with null string',
+ proname => 'string_to_table', proisstrict => 'f', prorows => '1000',
+ proretset => 't', prorettype => 'text', proargtypes => 'text text text',
+ prosrc => 'text_to_table_null' },
{ oid => '2089', descr => 'convert int4 number to hex',
proname => 'to_hex', prorettype => 'text', proargtypes => 'int4',
prosrc => 'to_hex32' },
diff --git a/src/test/regress/expected/arrays.out b/src/test/regress/expected/arrays.out
index c730563f03..f72f68e8f2 100644
--- a/src/test/regress/expected/arrays.out
+++ b/src/test/regress/expected/arrays.out
@@ -1755,6 +1755,147 @@ select string_to_array('1,2,3,4,*,6', ',', '*');
{1,2,3,4,NULL,6}
(1 row)
+select string_to_table('1|2|3', '|');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+(3 rows)
+
+select string_to_table('1|2|3|', '|');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+
+(4 rows)
+
+select string_to_table('1||2|3||', '||');
+ string_to_table
+-----------------
+ 1
+ 2|3
+
+(3 rows)
+
+select string_to_table('1|2|3', '');
+ string_to_table
+-----------------
+ 1|2|3
+(1 row)
+
+select string_to_table('', '|');
+ string_to_table
+-----------------
+(0 rows)
+
+select string_to_table('1|2|3', NULL);
+ string_to_table
+-----------------
+ 1
+ |
+ 2
+ |
+ 3
+(5 rows)
+
+select string_to_table(NULL, '|') IS NULL;
+ ?column?
+----------
+(0 rows)
+
+select string_to_table('abc', '');
+ string_to_table
+-----------------
+ abc
+(1 row)
+
+select string_to_table('abc', '', 'abc');
+ string_to_table
+-----------------
+
+(1 row)
+
+select string_to_table('abc', ',');
+ string_to_table
+-----------------
+ abc
+(1 row)
+
+select string_to_table('abc', ',', 'abc');
+ string_to_table
+-----------------
+
+(1 row)
+
+select string_to_table('1,2,3,4,,6', ',');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+ 4
+
+ 6
+(6 rows)
+
+select coalesce(v, 'NULL') FROM string_to_table('1,2,3,4,,6', ',') g(v);
+ coalesce
+----------
+ 1
+ 2
+ 3
+ 4
+
+ 6
+(6 rows)
+
+select string_to_table('1,2,3,4,,6', ',', '');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+ 4
+
+ 6
+(6 rows)
+
+select coalesce(v, '***') FROM string_to_table('1,2,3,4,,6', ',', '') g(v);
+ coalesce
+----------
+ 1
+ 2
+ 3
+ 4
+ ***
+ 6
+(6 rows)
+
+select string_to_table('1,2,3,4,*,6', ',', '*');
+ string_to_table
+-----------------
+ 1
+ 2
+ 3
+ 4
+
+ 6
+(6 rows)
+
+select coalesce(v, '***') FROM string_to_table('1,2,3,4,*,6', ',', '*') g(v);
+ coalesce
+----------
+ 1
+ 2
+ 3
+ 4
+ ***
+ 6
+(6 rows)
+
select array_to_string(NULL::int4[], ',') IS NULL;
?column?
----------
diff --git a/src/test/regress/sql/arrays.sql b/src/test/regress/sql/arrays.sql
index 25dd4e2c6d..d4d5982b80 100644
--- a/src/test/regress/sql/arrays.sql
+++ b/src/test/regress/sql/arrays.sql
@@ -544,6 +544,24 @@ select string_to_array('1,2,3,4,,6', ',');
select string_to_array('1,2,3,4,,6', ',', '');
select string_to_array('1,2,3,4,*,6', ',', '*');
+select string_to_table('1|2|3', '|');
+select string_to_table('1|2|3|', '|');
+select string_to_table('1||2|3||', '||');
+select string_to_table('1|2|3', '');
+select string_to_table('', '|');
+select string_to_table('1|2|3', NULL);
+select string_to_table(NULL, '|') IS NULL;
+select string_to_table('abc', '');
+select string_to_table('abc', '', 'abc');
+select string_to_table('abc', ',');
+select string_to_table('abc', ',', 'abc');
+select string_to_table('1,2,3,4,,6', ',');
+select coalesce(v, 'NULL') FROM string_to_table('1,2,3,4,,6', ',') g(v);
+select string_to_table('1,2,3,4,,6', ',', '');
+select coalesce(v, '***') FROM string_to_table('1,2,3,4,,6', ',', '') g(v);
+select string_to_table('1,2,3,4,*,6', ',', '*');
+select coalesce(v, '***') FROM string_to_table('1,2,3,4,*,6', ',', '*') g(v);
+
select array_to_string(NULL::int4[], ',') IS NULL;
select array_to_string('{}'::int4[], ',');
select array_to_string(array[1,2,3,4,NULL,6], ',');
Hi.
I have been looking at the patch: string_to_table-20200706-2.patch
Below are some review comments for your consideration.
====
COMMENT func.sgml (style)
+ <para>
+ splits string into table using supplied delimiter and
+ optional null string.
+ </para>
The format style of the short description is inconsistent with the
other functions.
e.g. Should start with Capital letter.
e.g. Should tag the parameter names properly
Something like:
<para>
Splits <parameter>string</parameter> into a table
using supplied <parameter>delimiter</parameter>
and optional null string <parameter>nullstr</parameter>.
</para>
====
COMMENT func.sgml (what does nullstr do)
The description does not sufficiently describe the purpose/behaviour
of the nullstr.
e.g. Firstly I thought that it meant if 2 consecutive delimiters were
encountered it would substitute this string as the row value. But it
is doing the opposite of what I guessed - if the extracted row value
is the same as nullstr then a NULL row is inserted instead.
====
COMMENT func.sgml (wrong sample output)
+<programlisting>xx
+yy,
+zz</programlisting>
This output is incorrect for the sample given. There is no "yy," in
the output because there is a 'yy' nullstr substitution.
Should be:
---
xx
NULL
zz
---
====
COMMENT func.sgml (related to regexp_split_to_table)
Because this new function is similar to the existing
regexp_split_to_table, perhaps they should cross-reference each other
so a reader of this documentation is made aware of the alternative
function?
====
COMMENT (test cases)
It is impossible to tell difference in the output between empty
strings and nulls currently, so maybe you can change all the tests to
have a form like below so they can be validated properly:
# select v, v IS NULL as "is null" from
string_to_table('a,b,*,c,d,',',','*') g(v);
v | is null
---+---------
a | f
b | f
| t
c | f
d | f
| f
(6 rows)
or maybe like this is even easier:
# select quote_nullable(string_to_table('a|*||c|d|','|','*'));
quote_nullable
----------------
'a'
NULL
''
'c'
'd'
''
(6 rows)
Something similar was already proposed before [1]/messages/by-id/CAFj8pRDSzDYmaS06dfMXBfbr8x+3xjDJxA5kbL3h8+eOGoRUcA@mail.gmail.com but that never got
put into the test code.
[1]: /messages/by-id/CAFj8pRDSzDYmaS06dfMXBfbr8x+3xjDJxA5kbL3h8+eOGoRUcA@mail.gmail.com
====
COMMENT (test cases)
There are multiple combinations of the parameters to this function and
MANY different results depending on different values they can take, so
the existing tests only cover a small sample.
I have attached a lot more test scenarios that you may want to include
for better test coverage. Everything seemed to work as expected.
PSA test-cases.pdf
====
COMMENT (accum_result)
+ Datum values[1];
+ bool nulls[1];
+
+ values[0] = PointerGetDatum(result_text);
+ nulls[0] = is_null;
Why not use variables instead of arrays with only 1 element?
====
COMMENT (text_to_array_internal)
+ if (!tstate.astate)
+ PG_RETURN_ARRAYTYPE_P(construct_empty_array(TEXTOID));
Maybe the condition is more readable when expressed as:
if (tstate.astate == NULL)
====
Kind Regards,
Peter Smith.
Fujitsu Australia
Attachments:
test-cases.pdfapplication/pdf; name=test-cases.pdfDownload
%PDF-1.5
%����
1 0 obj
<</Type/Catalog/Pages 2 0 R/Lang(en-AU) /StructTreeRoot 14 0 R/MarkInfo<</Marked true>>>>
endobj
2 0 obj
<</Type/Pages/Count 2/Kids[ 4 0 R 12 0 R] >>
endobj
3 0 obj
<</Author() /CreationDate(D:20200814141638+10'00') /ModDate(D:20200814141638+10'00') /Producer(�� M i c r o s o f t � E x c e l � 2 0 1 6) /Creator(�� M i c r o s o f t � E x c e l � 2 0 1 6) >>
endobj
4 0 obj
<</Type/Page/Parent 2 0 R/Resources<</ExtGState<</GS6 6 0 R/GS9 9 0 R>>/Font<</F1 7 0 R/F2 10 0 R>>/ProcSet[/PDF/Text/ImageB/ImageC/ImageI] >>/MediaBox[ 0 0 595.2 841.8] /Contents 5 0 R/Group<</Type/Group/S/Transparency/CS/DeviceRGB>>/Tabs/S/StructParents 0>>
endobj
5 0 obj
<</Filter/FlateDecode/Length 7837>>
stream
x���moGr���w�w�b{�i��>�9$�
'����`��l@"}��!�wO���LS�����Z�%��]����W���U�O�9���|���|~h�����>�����O�V�=X�U��7�s�z����WO�������O>��������O�{}x^��]�U������V7����O����'������4J���zoh��k���=4j�]���^��Q�;tm�zw��[w���A��^�[��/h���W8v���=|7{�Q:/����e�����G��6/���'���h�lVG�*oK�[��:��SM�]mM����./�����F��P�*��e�h���[4���X�����S.k������{S8wE7(����!/�m���W4 ����6/40�u����Q�g����fo��#sW���8��S}��{�U�W�x�f��aP>������R�w�d�:�u�= �����Yk]���[xm����JM������!�4 ��+
���J�Y�6 ���n�mV�
�6Y!������h�Y�� �.��:e��0��/���pH�N��/�~}��������c�B�O��b����)���z�D���3R
mX���a���e,���k��]�??}���oo������6?��������}x��O�|�|\�ldX
��B?�z�@��������_\m��k��v=��
�_�?�� Z��w�� ���d�����_�/��a ��o���@�������?�9F��=�������Vt^ua�5j��;����� �h��gs���2~������+���,C=� ���6�F7�������c�������!����Q�o�7�b����KB�n���vP�E}A]{������s��������/�^����?���c�n���5|���p��_G����7�����u���cx����c��������8���AM�#"��a��Ns�S�?�f��������_.���(�D����e���M�-�w���=��w��=��6=��
����z���.���8��P����v�����#f��;�,��z\|����� �&�P��I��C���L��q("Dg���Ws?LP�A��"��q����1z5��~���fk��a+�ZV���V2�<����q�Q�61BQ4�%��>d1Bqu&~d1B!6�%�u�yb�7B�u�
*�{Bo�"� ����u�� ��y+��E��"�;������"��cP�
E��"�����!b��i�H�6����h���
����u��g�c��eH����!Ag~�7����T�����U\wkc����ZR�P@�H�x�\+������+�S���
�3W��� ����Z;�`����S������N���pN�fn��y������}��]���8��l��-���ptXF�F6I��U�!l�� Z��Q�^����Z\���-/��]�� �����[}~�p6I^8W�:���h�d����g`_�
�=^6���l_����i��j{?Q����� �ny#�.�������]jA�����&T
gyW�L" gyq/�)�;�y�to����'��s�<�����h��~��V6A��zS��V6A���/�'�p�T:�[��1��X�py�CS+�AF}���rJ��9��E����%�.je��}���&��>Ipg��W?���q{����T+���
,��pRzU��T'����(����; ��N���YFMFS��d3���f,�:��uc�p����A��:���X'����-�X'��^U2)&����(��z���qn�������w�u����W���dS����PL4-�-|�������wW����o��a�X�
>�u9��R����
��U����<Z��i�
/��j�N\�kk��f����b��-�!��|z/�O�����c/�Oo�Fu������t:��|�F��0\6%�[^,R6#��d�M6!��Px;)���e3���jw�%��A4)��~W��A4'��j�9�n���l��
�*e���J��D������}�;mD�|�O$����jK:��l �D�Z�O��&�So��� {b�a�J��D%�h��������}7���=?�����W�����Q��`O7�g mg����)(��'� -�t#{����r����L.e�6�(~���!�ndO!�8��,���B7��c������AE;�~����w��*�V�T���2��HV�L�R�j�������]��Z��Z�k��=���o-U�m->R�S�R%�"h�t�}�3-Uwn,��r���4�J.^��I������������>��A�s���u��H�v�Ti;�����D�`U����Uo�������"V�_d,��r��C�L&e�
�E��~<k�t�E�TE�H��a{�K��R��f��#�E[�3$�A��GI��$(�a��/'���*�If������`�/��jF>�n;�R��R�g������=s^���_n�!|u
�]@���:��#:_��}���x���Y�x��+@��5�^��G_��A*Uuq�"�Rb��n!�;/E�?�=���r����E�R.#VP,�-M�]
A<[����J�"�)�9���R�f/�6�
��7~�
4r��C3������ :6��qL�����������i��x����7������8>9�c9��`*|�������\[)�#��5��������{���D���=Nj�Y!Us�'a���Z�Re�;(�J���bU���;�H��2��cZf� b�)3��|��]��}������P�`����s�����XB��#����}�� �N����P�?y����O}N��2����\W1J�`,RUxj�-�r�Au{�:�����P�
�����f(��p�.5��}��Q��*d��� 4�E;��m����Nz?�*���l�j���h�
����Bn�]-UC4u��P�B�m����D
R�A� ��6��3�3��>1���<�r�q�b��g�F���k/�R�$���8
X`{���^
�U�T�������KU:�)�
��T�S�e
��2?����]����nZ�&�� �T0-LQ����84�K;�*#��;��~JU���/c���Ru\����zb8�sQ�>�p�
����4�p|��s� e��{�w�}5�d ����K�I,�x�|&3i/~�'���Ot�s *U�vh���~��TDI��� ���w�R�d#QW�;��&����&����j��IU��}��Mm��*S���{���T�]����t���U��.O��U,���8�0�~���T)�������Ty�����=�J��nP��f�z�����e���q���Z�X� �����B�R��Y��&k������1��������<#�s���n������=u�-U�7�uU�^�J���P��� u+�>p��m�����19��[�X������K!K,�x#e��%�_RS��y����1����TI��m���e}E�T}���e*��G/*w�1U3��T�c�8(����ir�<3�}w���0OA�����!L�M� �Nf�C���T�e��3�Qt0:�h�������(�,2%Z��W].S��l�����D��Z������6�Y�v��.�@�% �<��RFS�h,}��I�ckQM����E��� ��*=������DU�W�F�ps�4na)��T��Uf=�
U��{e���R�����F7��3vN��V ��`�c�.#���O�FB�3L����jT��������b6R��gp�A
d
Jv��b�����C\��o����J�mMz\��"���q�}j��M�3��_���8SB�8��B�S%��
#��Y&�]�P�����$���!�T��Hr�� ��
z�T����U�2�W�;��:����f.��Td@?�^���f�<'GV�k��R0gG0��B�:Gd \����3u�5T�m�E��(88���v%�m��G�^�PU�k���^,��z]K�U�t��_{���[�"��Q���d<���+{��
���J�����To����t�������~���A�3��y�H����p��P-y#U�{�>��r������=�O-ul��k6�56U��m�������\� Bq})��sO����l;g�eCU����2�}���3���J+�A��T��H{��2��MW�;��*����h��A*2�U��M���i�^A{������iO�1� ��Og�9�K��9����8CX��=M�<�*���K��ym(��S.S��u��G(�VA:�R�.�Tdb<�6��A�g���_M�<s.sza�J��� ���G�W�Pu�y����
'�/U�=��?P���I$�X|u,A�Y.S�\x��C,�*�{m�e���Dz�����
�����i�Q�V��A� ������%p�+��{T
�
U��gw�0(j�K5R����t���C���r(MU��
-��eR��kC�*s��B�z�M�ofh R����o���shY��-<sh�n@ah���������G�Q�P��yv��G
'�H���������-R;��@u�A]Z.S�|��G���i��i�1x������o���1�h���VA3�
(
3�a8K�f�9��������fP�p��T_�)����s`�2��%�Fy_��~���!��'P��;�$���,�P��$����G�U~+01���>�����`�j��P��v<�s`�3������\T+ �c���A���DO�]��P�P�/7������.���f����1f�#���0��o�OM�o^A��F���#�
7�G�#�D��X���>��G� ��$����������
��<1h��s���0�R����[����n�NLUs�H�\M�Hn���2(������8O8���t�����8`�����q���}4��h����]����q�p�n��U��~�n�����M��U�������0?_ ����c?~������@���:��*��E_?]5�������^Dk^���_�&�;e�=90���A�������R���J�.���*�{�Vb]�Z�2�gPj���B�V�p�3-��t�i�M8\����,a{��G;+0��-�UQ�=xcdLg�>=^���'x�v��g�x1T��J����5��ZO�BWxb�Cu������T� ���<�A)2$���0����&�U0��`�"�J��� �M'�N�:z���'>��T/�Z�j U�:��Er���yD5T�;@�
��/�}��y�����JOI��+C����p��`\��..�*��c \��L���J��� �\Ea��*�����nl,�]�Q�"j���+A������z�R���5��J��[��\�\��Y7�MW�*�T0�M1�2�j���N��J��X�g�����P����P;�y��R�w3C+�I}|`����������lu%��J��x.~������-1��Lc18��e�Tc����t.��PK��� ���}B-����V��g'B�R�����n�-�K������JExk���d����Y�����y6���Q������q�y����K���d~��U*%������U��'�)��:�����P�Y\n ����h]����p�L��h
J�b�u��3�g��h$1���Lf��c�.d,��'026�[��y��K�T����8����$�N�W���~,������S�T�j�.�v@,�
0��P(�z3����%x����]jo
eY
�nN��K)K�S�.�M��)���i���4y�N<["yQJ0>i������?E����Y��@-�O�M�>�Ze������O�y<�~3�|���� ��n*
��Y�a����:��T�����tje0��x�����(J��J���������J�R�*���e*�:%Z�XW].S�|l�YO_�x�kz����oP����L8�Bb��Su���
3�1��9���TK��H�t�4� �Ku8��������9:K�6�E��� �QA��E2U��������
�Wc��;a�ZU4�������/����X�f;��Wck �S0��1w��:{c \ �L����t_iPS)8��j��52�y��vh�6���]���TA�JhoX[m�~%����&���_������O���q�@�kC]���k����Z>��Fn�/�6Y���p��IF����R�%7�b�<��7�!7U�������2�{���Ti�jB�V���=Ld�@h*��j���Ai����2C����)���\Ai��1��:�����%pat2�(F�����*�+l�t>���i�TU4�����W�Wu���_Ks��r��'���z����zn.�"����}�Lg>5
M�a:OA [���L'k��F����8aR��&�������3��%�Re�+�����r-��R��k!���\���e"��d���S-7J�p���
�L�g���W@��`�<��B����Y#���Vpl��C��Y���,;f����@�*�^y��me@�/{����r�������N-o:�<��/��gmo�^���t7� |*�y
f�#�/�;��d \��L���������YvD���`�]���m�!�9�S��w���t��=U����w�e
���2��O���s9�����T��L�s����g*�r����O�2g ��O��A� *Y��e�����T���F�=�,�����@T���`�z�\����*s�`A����,��6(E��YS��� ��S������T0(��7�4pP�W��%p$S�*������Y������ O�
�s��4z���Y�������wc�L�c�Uf}����[���c���@����i��?�������S0����n=K`�������Y��e�;p��b��������G;�� T9��8���\��I�*�>�P����5��h�c���g�{��sY���#LS�n@a!����8�N����,���l�#8K8qD���#��z�q�,1��������cUa�:�,2w�9���5j`�����{��Km�a9!p���j"K���`$zJ#��f \"O2�v��pb��e�c����d����G�>���8 ������>�Yg2�s�/�X��=�����������W���0By�Q�jc��������1a�Y�-�Z�z��yP�L ����y�cmw��B~��e�'��z�"005��{A6wd���
�)W
�{�l�sI����X�����q�z�����AEQ�B�U�gz,�j�k�����/P������vd��^����OL�fH���Y4^�u�x���7%��i���!���1�10�N^�Jo�mQ�5K���a��{<:������]%.wA���d/0*�
��.��rY�~���8��#~��������OM�JVd]V���������jh4�b��v���4�a��s���&���Z5d&gVc�T���{���Q.����f�(����d5�N
Y�0=��W]����|Vcc��i�C`uN�!��4zxB-�9��K�Rm��k�� �����k��b!��c����83�)�^c[�����L-�z�
���kB��(���1��i�X�6w�x���&��q=.DMhv�N�n\b�@i�0Di�H���P�)����4��d����4����[���b<��zJs�O�B1�����zlT���&C���xB�^(=!j��m\1�l�]-���"1� cc(�!�iJ��UC��y��qM\��?V^V�
endstream
endobj
6 0 obj
<</Type/ExtGState/BM/Normal/ca 1>>
endobj
7 0 obj
<</Type/Font/Subtype/TrueType/Name/F1/BaseFont/ABCDEE+Calibri-Bold/Encoding/WinAnsiEncoding/FontDescriptor 8 0 R/FirstChar 32/LastChar 121/Widths 1547 0 R>>
endobj
8 0 obj
<</Type/FontDescriptor/FontName/ABCDEE+Calibri-Bold/Flags 32/ItalicAngle 0/Ascent 750/Descent -250/CapHeight 750/AvgWidth 536/MaxWidth 1781/FontWeight 700/XHeight 250/StemV 53/FontBBox[ -519 -250 1263 750] /FontFile2 1548 0 R>>
endobj
9 0 obj
<</Type/ExtGState/BM/Normal/CA 1>>
endobj
10 0 obj
<</Type/Font/Subtype/TrueType/Name/F2/BaseFont/ABCDEE+Calibri/Encoding/WinAnsiEncoding/FontDescriptor 11 0 R/FirstChar 32/LastChar 125/Widths 1549 0 R>>
endobj
11 0 obj
<</Type/FontDescriptor/FontName/ABCDEE+Calibri/Flags 32/ItalicAngle 0/Ascent 750/Descent -250/CapHeight 750/AvgWidth 521/MaxWidth 1743/FontWeight 400/XHeight 250/StemV 52/FontBBox[ -503 -250 1240 750] /FontFile2 1550 0 R>>
endobj
12 0 obj
<</Type/Page/Parent 2 0 R/Resources<</ExtGState<</GS6 6 0 R/GS9 9 0 R>>/Font<</F1 7 0 R/F2 10 0 R>>/ProcSet[/PDF/Text/ImageB/ImageC/ImageI] >>/MediaBox[ 0 0 595.2 841.8] /Contents 13 0 R/Group<</Type/Group/S/Transparency/CS/DeviceRGB>>/Tabs/S/StructParents 1>>
endobj
13 0 obj
<</Filter/FlateDecode/Length 4716>>
stream
x��]�o���n��������") +
�IWlH�v���P���
��i~l)���#)Q��:��D�C�,������!��^���G��J��I+
�0R���
i������������x�]����>��������������� ��w�G%i-�TU�US�
%eS0U��$���^�U�$���U7�v����,J�V����RoU��1�����,^�i$�~�PB���%�$2\�$� ��\wY�D��������,A[�����u�5�a3KJ�W@�)![A���iI�-��
�!�������4���:l�h�*aC�$<l(/I* ���pF�`K�,5�C%'2h�P�U�B���J+���pk+# ��"m�������%(���������m�Z���*h)oj���r�`�:����P&���J�x��J����_?}T�3}����U��xz}s�Y���Wg�G�LN�,�^��T��ri���Xq�Z���<�t�v�I����������������u��^S��Z��8�����g#V��VX`�����^=���5k��/�r�~-V�u��PU�V��P�>�Uc^)$���j��(P�(���t�>���U���g��E�z��'n�����������]���d��RP��]����o��zQ�B�,W�����Uw�e_����vxw_{Z�����uu������]�?o������]����=���K{V9�f��F���.m7cA�"��E5y��q������Y�y�>�W�0�y�����j:�^�O5n����O+���U����8��gcp7��������������{]��W�1����_�W�_V�}|�`��l~|��v���h^�<P�����?��[�o����R/��>����^�eC�0��y���������j�o�}
Q���zAw���U������'�Z0�f$
VWl���ad�����%��:��m����N�gf>������gZ,��[>os�oe.�E���X�N�B\[{.��Q�����i��Wo��k������P���_\�,9V6r���Yr8�����Jm�\���i��74?X\X{~���D~���Z~�_,A�Y\NnxU�����@���|t�k�@�}I:��2��X\<.x����*.��#�;�U\a{.pA�*T��=�f|�>����w����K�
"x���$�`X0��SI���UKd�X�7�~��K��vX����-�C?��0o�����������z��'���{��2���c��m]@q|����G|����O���?��t(4W��{o������|�o���tz�-!,x��j�gm��
���IK��nWu6�|����K���� ��!�1�3��<�L��]�G���q_���F�[s5*����OY7��[
�N������\
N
���AC���a���sw��S��,��L�[x��&u�>��|�vf����i�>1/$��7���a���~�
b��oZI��
��F��� b �qK����Vh&���rd��&���$�8�h�W���Ce���%�W���juiWx�9�2�I<�����2<��0�����}OS���F33���~��Ld][������'R��?���|�z��u�����F��^��&���C#
��C��tw~������5�����/�)|�������H�!���$�������������{���q�����������!�-��0�X����n
���C�n]�k��e�C��#�YY�8E4(��)�k�
���$5�g���]���p!i�2���v�A�a���pT���qE������,~��.N�
b�FV���I����p"vlX:yAj��jD��0�q���5��5���X��p�f��6�����m����$���H=(�l�f] �p�g��48��adL���>�� ��:��ph�p��\
����P*ei��d�H�# 4)��~}�1u� ��pi�m� p��Hi8����5��X$��p(i���#��bM�D�M;\����%m�����nz��(�@����/A�8��� @������b�s
�1Z}��!��z������R�;2��!
$PZ�( =�~�@�8�p}������������C���{ �s��
�a�^H��0��dq H{q�>}���"k ��*��!� x�����
��j��?X�`����j��������k ��W�qG��
�5i
<�&�wx0���Di���$�� )�VZ���!� �;l����.Gf*���+���e�16pl�S}`G�77f|l�5��\����
2�����Q��z����-�m��Z�����#�<��7"M ��A ��J�1�����y�Y�Q�nY9v��;c�x��2�"d����.Kb2�G(J�UUhQ����L-���K�~��������:��x4�u8O*/��b��K{�sH)���^�\���
����6��:�]pfR��}FB�����>KX];���Da�5��g������3:*�����i�B��8��H�I��������i��O��s`�<�X[�9�I���;���!��8��B ��9���0&����M���^�KG�%�>�I�1��e���d/�{�� ���[
ug�Q\5
�����'5R�8��$ 5�:�L�?$-�����5 e�z��O���M���������b�$������3z^sx������_"�Y6���7�[�O9�������Vv�R��@�mO�������K@��e� ,��j����� ��2�����?�9���<~��ps�n�f�f@�mO�q���# ����ll��o(��j)
b���##��5��>i�d�Z�1B���s�X�H��u� ��C u�� ���@B������g����S��~�������@B��F �}���?�T�3LHR���~�g���" �=}������HD\M�>��?�����RK@�����Z�"��j�8���� (�-�S�������^�e�H�iOK��4Z����$-��NKK���"�����(@�������Y��3� �L
-���NK���+������B�RK?��$d��@���i���=���c���i� ��H=c��3(m�c���R�`@2g������y_naH�h�W��D<��@2��c;�C=�ywS���x4�����o��7�j8<���'��|i9LuO��c]��i���"9�||2'>��-�f@��j�#f��������z�u����HL�o��W��0$���o~"dr ������������t�;b��.���KC�q�%��j��L[R��DM
/} Cr){��mK�#W$��DE�u����rS6��h9�w@�������4lw^��H4�l�����=�!��V��H�u@���e8�� ���rZd$Z�u�����%=v2���,Kd(�R��������0$����A� ���x���X;�K �V;��XT;�n6�o���c����|�������#���H�'CnuH"\t �t�����Xn
�m:�u��1��k M������u�A����1�����}G@�:/Qs���Qs��?�.�)���+�V�x3S��d�N�v��,���!��9��Ch�����4����=�|��'{����-���-�\�U[���'��4'^�Q��h4u�GS�g����5�V�Ze?]����M)}��=��,s����~�2t���s^������]T�=�<R��lp�/��T}AR�{}�A�h �n���ZDA��'��,s�����P����L��
��GiJ�.��{J�����-��x�����]�]��|�~{.��8��ks����i��A4>�:by>�zd�������A��?1��N{m�7�N_,C���v�����g@������F�V}�kC���__�M�O�5X7|�3�A(���-�e8v�K�jn�<R��
���,c���:�G0oY3�N�����O�.���fS�N�^�������{u%��$����o_"|���i�R 7����t���G��?�I�������������7� � )��7�y`L�3�Dq.�tr�{��F�x����R����2E���uK��4� ���4>������nA��C�����+]J1oM�|���
�>�8�"��0�#��7-�����~?>�-�\U#t�&o$��J0����������Z�2���1xj�����.�7dQ�Y��'�GL��z%D����7�i]�����jm��U��v���z�����pM�JB#�J��X�$V�MM�Fu0�G3���]��B�;��4<T@6�f�����[E�$<h�08'�GPt�-��w��|�F�AP�����,k"�6�TSw�
}�p�@��3mC*}tj�@-I�_�#$��
�M�����m��D{w�/��*���=
���A�\�m�B�^�~���j��7��. �Z�d� ��&(��F��V�[E���mbe���� ;���f�LI$��\�c�%���T���/��TRC�eX73�Q�}SG��y����R"c6������mI���T�El�j���yCIYE��9��������z&�#e�T$b6�ew�f5!U��3�{��L����
endstream
endobj
22 0 obj
<</Type/ObjStm/N 500/First 4729/Filter/FlateDecode/Length 5541>>
stream
x��]��59��x�y���Kh#�HV�f� !D!x{�������#v�of\��T�=���>�#������W�O����H�-^�G�S������>
�k}��O����H}������{�� ��=u?���z��W�����{1T�����O)�zZ��G����C ��i�)�x��E��4�T�o���0L�������
�O����;�_����.�t��>�}�}8�w�3������,<���)@4�P�����zf8��
��!�XP�7�� ��������7�,(
��`���3�"{QqP4T9����6�s*��]e���
�P���M�0�E+y�J�va�!�Yo@Zib4�y6�_*���J��`�xa,@��0 �l�B^<S
��a�Zh��h���xF��1��b�}���q82�� V;0�Y^�,c��r<��a��Y���h>���B������]�[p!�'o�'*fQ�foth�w� ?�[h��?���t=�C5F��T��w��0V��m�W�M+n
���}9���G o�E� �h�5� �F�y
���������r������.���!]nR���1�C�v,�C�p�|�����y��2�0�����,��r2�
t��("h��p���(k6�A6u��3;u����1���`�b���^`�I'��
y�knxbAt-zu�^�b.�`F�,�a�%�!T�*u8H(�[���-��������_�d�X�Y ���D]^���Aj(�km���_���_���`��d����-/o!L�!��8)�ap�
��;I��A\h��."����1�^�!���:D$�=x�>����>�5y��B�+T�K�����)�&������J��P7)NxE��7��"�����������w�K:����.q�f��xNpL!��0��1Q]B(T�PM�3�,�\6�v2��[aX����pE��~��@���.��|��M�S�-$��aH@-A�$q3���H@� $0N��O�#t#�`(���!��6���!�L��Ldf�o#@�\1����`L�$��t��q����7��#)���I�������%+�.t�� �N�8�2J���QU��$X�F���������AO�����S't,��e>t�L!��m�<!Qf�i#���1 `��:�
2�AM��*3�!�V����l��r(�q����)3�
�LRi��\!��2��e��,���!��Cy=a���6Z�����NR�C{�Q�W�/i����If\ y�>I�$H�7������E��I��)�O.#�aK����=�1�7m�a2*������$S�>,
�+�|��q>��!�Q!se����|4����\�1�W)��6�G\c�F����e�s��>�9H�j�\�0��[�P9�%�|2�P����W�Qe�O��2�g�b��6��"n�2qo|��
����L���;��8�����\�U����czn��d5,�!��z�h�����q������\Z���~R���o���pd{��q��e11:�S�� ��1���s�]�Yel!����Kv�����%Y�=k��+.��Y��~%xY�1^������� ~��g79K@&�����2�%0���kL��{Rf�9�Q���W�g=�h�����`��D�Y���fg�i���e5w'�Pf.�_�[��:����8�R�$�<Ov����d�#�<On��t2�1m�{��^�A�E1������m�w1���y1�pqVO>c�>Y��u1�1������h#�\an��&����3.�71mt���|��M������N�����P>�I�b/e��N/e&���d@�9U�1��#�����vl����h�W����.�uq<,@qu�"dn����82q'������qE\.�9{\�F�^�["��w.{�G�+�pu�z'�5�2�� �|�4�N�{\���������ip���e����� �����^��olf������M�z����Y���y��r���i'�2�������S"Sf���yrfC��F��a��������e��1����+1&m�u�4|��-��q<��3%�����������G�'�S����������^J�������6b��6�����Q'�u��;�;N�������������^���2s���
Ff�cw�7�X�`d�������.���6�����6b�rU�|�~��H�|���N��cP�����q�
+Ff�c��78���� .����6Zg�]�������k�F��\+�u����<O6o��4(3�|R�m��ef�O(�a����q>Y�����h�u�:V�9h#���s[�+��q��s\-��+�|q�� �!�]2�W�k�����E~���Eo�6���sm�=-�����V�������vV���,wA��8_2w���
���+����[]F�&��\#p=M����\ 1u��&k�]�������Gy��7�~����~��o?��o���?���?��W��_�}������~�������+��+�������S~����^���i���m��a�'X��ZV�k{X����X�K"�� V��~r�c*�<}�z~uq�4'�� ��O`|8��zj
�g���XH�{Z�=���zO�g��zO�����5X���<�����B�(&�e`�{d>�v_Nt�{U�r����%�����
'����53i2k.X�2������>C0��20�f;3�)�6b1�)s$��/g�nt
|U�r�5'���|#���%��sL���\2!��|�KF3�Ub2_6]��0Y��N���f� b�S�:H��_�z�����*��kN$$#1�o�x".����d�l�d�B2��20��V���d����
��������j�RMv��\I<��Y�]_�_�\q��wHF�d����l��vHF�d��%��Q5�/s�h��������6���&;e`����=�����1>��/w��tM|����s ����Op�d\J����$�����1-����������4���~%�^�d��_L��_�IX9��/��_�U�Rs�T=*�=������,��3r����"<�s<��$.#�����OW��f�c���/.57��R<�����`)��/I|�����[���Q����es�O�pgF���rV�92��9����m�L�|��k������_
s\�,��s\
�����5�_�����r}�9���PoN�k�%.`w�#[If�sV\��6G�x>g�u�nsd��sV\��6G�x��z����`^�s\�e���}��������[���Q�����l��-���Yqq{�����������_�.q�{�����a�6��x~������}��������[���Q����5l�������K\�6Gfx~���u�asd��sV\��6G�x��f����`)���f�1�f�����}����jU��m"���is�����^�b��92��9+��O�#S<������92����i3X���uy�/6��x�~����n��V3�zTl!�,�#w����5�����)^PK9k�����������O�R��a�[6��xn��7��e3X�������Vjn5��G��gr��O��w<��������)�_/��^�L���\���^�L���|��0�-��r<7�I �e���}��������[���Q�M��z���.������r����>��������}�9��Og���m�cN���9n����9.>��m�����Y���u+5��A��b�3
9k�Y�o|���%��o�#S<���z��92��s\\//�M��_0���|�����r->��:M���K��~���WK��Tn{J��������f.����&��'��h^^�)3@�j.q���6U���
�7:�6���~�����T�~�q�:����\-�
R��)%�e�e����_9��p��������������.����#�m8��W�����x#v�+#��~"�N���&WK��TnS��|^����7?����������������/���$�m���g�k����x+v�g#����~���&WK��Tn{J1y��!�Z2%���*z��Cr@���2z�;DR@��.q��]"9�o�g<��#�3�N��������������j U��mO)����r�������W�B@�-���k\L/v�H��W�����!��v�7�xv�G�f���Nc�{���~4�N���&WK��TnsJqA��]%��_�_Q�qE���%9�K^5.���$t���5�b7����
��gw����f�*���?O�~���:����\-�
R��w���5.����J\^��=�����)/v#LM W�h]�C�����}����n��}�����T�}�u�b����>%�r�W%��d�ub�-5�ZB�r���'���S�����[���Q�~��_D%�r�!&�QI���W�}&�r�a%��5)��+9��e����[jr��*H�����r���������k��)vWL��WR(��bR@�P^�B�����6L
�v�J�g��Pn����������{���\-�
R��)��e�����p���5>f^����?g^�B����/)���+9�o��Pn7���~�K
�v�J�����������j U��m
�5/v�LMW�n]�������}�J
�v�L�g��Pnw����
��g����~��&�r��%��lpO�_�R��%TA*�=����6��!�~���G���'������F�0���8��},`��-)���,9���I���d�?=w~�X}KM��P����b���eZr���u�</v�L
��<oI��n��]�jI���f�}&�r��%t3^K
�v?K�������[jr��*H�6��O��e�%���_�n���b����>y%?
n7����o�&Us��%lg<��%�3^|��]-)��g���o���� ���RL^v�LKN7�������9����T������xIu��lI��zK����-9����c�voK�:M�{��-5�ZB�r�S���n�i����yR]��gr@������@�����T������ar�np����C�;\r�O�7�������\-�
R��)�? l����Dq���-����C���W�������W���p�}�����p���C��%���ub�-5�ZB�r��%�^n����Dq���-�s��&��+����C��/�������6����������[|]������������j U��mO)&/���%'��_]oqu]���'���.vM
�W�[\]��%�mC��%�3^|]������������j U��mO)��t�l��(n~u�����f�����W�����]��qu]����a��������������p�?��|�X}KM��P��������=4=9Q���z���b����.y���.vM��������.9``�8��.)�������p�-��7�&�@
endstream
endobj
523 0 obj
<</Type/ObjStm/N 500/First 4836/Filter/FlateDecode/Length 5614>>
stream
x��]��e�m��?�?(�E�G��'
�gAA`d�F���Yk_��H�Q�����nikQ"�H�l���|�g��Q�������|Z���?m����������7���}�g����������*������s~v�8<�����#�/����Q>��s�O�3�ab��J�b�1 ,������p�~���s%0�L���E�f��M�0i�TnK���<B�&��s0�Q��P��\;R8�B�s�O��BX��@�\��
j�j�W?��@
� �.��.�1��no�#�w�����Rl���Y����^���W��z��{C�� ��n��SCi�>��T���C3L�<S1X��/��L�p0f��y��K#:��U�RC-�Q�!�6
5T�H���t4j����
�����q�~������R�����0���=����acO��@���
W�B���������\p�Y�b#*�hUz:]������7���>>���N���I��:
g������:sI�:7�,V��Y�Z����u.:7��tP���`B��b����7����5U�p���[�V����yh���>�(X�Y��=8!� �m!���l�kZ����g"ac�ia�]����.D����5D�A
�{R;x��.����t��� On����[
W ,���[�5A�D_�}qR�!l&�"
�QCD�4j�@�N
����!p�������#���o��?��,�<6LEt��'J
���Q��"*�PCD�Vj3@����q��~m�]o��y ����utp��%W.������S\�J��Q��"*U�!�M�����������C��QXK���aH����q��c��B8L �?�LM��L�
��]7���5I��k���u�3H�u�'t
�d-Z83q6�J�}��)�M]0��'u04�Ze: B�|�Cgx$�-�b Qg82�F�����+�c�mD#$�p7�nD�n�����Bg��\/���Pg�F��^���3�t�T��:�-!�J�
�� �|m`�vg����j�X��� ���Hq��<�����oS�N\���� uf�lJ��9{��L����7sg���'�O<��6z�gS>����F��p��z�B;��f�dB�/$��,��'�i�ff�2��S�hr>}:�BM������:*mH�'8�q�X�;uF03��G0������R(Q��B�|�7�c����h���Dm�
oCi�M%!a�:����[Q����Rg��b�:#�!QgwE�������J�6�7�nQZ�Ab.�BR����S4A�z�� uV�*uf�#?b��33���03���O`>�$��$|:#Spf�����*�D���S�c�"����*�|1��q��;�n�tfB�}I���
������B�5��xC��Y��NZ���Op\�zY�U�2�-J��q�Y� �|3��D�D���q/��M� ����i,���@�����#E0��@�2Y)r��sL�qJ�:3��Sg��t��8�A��2`a��Ma�aL���oB6�����U���s`|��q.J��Z�3�\+uf�#�cf�9*!e�����8�N1��!t�A1�t(s"0�E0���� �{�#�\��;�������:3��ff��Rg��0>�`�\*m���T�k�H���"QX&�N��.(61�
���s��@X<V�����h���CX?2���.����.Bm��M��*_i#�[c���:��F�9��`O��p�8�Pg�9$��9� �����p>�Xq�*�Z���.����PXP�����BQbp�,9��)��}mP���s���S��q�sH�c�1uF�CbeRxb��K���1�OR�@<�?����� 1�2Z =1��,>Z�F������\�H���F���>RD$V.X=���*�
��PZ�&k(�2�SMQ�,��}g)��.�I����rE��y���'i#.#By���\1X��*�!�T}���X|A�6`����`=Ou�R���E!���v���M�{���F\����VD�r��\G{����KOGHB����!��u��X�I��$"e���3&�s�At��p��A?���Ct���!�
(�i�����d�M��g����
<���\�����8q�m����t�p!J��rf�C� ���s
tft�`�X�RZ��t����H,��N��*#y�(�����2��Kz$z,#y�����J�Y�
u������jF2��pQ"BSF2�@J��<�*#y?q��G�����Df�or�8�J���Y�s3�2�7s����u��M�"�C��0��z)M��%�d!������5C���a�<Pd/z��=G��0g���E�va<�S3Bd��*����
�\�� �i�!1��<hPd�U��|j��GH�a�&L���90�&���
�e��2J!����=&�BF�� �_���0��;���}���S>����?���������_�����/�����o?�������g��������~�l���|���>������ �{���� ��W�0Q��;�� �������5�� ��c��p�����w���dq��������}?��O||�m/I�%-���,I]�Qb��
p��>Z(� �8b@}��p�p�\�n0v�asY
�:M_N|}���Z�n���Tn��5l��%^���k��5l�L}��1y
�-S@?��8�
�-S���q�6�e���x+q��R@�i�8������y��<�a���|�\1y
�-WO����J��f��'�����2�3��3���2��
w����e)���v�4����u�,���ux���O~�'?l�-wL^�f���%���c��6[f������5m�L��'q��6[���
%�x�����x�8��e)��4Y�������� '?��������l����>yiL^�f��'/��k�l��O���l�L�o�\�������6�%�����=������=��k�����b�Z&[���xI.y���l�����oB@�-s@���nOh�e
X}�8�m��r@7��;�6�,t�&���������%�]��z���|G+�6��W����m/}L������|
,T��.��2�<�y(i�o��s@���F���6l����6��+i�o�iS��m��������%�]��%�E�����'����W��(��<t���4�����/��F��,�����4������%+i���e)����q��[���w�����b��-G|\~�z����f�p���4��f���xI�\l�L}&�r��,�3^�(W��R��m��������%�]�M�VL^j��LN�~�z����f��'��Q�6[��~�K�j�e
�0�xjsY
�g��Q�6�e�~�<��ux�?m�c��AWo{I1y���;9�}��c�R�-S@���F��l��/n��b�e�7�W�(������xq����R��m�}x�?m�c��AWo{I!y�b�����$��$$�^l�L}�����l����7�{��2�m7�{��,�3^�(�����u�,����?m�c��AWo{I y�l���o/����]b��6[��.y��Q����)�K^;n��j�e
��p���^m.K����Fy�6�e�~�<��/'��uM~-q7��m.���UM������\��5&�j�e���_��&[��.y��k����9�o�g�jrY
����[�4���������o]�_K�
�z�K����l��7���]�qw�7�-s@����zo&[��~w}����L��}����fsY
�g��5��l.K���~����5����������������������������]�qw�7�-S@?����n�p�}���w��K�g��5�n�p��~N|����5��������\�]�����Q���������C���w��}�&�3^�]��
�0�a���.)����_C��
��������o]�_K�
�z�KJ��f�����w�w�]����'�����;49�����z�o���~w}���w��K�g��5�n�p��~�|����5������������;4;y�x����t��;4)��]�Iw��C��/���7\r@���k������/~
��7\r���''��uM~-q7��m*�t��;4;y�x����t��;49�O^Iw��C��/���7\r���q��o���n���t��6�%���5��������%�]��%��e�����b����t��;49�K^�t��;49�K^�t��.)��]��5�n�p���'�k������N���q��[���w�����b����H�?��.Iw��C����t��;4)��]���n�p�}~oH:�6���n���8��e)�����_��&���t��������-�hP���$�u�M��W�]�����~�K�������a�3�}�$�3^����C����''��uM~-q7��m*�t��[*2�����K�]�o���>y%�u��J�g���n�R�}�8��wHR��g�;�}�$|�y�q��[���w�����b��o����|�J���-��'���n�RI���$�u��J��p���C��o%NcsY
�����������%�]��%��e�R�4(~w]���}K%����t��[*9������}K%�m���g�!����c������o?O>N|}���Z�n���T.����TD�����K�]�o���>y%�u��J�g���n�R����C����4q��R���''��uM~-q7��m/)!/�-5�hP������a�R�}������������#m�L ���k 3������n��:������o?O>N|}���Z�n����G�?�����?����'l������?}���'~��8���_=���M��<:D{ 8��z ���E^����;�_w����I������s�u��>�y(��E��,�ZZ��q/|;��pm�g����%`�Y=�,}�}���Qy�1����0`�R��+��������O|�����~��|�������E�vuh���pS�������-@��:D�"���#<�U�S������T��RD c�qX-C���d���������1�]�m���xu�s&9y<6���a�1������)b��f�ms�����`qh-C\AL��������,���d�y��<��|����9��xs������[!�5�9S���V�x�w�w�5R/;f_��1���w�t��RD�s�pX'�����d��E��L��s��I�;�1�?Zq�3C���$�l�a�1���'�L;&����Z�d��+YZK}���a�, '��r������9�^�����U�������i��K� j����)bd�0�����s�����2b�1}��"BO"���$= �|��U�[W�7��������t�������?�&J���6��h@j����<��5�i�k6��hP����>l������������;�w�|����V��M�x,������h�s�f�
���������#����?mv�P���1�cX
endstream
endobj
1027 0 obj
<</Type/ObjStm/N 500/First 5319/Filter/FlateDecode/Length 5720>>
stream
x��]��-����w8op[$����&��3�����o�oo���aI������UI���g�%�s}�%�S+�;?�t4��t�FiQ���Gu�Q>:�7�����O-���]����!.�6����
v}�l4@�|�`@+������?��b 1�!W����|�8��������@���&��P+��?�
�������w���?�#����PW��]�����vy�����/E� Z+Z37�4��hZs�� �Uou�����1�c�h���S����RF���x���$S��}�����0��]��oH}�h��Jx�o
�
w����j��8j�����4��M�w>^@��=Bc�x#|�������>>������+|4��"@|�V�G�|����$|�n4�@��c&���� �9<�k�Q�%XC�x8}��G��B��y?6�#w��%n��M�����WD�����
/r7�7������[hn�����O5����()��j��J�FC�7Im�6\���oo��1����Qo��Z�(B�a1�2o!J�t�|���o!J����-���Ox�7����[���Vn������+�O�7J|���X/6��v)����[�M�F�=�A���-����{�����>�=�%,���*���]c���,F�~��O}��x.�9�T�������|��>0{wE�:��0{�L}���������q�u=XG y�`��{HX��Z��o����Sf������y��r7��p���B��}���>�����������qF���3���%b��3E1+�S�3���`���:-,��|Z�O����=�=���O|���/���r�r#�!��c���C9�������7���Mlc����)a�)�N�C���S��E����1������V3���C�kX+h�����0��%�k�Qr�`��&��+�oZl��f��Zl�l����X�����.���C����CHCbOu�Q"