Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ REGRESS = scan \
age_reduce \
map_projection \
direct_field_access \
generated_columns \
security \
reserved_keyword_alias \
agtype_jsonb_cast \
Expand Down
222 changes: 222 additions & 0 deletions regress/expected/generated_columns.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*
* Regression test for issue #2450.
*
* A GENERATED ALWAYS ... STORED column (or any user-added column) on a label
* table adds an attribute AGE's CREATE/MERGE/SET paths do not populate. The
* slot's tuple descriptor is the full relation descriptor, so heap_form_tuple()
* used to read the uninitialized slot entry and segfault. AGE now null-inits the
* entity slot and computes stored generated columns before materializing the
* tuple. Generated columns must be computed on insert and recomputed on update;
* plain user columns must default to NULL rather than crash.
*/
LOAD 'age';
SET search_path TO ag_catalog;
SELECT create_graph('generated_columns');
NOTICE: graph "generated_columns" has been created
create_graph
--------------

(1 row)

--
-- GENERATED ALWAYS ... STORED column on a vertex label
--
SELECT create_vlabel('generated_columns', 'Product');
NOTICE: VLabel "Product" has been created
create_vlabel
---------------

(1 row)

ALTER TABLE generated_columns."Product"
ADD COLUMN category varchar(25)
GENERATED ALWAYS AS (agtype_access_operator(properties, '"category"')) STORED;
-- CREATE: the generated column is computed from the new properties
SELECT * FROM cypher('generated_columns', $$
CREATE (p:Product {category: 'disk', type: 'M1234'}) RETURN p
$$) AS (p agtype);
p
----------------------------------------------------------------------------------------------------------
{"id": 844424930131969, "label": "Product", "properties": {"type": "M1234", "category": "disk"}}::vertex
(1 row)

-- CREATE where the generation expression yields NULL (no such key)
SELECT * FROM cypher('generated_columns', $$
CREATE (p:Product {type: 'no-cat'}) RETURN p
$$) AS (p agtype);
p
---------------------------------------------------------------------------------------
{"id": 844424930131970, "label": "Product", "properties": {"type": "no-cat"}}::vertex
(1 row)

-- MERGE also computes the generated column
SELECT * FROM cypher('generated_columns', $$
MERGE (p:Product {category: 'merged'}) RETURN p
$$) AS (p agtype);
p
-------------------------------------------------------------------------------------------
{"id": 844424930131971, "label": "Product", "properties": {"category": "merged"}}::vertex
(1 row)

