diff --git a/age--1.5.0--y.y.y.sql b/age--1.5.0--y.y.y.sql index 3e05a6a01..0d9558cb3 100644 --- a/age--1.5.0--y.y.y.sql +++ b/age--1.5.0--y.y.y.sql @@ -117,3 +117,13 @@ RETURNS NULL ON NULL INPUT PARALLEL SAFE AS 'MODULE_PATHNAME'; +CREATE FUNCTION ag_catalog.agtype_to_json(agtype) + RETURNS json + LANGUAGE c + IMMUTABLE +RETURNS NULL ON NULL INPUT +PARALLEL SAFE +AS 'MODULE_PATHNAME'; + +CREATE CAST (agtype AS json) + WITH FUNCTION ag_catalog.agtype_to_json(agtype); diff --git a/regress/expected/expr.out b/regress/expected/expr.out index 7d57eeabe..281c3e661 100644 --- a/regress/expected/expr.out +++ b/regress/expected/expr.out @@ -2417,6 +2417,185 @@ SELECT agtype_typecast_path(null); (1 row) +-- +-- Tests for explicit typecast to json +-- +-- Should pass +SELECT agtype_to_json('{}'::agtype); + agtype_to_json +---------------- + {} +(1 row) + +SELECT agtype_to_json('{ "hello": "world" }'::agtype); + agtype_to_json +-------------------- + {"hello": "world"} +(1 row) + +SELECT agtype_to_json('{ "hello": "world" }'::agtype)->>'hello'; + ?column? +---------- + world +(1 row) + +SELECT agtype_to_json('[]'::agtype); + agtype_to_json +---------------- + [] +(1 row) + +SELECT agtype_to_json('[1, 2, 3]'::agtype); + agtype_to_json +---------------- + [1, 2, 3] +(1 row) + +SELECT agtype_to_json(null::agtype); + agtype_to_json +---------------- + +(1 row) + +SELECT cast('{}'::agtype as json); + json +------ + {} +(1 row) + +SELECT cast('{ "hello": "world" }'::agtype as json); + json +-------------------- + {"hello": "world"} +(1 row) + +SELECT cast('{ "hello": "world" }'::agtype as json)->>'hello'; + ?column? +---------- + world +(1 row) + +SELECT cast('[]'::agtype as json); + json +------ + [] +(1 row) + +SELECT cast('[1, 2, 3]'::agtype as json); + json +----------- + [1, 2, 3] +(1 row) + +SELECT cast('[1, 2, 3]'::agtype as json)->1; + ?column? +---------- + 2 +(1 row) + +SELECT cast(null::agtype as json); + json +------ + +(1 row) + +SELECT vertex_in_json, vertex_in_json->'id' as id, pg_typeof(vertex_in_json) FROM cypher('type_coercion', $$ MATCH (a) RETURN a $$) AS (vertex_in_json json); + vertex_in_json | id | pg_typeof +--------------------------------------------------------+-----------------+----------- + {"id": 281474976710657, "label": "", "properties": {}} | 281474976710657 | json + {"id": 281474976710658, "label": "", "properties": {}} | 281474976710658 | json +(2 rows) + +SELECT edge_in_json, edge_in_json->'id' as id, pg_typeof(edge_in_json) FROM cypher('type_coercion', $$ MATCH ()-[e]->() RETURN e $$) AS (edge_in_json json); + edge_in_json | id | pg_typeof +--------------------------------------------------------------------------------------------------------------------+-----------------+----------- + {"id": 844424930131969, "label": "edge", "end_id": 281474976710658, "start_id": 281474976710657, "properties": {}} | 844424930131969 | json +(1 row) + +SELECT vle_in_json, vle_in_json->0 as first_edge, pg_typeof(vle_in_json) FROM cypher('type_coercion', $$ MATCH ()-[e *]->() RETURN e $$) AS (vle_in_json json); + vle_in_json | first_edge | pg_typeof +----------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------+----------- + [{"id": 844424930131969, "label": "edge", "end_id": 281474976710658, "start_id": 281474976710657, "properties": {}}] | {"id": 844424930131969, "label": "edge", "end_id": 281474976710658, "start_id": 281474976710657, "properties": {}} | json +(1 row) + +SELECT *, pg_typeof(props_in_json) FROM cypher('type_coercion', $$ MATCH (a) RETURN properties(a) $$) AS (props_in_json json); + props_in_json | pg_typeof +---------------+----------- + {} | json + {} | json +(2 rows) + +SELECT path_in_json, path_in_json->0 as first_node FROM cypher('type_coercion', $$ MATCH p=()-[]->() RETURN p $$) AS (path_in_json json); + path_in_json | first_node +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------- + [{"id": 281474976710657, "label": "", "properties": {}}, {"id": 844424930131969, "label": "edge", "end_id": 281474976710658, "start_id": 281474976710657, "properties": {}}, {"id": 281474976710658, "label": "", "properties": {}}] | {"id": 281474976710657, "label": "", "properties": {}} +(1 row) + +SELECT *, pg_typeof(nodes_in_json) FROM cypher('type_coercion', $$ MATCH p=()-[]->() RETURN nodes(p) $$) AS (nodes_in_json json); + nodes_in_json | pg_typeof +------------------------------------------------------------------------------------------------------------------+----------- + [{"id": 281474976710657, "label": "", "properties": {}}, {"id": 281474976710658, "label": "", "properties": {}}] | json +(1 row) + +SELECT *, pg_typeof(rels_in_json) FROM cypher('type_coercion', $$ MATCH p=()-[]->() RETURN relationships(p) $$) AS (rels_in_json json); + rels_in_json | pg_typeof +----------------------------------------------------------------------------------------------------------------------+----------- + [{"id": 844424930131969, "label": "edge", "end_id": 281474976710658, "start_id": 281474976710657, "properties": {}}] | json +(1 row) + +SELECT cast(result as json) FROM cypher('type_coercion', $$ MATCH (a) RETURN a $$) AS (result agtype); + result +-------------------------------------------------------- + {"id": 281474976710657, "label": "", "properties": {}} + {"id": 281474976710658, "label": "", "properties": {}} +(2 rows) + +SELECT cast(result as json) FROM cypher('type_coercion', $$ MATCH ()-[e]-() RETURN e $$) AS (result agtype); + result +-------------------------------------------------------------------------------------------------------------------- + {"id": 844424930131969, "label": "edge", "end_id": 281474976710658, "start_id": 281474976710657, "properties": {}} + {"id": 844424930131969, "label": "edge", "end_id": 281474976710658, "start_id": 281474976710657, "properties": {}} +(2 rows) + +SELECT cast(result as json) FROM cypher('type_coercion', $$ MATCH ()-[e *]->() RETURN e $$) AS (result agtype); + result +---------------------------------------------------------------------------------------------------------------------- + [{"id": 844424930131969, "label": "edge", "end_id": 281474976710658, "start_id": 281474976710657, "properties": {}}] +(1 row) + +SELECT cast(result as json) FROM cypher('type_coercion', $$ MATCH p=()-[]->() RETURN p $$) AS (result agtype); + result +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + [{"id": 281474976710657, "label": "", "properties": {}}, {"id": 844424930131969, "label": "edge", "end_id": 281474976710658, "start_id": 281474976710657, "properties": {}}, {"id": 281474976710658, "label": "", "properties": {}}] +(1 row) + +SELECT pg_typeof(cast(result as json)) FROM cypher('type_coercion', $$ MATCH p=()-[]->() RETURN p $$) AS (result agtype); + pg_typeof +----------- + json +(1 row) + +-- Should fail +SELECT agtype_to_json('1'::agtype); +ERROR: cannot cast agtype integer to json +SELECT agtype_to_json('1.111'::agtype); +ERROR: cannot cast agtype float to json +SELECT agtype_to_json('true'::agtype); +ERROR: cannot cast agtype boolean to json +SELECT agtype_to_json('false'::agtype); +ERROR: cannot cast agtype boolean to json +SELECT agtype_to_json('1::numeric'::agtype); +ERROR: cannot cast agtype numeric to json +SELECT cast(result as json) FROM cypher('type_coercion', $$ RETURN 1 $$) AS (result agtype); +ERROR: cannot cast agtype integer to json +SELECT cast(result as json) FROM cypher('type_coercion', $$ RETURN 1.111 $$) AS (result agtype); +ERROR: cannot cast agtype float to json +SELECT cast(result as json) FROM cypher('type_coercion', $$ RETURN true $$) AS (result agtype); +ERROR: cannot cast agtype boolean to json +SELECT cast(result as json) FROM cypher('type_coercion', $$ RETURN false $$) AS (result agtype); +ERROR: cannot cast agtype boolean to json +SELECT cast(result as json) FROM cypher('type_coercion', $$ RETURN 1::numeric $$) AS (result agtype); +ERROR: cannot cast agtype numeric to json -- test functions -- create some vertices and edges SELECT * FROM cypher('expr', $$CREATE (:v)$$) AS (a agtype); diff --git a/regress/sql/expr.sql b/regress/sql/expr.sql index 0961adb2f..19d9b1b8b 100644 --- a/regress/sql/expr.sql +++ b/regress/sql/expr.sql @@ -1064,6 +1064,52 @@ SELECT agtype_in('null::path'); SELECT * FROM cypher('expr', $$ RETURN null::path $$) AS r(result agtype); SELECT agtype_typecast_path(agtype_in('null')); SELECT agtype_typecast_path(null); +-- +-- Tests for explicit typecast to json +-- + +-- Should pass +SELECT agtype_to_json('{}'::agtype); +SELECT agtype_to_json('{ "hello": "world" }'::agtype); +SELECT agtype_to_json('{ "hello": "world" }'::agtype)->>'hello'; +SELECT agtype_to_json('[]'::agtype); +SELECT agtype_to_json('[1, 2, 3]'::agtype); +SELECT agtype_to_json(null::agtype); + +SELECT cast('{}'::agtype as json); +SELECT cast('{ "hello": "world" }'::agtype as json); +SELECT cast('{ "hello": "world" }'::agtype as json)->>'hello'; +SELECT cast('[]'::agtype as json); +SELECT cast('[1, 2, 3]'::agtype as json); +SELECT cast('[1, 2, 3]'::agtype as json)->1; +SELECT cast(null::agtype as json); + +SELECT vertex_in_json, vertex_in_json->'id' as id, pg_typeof(vertex_in_json) FROM cypher('type_coercion', $$ MATCH (a) RETURN a $$) AS (vertex_in_json json); +SELECT edge_in_json, edge_in_json->'id' as id, pg_typeof(edge_in_json) FROM cypher('type_coercion', $$ MATCH ()-[e]->() RETURN e $$) AS (edge_in_json json); +SELECT vle_in_json, vle_in_json->0 as first_edge, pg_typeof(vle_in_json) FROM cypher('type_coercion', $$ MATCH ()-[e *]->() RETURN e $$) AS (vle_in_json json); +SELECT *, pg_typeof(props_in_json) FROM cypher('type_coercion', $$ MATCH (a) RETURN properties(a) $$) AS (props_in_json json); +SELECT path_in_json, path_in_json->0 as first_node FROM cypher('type_coercion', $$ MATCH p=()-[]->() RETURN p $$) AS (path_in_json json); +SELECT *, pg_typeof(nodes_in_json) FROM cypher('type_coercion', $$ MATCH p=()-[]->() RETURN nodes(p) $$) AS (nodes_in_json json); +SELECT *, pg_typeof(rels_in_json) FROM cypher('type_coercion', $$ MATCH p=()-[]->() RETURN relationships(p) $$) AS (rels_in_json json); + +SELECT cast(result as json) FROM cypher('type_coercion', $$ MATCH (a) RETURN a $$) AS (result agtype); +SELECT cast(result as json) FROM cypher('type_coercion', $$ MATCH ()-[e]-() RETURN e $$) AS (result agtype); +SELECT cast(result as json) FROM cypher('type_coercion', $$ MATCH ()-[e *]->() RETURN e $$) AS (result agtype); +SELECT cast(result as json) FROM cypher('type_coercion', $$ MATCH p=()-[]->() RETURN p $$) AS (result agtype); +SELECT pg_typeof(cast(result as json)) FROM cypher('type_coercion', $$ MATCH p=()-[]->() RETURN p $$) AS (result agtype); + +-- Should fail +SELECT agtype_to_json('1'::agtype); +SELECT agtype_to_json('1.111'::agtype); +SELECT agtype_to_json('true'::agtype); +SELECT agtype_to_json('false'::agtype); +SELECT agtype_to_json('1::numeric'::agtype); + +SELECT cast(result as json) FROM cypher('type_coercion', $$ RETURN 1 $$) AS (result agtype); +SELECT cast(result as json) FROM cypher('type_coercion', $$ RETURN 1.111 $$) AS (result agtype); +SELECT cast(result as json) FROM cypher('type_coercion', $$ RETURN true $$) AS (result agtype); +SELECT cast(result as json) FROM cypher('type_coercion', $$ RETURN false $$) AS (result agtype); +SELECT cast(result as json) FROM cypher('type_coercion', $$ RETURN 1::numeric $$) AS (result agtype); -- test functions -- create some vertices and edges diff --git a/sql/agtype_coercions.sql b/sql/agtype_coercions.sql index cdf5f6f8c..745190fd7 100644 --- a/sql/agtype_coercions.sql +++ b/sql/agtype_coercions.sql @@ -141,3 +141,14 @@ AS 'MODULE_PATHNAME'; CREATE CAST (agtype AS int[]) WITH FUNCTION ag_catalog.agtype_to_int4_array(variadic "any"); + +CREATE FUNCTION ag_catalog.agtype_to_json(agtype) + RETURNS json + LANGUAGE c + IMMUTABLE +RETURNS NULL ON NULL INPUT +PARALLEL SAFE +AS 'MODULE_PATHNAME'; + +CREATE CAST (agtype AS json) + WITH FUNCTION ag_catalog.agtype_to_json(agtype); \ No newline at end of file diff --git a/src/backend/utils/adt/agtype.c b/src/backend/utils/adt/agtype.c index da8ee9a99..5663e977e 100644 --- a/src/backend/utils/adt/agtype.c +++ b/src/backend/utils/adt/agtype.c @@ -29,6 +29,7 @@ */ #include "postgres.h" +#include "utils/jsonfuncs.h" #include #include @@ -96,7 +97,8 @@ static void agtype_in_array_start(void *pstate); static void agtype_in_array_end(void *pstate); static void agtype_in_object_field_start(void *pstate, char *fname, bool isnull); -static void agtype_put_escaped_value(StringInfo out, agtype_value *scalar_val); +static void agtype_put_escaped_value(StringInfo out, agtype_value *scalar_val, + bool extend); static void escape_agtype(StringInfo buf, const char *str); bool is_decimal_needed(char *numstr); static void agtype_in_scalar(void *pstate, char *token, @@ -114,7 +116,8 @@ static void datum_to_agtype(Datum val, bool is_null, agtype_in_state *result, agt_type_category tcategory, Oid outfuncoid, bool key_scalar); static char *agtype_to_cstring_worker(StringInfo out, agtype_container *in, - int estimated_len, bool indent); + int estimated_len, bool indent, + bool extend); static text *agtype_value_to_text(agtype_value *scalar_val, bool err_not_scalar); static void add_indent(StringInfo out, bool indent, int level); @@ -791,7 +794,8 @@ static bool is_array_path(agtype_value *agtv) return true; } -static void agtype_put_escaped_value(StringInfo out, agtype_value *scalar_val) +static void agtype_put_escaped_value(StringInfo out, agtype_value *scalar_val, + bool extend) { char *numstr; @@ -808,7 +812,10 @@ static void agtype_put_escaped_value(StringInfo out, agtype_value *scalar_val) appendStringInfoString( out, DatumGetCString(DirectFunctionCall1( numeric_out, PointerGetDatum(scalar_val->val.numeric)))); - appendBinaryStringInfo(out, "::numeric", 9); + if (extend) + { + appendBinaryStringInfo(out, "::numeric", 9); + } break; case AGTV_INTEGER: appendStringInfoString( @@ -834,8 +841,12 @@ static void agtype_put_escaped_value(StringInfo out, agtype_value *scalar_val) agtype *prop; scalar_val->type = AGTV_OBJECT; prop = agtype_value_to_agtype(scalar_val); - agtype_to_cstring_worker(out, &prop->root, prop->vl_len_, false); - appendBinaryStringInfo(out, "::vertex", 8); + agtype_to_cstring_worker(out, &prop->root, prop->vl_len_, + false, extend); + if (extend) + { + appendBinaryStringInfo(out, "::vertex", 8); + } break; } case AGTV_EDGE: @@ -843,8 +854,12 @@ static void agtype_put_escaped_value(StringInfo out, agtype_value *scalar_val) agtype *prop; scalar_val->type = AGTV_OBJECT; prop = agtype_value_to_agtype(scalar_val); - agtype_to_cstring_worker(out, &prop->root, prop->vl_len_, false); - appendBinaryStringInfo(out, "::edge", 6); + agtype_to_cstring_worker(out, &prop->root, prop->vl_len_, + false, extend); + if (extend) + { + appendBinaryStringInfo(out, "::edge", 6); + } break; } case AGTV_PATH: @@ -852,8 +867,12 @@ static void agtype_put_escaped_value(StringInfo out, agtype_value *scalar_val) agtype *prop; scalar_val->type = AGTV_ARRAY; prop = agtype_value_to_agtype(scalar_val); - agtype_to_cstring_worker(out, &prop->root, prop->vl_len_, false); - appendBinaryStringInfo(out, "::path", 6); + agtype_to_cstring_worker(out, &prop->root, prop->vl_len_, + false, extend); + if (extend) + { + appendBinaryStringInfo(out, "::path", 6); + } break; } @@ -1051,7 +1070,8 @@ static void agtype_in_scalar(void *pstate, char *token, char *agtype_to_cstring(StringInfo out, agtype_container *in, int estimated_len) { - return agtype_to_cstring_worker(out, in, estimated_len, false); + return agtype_to_cstring_worker(out, in, estimated_len, false, + true); } /* @@ -1060,14 +1080,16 @@ char *agtype_to_cstring(StringInfo out, agtype_container *in, char *agtype_to_cstring_indent(StringInfo out, agtype_container *in, int estimated_len) { - return agtype_to_cstring_worker(out, in, estimated_len, true); + return agtype_to_cstring_worker(out, in, estimated_len, true, + true); } /* * common worker for above two functions */ static char *agtype_to_cstring_worker(StringInfo out, agtype_container *in, - int estimated_len, bool indent) + int estimated_len, bool indent, + bool extend) { bool first = true; agtype_iterator *it; @@ -1135,14 +1157,14 @@ static char *agtype_to_cstring_worker(StringInfo out, agtype_container *in, add_indent(out, use_indent, level); /* agtype rules guarantee this is a string */ - agtype_put_escaped_value(out, &v); + agtype_put_escaped_value(out, &v, extend); appendBinaryStringInfo(out, ": ", 2); type = agtype_iterator_next(&it, &v, false); if (type == WAGT_VALUE) { first = false; - agtype_put_escaped_value(out, &v); + agtype_put_escaped_value(out, &v, extend); } else { @@ -1163,7 +1185,7 @@ static char *agtype_to_cstring_worker(StringInfo out, agtype_container *in, if (!raw_scalar) add_indent(out, use_indent, level); - agtype_put_escaped_value(out, &v); + agtype_put_escaped_value(out, &v, extend); break; case WAGT_END_ARRAY: level--; @@ -3097,6 +3119,47 @@ Datum agtype_to_text(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(text_value); } +PG_FUNCTION_INFO_V1(agtype_to_json); + +/* + * Cast agtype to json. + * + * If the input agtype is vertex, edge or path, the trailing + * type(::vertex, ::edge, ::path) is removed. + */ +Datum agtype_to_json(PG_FUNCTION_ARGS) +{ + Datum result; + char *json_str; + agtype *agt; + + agt = AG_GET_ARG_AGTYPE_P(0); + + if (AGT_ROOT_IS_SCALAR(agt)) + { + enum agtype_value_type type; + + type = get_ith_agtype_value_type(&agt->root, 0); + if (type >= AGTV_NUMERIC && type <= AGTV_BOOL) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot cast agtype %s to json", + agtype_value_type_to_string(type)))); + } + } + + json_str = agtype_to_cstring_worker(NULL, &agt->root, VARSIZE(agt), + false, false); + + result = DirectFunctionCall1(json_in, CStringGetDatum(json_str)); + + PG_FREE_IF_COPY(agt, 0); + pfree(json_str); + + PG_RETURN_DATUM(result); +} + PG_FUNCTION_INFO_V1(bool_to_agtype); /* @@ -3612,7 +3675,7 @@ static Datum process_access_operator_result(FunctionCallInfo fcinfo, str = agtype_to_cstring_worker(out, agtc, agtv->val.binary.len, - false); + false, true); result = cstring_to_text(str); } else diff --git a/src/backend/utils/adt/agtype_util.c b/src/backend/utils/adt/agtype_util.c index fbd4e36b8..1022d4c2f 100644 --- a/src/backend/utils/adt/agtype_util.c +++ b/src/backend/utils/adt/agtype_util.c @@ -591,6 +591,88 @@ agtype_value *get_ith_agtype_value_from_container(agtype_container *container, return result; } +/* + * Get type of i-th value of an agtype array. + */ +enum agtype_value_type get_ith_agtype_value_type(agtype_container *container, + uint32 i) +{ + enum agtype_value_type type; + uint32 nelements; + agtentry entry; + + if (!AGTYPE_CONTAINER_IS_ARRAY(container)) + { + ereport(ERROR, (errmsg("container is not an agtype array"))); + } + + nelements = AGTYPE_CONTAINER_SIZE(container); + if (i >= nelements) + { + ereport(ERROR, (errmsg("index out of bounds"))); + } + + entry = container->children[i]; + switch ((entry)&AGTENTRY_TYPEMASK) + { + case AGTENTRY_IS_STRING: + type = AGTV_STRING; + break; + case AGTENTRY_IS_NUMERIC: + type = AGTV_NUMERIC; + break; + case AGTENTRY_IS_AGTYPE: + { + char *base_addr; + uint32 agt_header; + char *base; + + base_addr = (char *)&container->children[nelements]; + base = base_addr + INTALIGN(get_agtype_offset(container, i)); + agt_header = *((uint32 *)base); + + switch (agt_header) + { + case AGT_HEADER_INTEGER: + type = AGTV_INTEGER; + break; + case AGT_HEADER_FLOAT: + type = AGTV_FLOAT; + break; + case AGT_HEADER_VERTEX: + type = AGTV_VERTEX; + break; + case AGT_HEADER_EDGE: + type = AGTV_EDGE; + break; + case AGT_HEADER_PATH: + type = AGTV_PATH; + break; + default: + ereport(ERROR, (errmsg("unexpected agt_header type"))); + break; + } + break; + } + case AGTENTRY_IS_BOOL_TRUE: + type = AGTV_BOOL; + break; + case AGTENTRY_IS_BOOL_FALSE: + type = AGTV_BOOL; + break; + case AGTENTRY_IS_NULL: + type = AGTV_NULL; + break; + case AGTENTRY_IS_CONTAINER: + type = AGTV_BINARY; + break; + default: + ereport(ERROR, (errmsg("unexpected agtentry type"))); + break; + } + return type; +} + /* * A helper function to fill in an agtype_value to represent an element of an * array, or a key or value of an object. diff --git a/src/include/utils/agtype.h b/src/include/utils/agtype.h index ebde040a7..cf68fc434 100644 --- a/src/include/utils/agtype.h +++ b/src/include/utils/agtype.h @@ -468,6 +468,8 @@ agtype_value *find_agtype_value_from_container(agtype_container *container, agtype_value *key); agtype_value *get_ith_agtype_value_from_container(agtype_container *container, uint32 i); +enum agtype_value_type get_ith_agtype_value_type(agtype_container *container, + uint32 i); agtype_value *push_agtype_value(agtype_parse_state **pstate, agtype_iterator_token seq, agtype_value *agtval); @@ -555,7 +557,6 @@ agtype_iterator *get_next_list_element(agtype_iterator *it, void pfree_agtype_value(agtype_value* value); void pfree_agtype_in_state(agtype_in_state* value); agtype_value *agtype_value_from_cstring(char *str, int len); - /* Oid accessors for AGTYPE */ Oid get_AGTYPEOID(void); Oid get_AGTYPEARRAYOID(void);