This is an automated email from the ASF dual-hosted git repository.
MuhammadTahaNaveed pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/age.git
The following commit(s) were added to refs/heads/master by this push:
new 80141740 Add delimiter parameter to load_labels_from_file and
load_edges_from_file (#2469)
80141740 is described below
commit 801417404978823bd8732452c3f7959017584785
Author: mmustafasenoglu <[email protected]>
AuthorDate: Fri Jul 17 12:42:01 2026 +0300
Add delimiter parameter to load_labels_from_file and load_edges_from_file
(#2469)
Both load_labels_from_file and load_edges_from_file relied on PostgreSQL
COPY's default comma delimiter, causing silent data corruption on labels
and segfault on edges when using non-comma-delimited files.
This adds an optional 'delimiter' TEXT parameter (default ',') to both
functions, allowing users to specify the field separator:
SELECT load_labels_from_file('graph', 'Label', 'file.csv', true, false,
'|');
SELECT load_edges_from_file('graph', 'Edge', 'file.csv', false, '|');
Changes:
- create_copy_options() now accepts and passes the delimiter to COPY
- SQL function signatures updated with new delimiter parameter
- Regression tests added for pipe-delimited vertex and edge files
Add DROP/CREATE for load_labels_from_file and load_edges_from_file
with the new delimiter parameter to age--1.8.0--y.y.y.sql. Without
these definitions, the age_upgrade regression test fails because
the old function signatures remain after upgrade.
Signed-off-by: Mustafa Senoglu <[email protected]>
---
age--1.8.0--y.y.y.sql | 34 ++++++++++++++++-
regress/age_load/data/pipe_edges.csv | 3 ++
regress/age_load/data/pipe_vertices.csv | 4 ++
regress/expected/age_load.out | 66 +++++++++++++++++++++++++++++++++
regress/sql/age_load.sql | 19 ++++++++++
sql/age_main.sql | 6 ++-
src/backend/utils/load/ag_load_edges.c | 18 +++++++--
src/backend/utils/load/ag_load_labels.c | 18 +++++++--
src/backend/utils/load/age_load.c | 41 +++++++++++++++++++-
src/include/utils/load/ag_load_edges.h | 2 +-
src/include/utils/load/ag_load_labels.h | 3 +-
11 files changed, 201 insertions(+), 13 deletions(-)
diff --git a/age--1.8.0--y.y.y.sql b/age--1.8.0--y.y.y.sql
index 4c85b28c..097d066e 100644
--- a/age--1.8.0--y.y.y.sql
+++ b/age--1.8.0--y.y.y.sql
@@ -22,7 +22,7 @@
--* This is a TEMPLATE for upgrading from the previous version of Apache AGE
--* Please adjust the below ALTER EXTENSION to reflect the -- correct version
it
---* is upgrading to.
+-- is upgrading to.
-- This will only work within a major version of PostgreSQL, not across
-- major versions.
@@ -33,3 +33,35 @@
--* Please add all additions, deletions, and modifications to the end of this
--* file. We need to keep the order of these changes.
--* REMOVE ALL LINES ABOVE, and this one, that start with --*
+
+--
+-- Add delimiter parameter to load_labels_from_file and load_edges_from_file
+--
+-- Issue #2449: Both load_labels_from_file and load_edges_from_file now accept
+-- an optional delimiter parameter (default ',') to support non-CSV delimiters
+-- such as pipe-delimited files.
+
+-- Drop and recreate load_labels_from_file with new delimiter parameter
+DROP FUNCTION IF EXISTS ag_catalog.load_labels_from_file(name, name, text,
bool, bool);
+
+CREATE FUNCTION ag_catalog.load_labels_from_file(graph_name name,
+ label_name name,
+ file_path text,
+ id_field_exists bool default
true,
+ load_as_agtype bool default
false,
+ delimiter text default ',')
+ RETURNS void
+ LANGUAGE c
+ AS 'MODULE_PATHNAME';
+
+-- Drop and recreate load_edges_from_file with new delimiter parameter
+DROP FUNCTION IF EXISTS ag_catalog.load_edges_from_file(name, name, text,
bool);
+
+CREATE FUNCTION ag_catalog.load_edges_from_file(graph_name name,
+ label_name name,
+ file_path text,
+ load_as_agtype bool default
false,
+ delimiter text default ',')
+ RETURNS void
+ LANGUAGE c
+ AS 'MODULE_PATHNAME';
diff --git a/regress/age_load/data/pipe_edges.csv
b/regress/age_load/data/pipe_edges.csv
new file mode 100644
index 00000000..776fdf72
--- /dev/null
+++ b/regress/age_load/data/pipe_edges.csv
@@ -0,0 +1,3 @@
+start_id|start_vertex_type|end_id|end_vertex_type|distance
+1|City|2|City|2500
+2|City|3|City|1050
diff --git a/regress/age_load/data/pipe_vertices.csv
b/regress/age_load/data/pipe_vertices.csv
new file mode 100644
index 00000000..48e6d1e2
--- /dev/null
+++ b/regress/age_load/data/pipe_vertices.csv
@@ -0,0 +1,4 @@
+id|name|country
+1|Moscow|Russia
+2|Berlin|Germany
+3|Paris|France
diff --git a/regress/expected/age_load.out b/regress/expected/age_load.out
index 17c5ecc2..01afcb64 100644
--- a/regress/expected/age_load.out
+++ b/regress/expected/age_load.out
@@ -511,6 +511,72 @@ NOTICE: graph "agload_delim" has been dropped
(1 row)
+--
+-- Test delimiter parameter for pipe-delimited files
+--
+SELECT create_graph('agload_pipe');
+NOTICE: graph "agload_pipe" has been created
+ create_graph
+--------------
+
+(1 row)
+
+SELECT create_vlabel('agload_pipe', 'City');
+NOTICE: VLabel "City" has been created
+ create_vlabel
+---------------
+
+(1 row)
+
+SELECT create_elabel('agload_pipe', 'Connected');
+NOTICE: ELabel "Connected" has been created
+ create_elabel
+---------------
+
+(1 row)
+
+-- pipe-delimited vertex file with delimiter parameter
+SELECT load_labels_from_file('agload_pipe', 'City',
'age_load/pipe_vertices.csv', true, false, '|');
+ load_labels_from_file
+-----------------------
+
+(1 row)
+
+-- pipe-delimited edge file with delimiter parameter
+SELECT load_edges_from_file('agload_pipe', 'Connected',
'age_load/pipe_edges.csv', false, '|');
+ load_edges_from_file
+----------------------
+
+(1 row)
+
+-- verify data loaded correctly
+SELECT * FROM cypher('agload_pipe', $$ MATCH (n:City) RETURN n.name, n.country
ORDER BY n.name $$) AS (name agtype, country agtype);
+ name | country
+----------+-----------
+ "Berlin" | "Germany"
+ "Moscow" | "Russia"
+ "Paris" | "France"
+(3 rows)
+
+SELECT * FROM cypher('agload_pipe', $$ MATCH (a:City)-[e:Connected]->(b:City)
RETURN a.name, b.name, e.distance ORDER BY a.name $$) AS (a_name agtype, b_name
agtype, distance agtype);
+ a_name | b_name | distance
+----------+----------+----------
+ "Berlin" | "Paris" | "1050"
+ "Moscow" | "Berlin" | "2500"
+(2 rows)
+
+SELECT drop_graph('agload_pipe', true);
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table agload_pipe._ag_label_vertex
+drop cascades to table agload_pipe._ag_label_edge
+drop cascades to table agload_pipe."City"
+drop cascades to table agload_pipe."Connected"
+NOTICE: graph "agload_pipe" has been dropped
+ drop_graph
+------------
+
+(1 row)
+
--
-- Test security and permissions
--
diff --git a/regress/sql/age_load.sql b/regress/sql/age_load.sql
index 196b0980..3492b57c 100644
--- a/regress/sql/age_load.sql
+++ b/regress/sql/age_load.sql
@@ -223,6 +223,25 @@ SELECT load_labels_from_file('agload_delim', 'V',
'age_load/labels_long_row.csv'
SELECT drop_graph('agload_delim', true);
+--
+-- Test delimiter parameter for pipe-delimited files
+--
+SELECT create_graph('agload_pipe');
+SELECT create_vlabel('agload_pipe', 'City');
+SELECT create_elabel('agload_pipe', 'Connected');
+
+-- pipe-delimited vertex file with delimiter parameter
+SELECT load_labels_from_file('agload_pipe', 'City',
'age_load/pipe_vertices.csv', true, false, '|');
+
+-- pipe-delimited edge file with delimiter parameter
+SELECT load_edges_from_file('agload_pipe', 'Connected',
'age_load/pipe_edges.csv', false, '|');
+
+-- verify data loaded correctly
+SELECT * FROM cypher('agload_pipe', $$ MATCH (n:City) RETURN n.name, n.country
ORDER BY n.name $$) AS (name agtype, country agtype);
+SELECT * FROM cypher('agload_pipe', $$ MATCH (a:City)-[e:Connected]->(b:City)
RETURN a.name, b.name, e.distance ORDER BY a.name $$) AS (a_name agtype, b_name
agtype, distance agtype);
+
+SELECT drop_graph('agload_pipe', true);
+
--
-- Test security and permissions
--
diff --git a/sql/age_main.sql b/sql/age_main.sql
index 233d0d23..5b1044d5 100644
--- a/sql/age_main.sql
+++ b/sql/age_main.sql
@@ -167,7 +167,8 @@ CREATE FUNCTION ag_catalog.load_labels_from_file(graph_name
name,
label_name name,
file_path text,
id_field_exists bool default
true,
- load_as_agtype bool default
false)
+ load_as_agtype bool default
false,
+ delimiter text default ',')
RETURNS void
LANGUAGE c
AS 'MODULE_PATHNAME';
@@ -175,7 +176,8 @@ CREATE FUNCTION ag_catalog.load_labels_from_file(graph_name
name,
CREATE FUNCTION ag_catalog.load_edges_from_file(graph_name name,
label_name name,
file_path text,
- load_as_agtype bool default
false)
+ load_as_agtype bool default
false,
+ delimiter text default ',')
RETURNS void
LANGUAGE c
AS 'MODULE_PATHNAME';
diff --git a/src/backend/utils/load/ag_load_edges.c
b/src/backend/utils/load/ag_load_edges.c
index 01585bab..912547c0 100644
--- a/src/backend/utils/load/ag_load_edges.c
+++ b/src/backend/utils/load/ag_load_edges.c
@@ -134,7 +134,7 @@ static void process_edge_row(char **fields, int nfields,
* Create COPY options for CSV parsing.
* Returns a List of DefElem nodes.
*/
-static List *create_copy_options(void)
+static List *create_copy_options(char delimiter)
{
List *options = NIL;
@@ -150,6 +150,17 @@ static List *create_copy_options(void)
(Node *) makeBoolean(false),
-1));
+ /* DELIMITER */
+ {
+ char delimiter_str[2];
+ delimiter_str[0] = delimiter;
+ delimiter_str[1] = '\0';
+ options = lappend(options,
+ makeDefElem("delimiter",
+ (Node *)
makeString(pstrdup(delimiter_str)),
+ -1));
+ }
+
return options;
}
@@ -161,7 +172,8 @@ int create_edges_from_csv_file(char *file_path,
Oid graph_oid,
char *label_name,
int label_id,
- bool load_as_agtype)
+ bool load_as_agtype,
+ char delimiter)
{
Relation label_rel;
Oid label_relid;
@@ -196,7 +208,7 @@ int create_edges_from_csv_file(char *file_path,
init_batch_insert(&batch_state, label_name, graph_oid);
/* Create COPY options for CSV parsing */
- copy_options = create_copy_options();
+ copy_options = create_copy_options(delimiter);
/* Create a minimal ParseState for BeginCopyFrom */
pstate = make_parsestate(NULL);
diff --git a/src/backend/utils/load/ag_load_labels.c
b/src/backend/utils/load/ag_load_labels.c
index 236d47a1..927c9008 100644
--- a/src/backend/utils/load/ag_load_labels.c
+++ b/src/backend/utils/load/ag_load_labels.c
@@ -120,7 +120,7 @@ static void process_vertex_row(char **fields, int nfields,
* Create COPY options for csv parsing.
* Returns a List of DefElem nodes.
*/
-static List *create_copy_options(void)
+static List *create_copy_options(char delimiter)
{
List *options = NIL;
@@ -136,6 +136,17 @@ static List *create_copy_options(void)
(Node *) makeBoolean(false),
-1));
+ /* DELIMITER */
+ {
+ char delimiter_str[2];
+ delimiter_str[0] = delimiter;
+ delimiter_str[1] = '\0';
+ options = lappend(options,
+ makeDefElem("delimiter",
+ (Node *)
makeString(pstrdup(delimiter_str)),
+ -1));
+ }
+
return options;
}
@@ -148,7 +159,8 @@ int create_labels_from_csv_file(char *file_path,
char *label_name,
int label_id,
bool id_field_exists,
- bool load_as_agtype)
+ bool load_as_agtype,
+ char delimiter)
{
Relation label_rel;
Oid label_relid;
@@ -193,7 +205,7 @@ int create_labels_from_csv_file(char *file_path,
init_batch_insert(&batch_state, label_name, graph_oid);
/* Create COPY options for CSV parsing */
- copy_options = create_copy_options();
+ copy_options = create_copy_options(delimiter);
/* Create a minimal ParseState for BeginCopyFrom */
pstate = make_parsestate(NULL);
diff --git a/src/backend/utils/load/age_load.c
b/src/backend/utils/load/age_load.c
index e4f10d7e..d655ef2e 100644
--- a/src/backend/utils/load/age_load.c
+++ b/src/backend/utils/load/age_load.c
@@ -576,6 +576,7 @@ Datum load_labels_from_file(PG_FUNCTION_ARGS)
int32 label_id;
bool id_field_exists;
bool load_as_agtype;
+ char delimiter;
if (PG_ARGISNULL(0))
{
@@ -604,6 +605,23 @@ Datum load_labels_from_file(PG_FUNCTION_ARGS)
id_field_exists = PG_GETARG_BOOL(3);
load_as_agtype = PG_GETARG_BOOL(4);
+ if (PG_NARGS() > 5 && !PG_ARGISNULL(5))
+ {
+ text *delim_text = PG_GETARG_TEXT_P(5);
+ char *delim_str = text_to_cstring(delim_text);
+ if (strlen(delim_str) != 1)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("delimiter must be a single character")));
+ }
+ delimiter = delim_str[0];
+ }
+ else
+ {
+ delimiter = ',';
+ }
+
graph_name_str = NameStr(*graph_name);
label_name_str = NameStr(*label_name);
@@ -625,7 +643,7 @@ Datum load_labels_from_file(PG_FUNCTION_ARGS)
create_labels_from_csv_file(file_path_str, graph_name_str, graph_oid,
label_name_str, label_id, id_field_exists,
- load_as_agtype);
+ load_as_agtype, delimiter);
free(file_path_str);
@@ -645,6 +663,7 @@ Datum load_edges_from_file(PG_FUNCTION_ARGS)
Oid label_relid;
int32 label_id;
bool load_as_agtype;
+ char delimiter;
if (PG_ARGISNULL(0))
{
@@ -672,6 +691,23 @@ Datum load_edges_from_file(PG_FUNCTION_ARGS)
file_name = PG_GETARG_TEXT_P(2);
load_as_agtype = PG_GETARG_BOOL(3);
+ if (PG_NARGS() > 4 && !PG_ARGISNULL(4))
+ {
+ text *delim_text = PG_GETARG_TEXT_P(4);
+ char *delim_str = text_to_cstring(delim_text);
+ if (strlen(delim_str) != 1)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("delimiter must be a single character")));
+ }
+ delimiter = delim_str[0];
+ }
+ else
+ {
+ delimiter = ',';
+ }
+
graph_name_str = NameStr(*graph_name);
label_name_str = NameStr(*label_name);
@@ -692,7 +728,8 @@ Datum load_edges_from_file(PG_FUNCTION_ARGS)
check_rls_for_load(label_relid);
create_edges_from_csv_file(file_path_str, graph_name_str, graph_oid,
- label_name_str, label_id, load_as_agtype);
+ label_name_str, label_id, load_as_agtype,
+ delimiter);
free(file_path_str);
diff --git a/src/include/utils/load/ag_load_edges.h
b/src/include/utils/load/ag_load_edges.h
index 4db00d93..b2f33648 100644
--- a/src/include/utils/load/ag_load_edges.h
+++ b/src/include/utils/load/ag_load_edges.h
@@ -39,6 +39,6 @@
*/
int create_edges_from_csv_file(char *file_path, char *graph_name, Oid
graph_oid,
char *label_name, int label_id,
- bool load_as_agtype);
+ bool load_as_agtype, char delimiter);
#endif /* AG_LOAD_EDGES_H */
diff --git a/src/include/utils/load/ag_load_labels.h
b/src/include/utils/load/ag_load_labels.h
index c3d517f3..5a9c0246 100644
--- a/src/include/utils/load/ag_load_labels.h
+++ b/src/include/utils/load/ag_load_labels.h
@@ -39,6 +39,7 @@
*/
int create_labels_from_csv_file(char *file_path, char *graph_name, Oid
graph_oid,
char *label_name, int label_id,
- bool id_field_exists, bool load_as_agtype);
+ bool id_field_exists, bool load_as_agtype,
+ char delimiter);
#endif /* AG_LOAD_LABELS_H */