-- MERGE again on the same pattern must MATCH (not re-insert); this exercises the
-- matched path where the slot is intentionally left uncleared (issue #2450)
SELECT * FROM cypher('generated_columns', $$
MERGE (p:Product {category: 'merged'}) RETURN p
$$) AS (p agtype);
p
-------------------------------------------------------------------------------------------
{"id": 844424930131971, "label": "Product", "properties": {"category": "merged"}}::vertex
(1 row)

-- SET recomputes the generated column from the updated properties
SELECT * FROM cypher('generated_columns', $$
MATCH (p:Product {category: 'disk'}) SET p.category = 'ssd', p.note = 'x' RETURN p
$$) AS (p agtype);
p
----------------------------------------------------------------------------------------------------------------------
{"id": 844424930131969, "label": "Product", "properties": {"note": "x", "type": "M1234", "category": "ssd"}}::vertex
(1 row)

-- The stored generated column always mirrors properties.category
SELECT category, properties FROM generated_columns."Product" ORDER BY category NULLS LAST;
category | properties
----------+---------------------------------------------------
"merged" | {"category": "merged"}
"ssd" | {"note": "x", "type": "M1234", "category": "ssd"}
| {"type": "no-cat"}
(3 rows)

--
-- GENERATED ALWAYS ... STORED column on an edge label
--
SELECT create_elabel('generated_columns', 'REL');
NOTICE: ELabel "REL" has been created
create_elabel
---------------

(1 row)

ALTER TABLE generated_columns."REL"
ADD COLUMN kind varchar(25)
GENERATED ALWAYS AS (agtype_access_operator(properties, '"kind"')) STORED;
SELECT * FROM cypher('generated_columns', $$
CREATE (:Product {category: 'a'})-[r:REL {kind: 'link'}]->(:Product {category: 'b'})
RETURN r
$$) AS (r agtype);
r
----------------------------------------------------------------------------------------------------------------------------------------
{"id": 1125899906842625, "label": "REL", "end_id": 844424930131973, "start_id": 844424930131972, "properties": {"kind": "link"}}::edge
(1 row)

SELECT kind FROM generated_columns."REL";
kind
--------
"link"
(1 row)

--
-- Plain (non-generated) user column: must not crash; defaults to NULL
--
SELECT create_vlabel('generated_columns', 'Plain');
NOTICE: VLabel "Plain" has been created
create_vlabel
---------------

(1 row)

ALTER TABLE generated_columns."Plain" ADD COLUMN tag text;
SELECT * FROM cypher('generated_columns', $$
CREATE (p:Plain {n: 1}) RETURN p
$$) AS (p agtype);
p
----------------------------------------------------------------------------
{"id": 1407374883553281, "label": "Plain", "properties": {"n": 1}}::vertex
(1 row)

SELECT * FROM cypher('generated_columns', $$
MATCH (p:Plain) SET p.n = 2 RETURN p
$$) AS (p agtype);
p
----------------------------------------------------------------------------
{"id": 1407374883553281, "label": "Plain", "properties": {"n": 2}}::vertex
(1 row)

SELECT properties, tag FROM generated_columns."Plain";
properties | tag
------------+-----
{"n": 2} |
(1 row)

--
-- Generated column referencing the tableoid system column: the entity slot must
-- carry tts_tableOid before the generated columns are computed, otherwise the
-- expression sees InvalidOid instead of the label table's OID (issue #2450).
--
SELECT create_vlabel('generated_columns', 'T');
NOTICE: VLabel "T" has been created
create_vlabel
---------------

(1 row)

ALTER TABLE generated_columns."T"
ADD COLUMN tbl oid GENERATED ALWAYS AS (tableoid) STORED;
SELECT * FROM cypher('generated_columns', $$
CREATE (n:T {k: 1}) RETURN n
$$) AS (n agtype);
n
------------------------------------------------------------------------
{"id": 1688849860263937, "label": "T", "properties": {"k": 1}}::vertex
(1 row)

SELECT * FROM cypher('generated_columns', $$
MATCH (n:T) SET n.k = 2 RETURN n
$$) AS (n agtype);
n
------------------------------------------------------------------------
{"id": 1688849860263937, "label": "T", "properties": {"k": 2}}::vertex
(1 row)

-- tbl must equal the real label-table OID on both the CREATE and SET paths
SELECT tbl = 'generated_columns."T"'::regclass::oid AS tableoid_ok
FROM generated_columns."T";
tableoid_ok
-------------
t
(1 row)

--
-- Cleanup
--
SELECT drop_graph('generated_columns', true);
NOTICE: drop cascades to 6 other objects
DETAIL: drop cascades to table generated_columns._ag_label_vertex
drop cascades to table generated_columns._ag_label_edge
drop cascades to table generated_columns."Product"
drop cascades to table generated_columns."REL"
drop cascades to table generated_columns."Plain"
drop cascades to table generated_columns."T"
NOTICE: graph "generated_columns" has been dropped
drop_graph
------------

(1 row)

129 changes: 129 additions & 0 deletions regress/sql/generated_columns.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/*
* Regression test for issue #2450.
*
* A GENERATED ALWAYS ... STORED column (or any user-added column) on a label
* table adds an attribute AGE's CREATE/MERGE/SET paths do not populate. The
* slot's tuple descriptor is the full relation descriptor, so heap_form_tuple()
* used to read the uninitialized slot entry and segfault. AGE now null-inits the
* entity slot and computes stored generated columns before materializing the
* tuple. Generated columns must be computed on insert and recomputed on update;
* plain user columns must default to NULL rather than crash.
*/

LOAD 'age';
SET search_path TO ag_catalog;

SELECT create_graph('generated_columns');

--
-- GENERATED ALWAYS ... STORED column on a vertex label
--
SELECT create_vlabel('generated_columns', 'Product');
ALTER TABLE generated_columns."Product"
ADD COLUMN category varchar(25)
GENERATED ALWAYS AS (agtype_access_operator(properties, '"category"')) STORED;

-- CREATE: the generated column is computed from the new properties
SELECT * FROM cypher('generated_columns', $$
CREATE (p:Product {category: 'disk', type: 'M1234'}) RETURN p
$$) AS (p agtype);

-- CREATE where the generation expression yields NULL (no such key)
SELECT * FROM cypher('generated_columns', $$
CREATE (p:Product {type: 'no-cat'}) RETURN p
$$) AS (p agtype);

-- MERGE also computes the generated column
SELECT * FROM cypher('generated_columns', $$
MERGE (p:Product {category: 'merged'}) RETURN p
$$) AS (p agtype);

-- MERGE again on the same pattern must MATCH (not re-insert); this exercises the
-- matched path where the slot is intentionally left uncleared (issue #2450)
SELECT * FROM cypher('generated_columns', $$
MERGE (p:Product {category: 'merged'}) RETURN p
$$) AS (p agtype);

-- SET recomputes the generated column from the updated properties
SELECT * FROM cypher('generated_columns', $$
MATCH (p:Product {category: 'disk'}) SET p.category = 'ssd', p.note = 'x' RETURN p
$$) AS (p agtype);

-- The stored generated column always mirrors properties.category
SELECT category, properties FROM generated_columns."Product" ORDER BY category NULLS LAST;

--
-- GENERATED ALWAYS ... STORED column on an edge label
--
SELECT create_elabel('generated_columns', 'REL');
ALTER TABLE generated_columns."REL"
ADD COLUMN kind varchar(25)
GENERATED ALWAYS AS (agtype_access_operator(properties, '"kind"')) STORED;

SELECT * FROM cypher('generated_columns', $$
CREATE (:Product {category: 'a'})-[r:REL {kind: 'link'}]->(:Product {category: 'b'})
RETURN r
$$) AS (r agtype);

SELECT kind FROM generated_columns."REL";

--
-- Plain (non-generated) user column: must not crash; defaults to NULL
--
SELECT create_vlabel('generated_columns', 'Plain');
ALTER TABLE generated_columns."Plain" ADD COLUMN tag text;

SELECT * FROM cypher('generated_columns', $$
CREATE (p:Plain {n: 1}) RETURN p
$$) AS (p agtype);

SELECT * FROM cypher('generated_columns', $$
MATCH (p:Plain) SET p.n = 2 RETURN p
$$) AS (p agtype);

SELECT properties, tag FROM generated_columns."Plain";

--
-- Generated column referencing the tableoid system column: the entity slot must
-- carry tts_tableOid before the generated columns are computed, otherwise the
-- expression sees InvalidOid instead of the label table's OID (issue #2450).
--
SELECT create_vlabel('generated_columns', 'T');
ALTER TABLE generated_columns."T"
ADD COLUMN tbl oid GENERATED ALWAYS AS (tableoid) STORED;

SELECT * FROM cypher('generated_columns', $$
CREATE (n:T {k: 1}) RETURN n
$$) AS (n agtype);

SELECT * FROM cypher('generated_columns', $$
MATCH (n:T) SET n.k = 2 RETURN n
$$) AS (n agtype);

-- tbl must equal the real label-table OID on both the CREATE and SET paths
SELECT tbl = 'generated_columns."T"'::regclass::oid AS tableoid_ok
FROM generated_columns."T";

--
-- Cleanup
--
SELECT drop_graph('generated_columns', true);
4 changes: 2 additions & 2 deletions src/backend/executor/cypher_create.c
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ static void create_edge(cypher_create_custom_scan_state *css,

estate->es_result_relations = &resultRelInfo;

ExecClearTuple(elemTupleSlot);
clear_entity_slot(elemTupleSlot);

/* Graph Id for the edge */
id = ExecEvalExpr(node->id_expr_state, econtext, &isNull);
Expand Down Expand Up @@ -491,7 +491,7 @@ static Datum create_vertex(cypher_create_custom_scan_state *css,

estate->es_result_relations = &resultRelInfo;

ExecClearTuple(elemTupleSlot);
clear_entity_slot(elemTupleSlot);

/* get the next graphid for this vertex. */
id = ExecEvalExpr(node->id_expr_state, econtext, &isNull);
Expand Down
Loading
Loading