diff --git a/vector/CMakeLists.txt b/vector/CMakeLists.txt index 4b26761806f..6ec8dcb526f 100644 --- a/vector/CMakeLists.txt +++ b/vector/CMakeLists.txt @@ -180,7 +180,8 @@ build_program_in_subdir( grass_dbmiclient grass_dbmidriver grass_gis - grass_vector) + grass_vector + grass_parson) build_program_in_subdir( v.db.select diff --git a/vector/v.db.connect/Makefile b/vector/v.db.connect/Makefile index b4599ed83a5..076e5827052 100644 --- a/vector/v.db.connect/Makefile +++ b/vector/v.db.connect/Makefile @@ -3,7 +3,7 @@ MODULE_TOPDIR = ../.. PGM=v.db.connect -LIBES = $(VECTORLIB) $(DBMILIB) $(GISLIB) +LIBES = $(VECTORLIB) $(DBMILIB) $(GISLIB) $(PARSONLIB) DEPENDENCIES = $(VECTORDEP) $(DBMIDEP) $(GISDEP) EXTRA_INC = $(VECT_INC) EXTRA_CFLAGS = $(VECT_CFLAGS) diff --git a/vector/v.db.connect/main.c b/vector/v.db.connect/main.c index 0d6c79485f3..f6be0d3d088 100644 --- a/vector/v.db.connect/main.c +++ b/vector/v.db.connect/main.c @@ -22,10 +22,13 @@ #include #include #include +#include #include #include #include +enum OutputFormat { PLAIN, CSV, JSON }; + int main(int argc, char **argv) { char *input; @@ -33,8 +36,8 @@ int main(int argc, char **argv) struct GModule *module; struct Option *inopt, *dbdriver, *dbdatabase, *dbtable, *field_opt, *dbkey, - *sep_opt; - struct Flag *print, *columns, *delete, *shell_print; + *sep_opt, *format_opt; + struct Flag *print, *columns, *delete, *csv_print; dbDriver *driver; dbString table_name; dbTable *table; @@ -44,6 +47,11 @@ int main(int argc, char **argv) char *fieldname; struct Map_info Map; char *sep; + enum OutputFormat format; + JSON_Value *root_value = NULL, *conn_value = NULL; + JSON_Array *root_array = NULL; + JSON_Object *conn_object = NULL; + int skip_header = 0; /* set up the options and flags for the command line parser */ @@ -79,21 +87,31 @@ int main(int argc, char **argv) field_opt->gisprompt = "new,layer,layer"; sep_opt = G_define_standard_option(G_OPT_F_SEP); - sep_opt->label = _("Field separator for shell script style output"); + sep_opt->label = _("Field separator for printing output"); sep_opt->guisection = _("Print"); + format_opt = G_define_standard_option(G_OPT_F_FORMAT); + format_opt->options = "plain,csv,json"; + format_opt->required = NO; + format_opt->answer = NULL; + format_opt->descriptions = ("plain;Human readable text output;" + "csv;CSV (Comma Separated Values);" + "json;JSON (JavaScript Object Notation);"); + print = G_define_flag(); print->key = 'p'; print->description = _("Print all map connection parameters and exit"); print->guisection = _("Print"); - shell_print = G_define_flag(); - shell_print->key = 'g'; - shell_print->label = - _("Print all map connection parameters in shell script style and exit"); - shell_print->description = - _("Format: layer[/layer name] table key database driver"); - shell_print->guisection = _("Print"); + csv_print = G_define_flag(); + csv_print->key = 'g'; + csv_print->label = _( + "Print all map connection parameters in a legacy format [deprecated]"); + csv_print->description = _( + "Order: layer[/layer name] table key database driver" + "This flag is deprecated and will be removed in a future release. Use " + "format=csv instead."); + csv_print->guisection = _("Print"); columns = G_define_flag(); columns->key = 'c'; @@ -111,6 +129,47 @@ int main(int argc, char **argv) if (G_parser(argc, argv)) exit(EXIT_FAILURE); + // If no format option is specified, preserve backward compatibility + if (format_opt->answer == NULL || format_opt->answer[0] == '\0') { + if (csv_print->answer || columns->answer) { + format_opt->answer = "csv"; + skip_header = 1; + } + else + format_opt->answer = "plain"; + } + + if (strcmp(format_opt->answer, "json") == 0) { + format = JSON; + root_value = G_json_value_init_array(); + if (root_value == NULL) { + G_fatal_error(_("Failed to initialize JSON array. Out of memory?")); + } + root_array = G_json_array(root_value); + } + else if (strcmp(format_opt->answer, "csv") == 0) { + format = CSV; + } + else { + format = PLAIN; + } + + if (format != PLAIN && !print->answer && !csv_print->answer && + !columns->answer) { + G_fatal_error( + _("The -p or -c flag is required when using the format option.")); + } + + if (csv_print->answer) { + G_verbose_message( + _("Flag 'g' is deprecated and will be removed in a future " + "release. Please use format=csv instead.")); + if (format == JSON) { + G_fatal_error(_("The -g flag cannot be used with format=json. " + "Please select only one output format.")); + } + } + /* The check must allow '.' in the name (schema.table) */ /* if (dbtable->answer) { @@ -139,11 +198,11 @@ int main(int argc, char **argv) sep = G_option_to_separator(sep_opt); - if (print->answer && shell_print->answer) + if (print->answer && csv_print->answer) G_fatal_error(_("Please choose only one print style")); Vect_set_open_level(1); /* no topology needed */ - if (print->answer || shell_print->answer || columns->answer) { + if (print->answer || csv_print->answer || columns->answer) { if (Vect_open_old2(&Map, inopt->answer, "", field_opt->answer) < 0) G_fatal_error(_("Unable to open vector map <%s>"), inopt->answer); } @@ -154,7 +213,7 @@ int main(int argc, char **argv) Vect_hist_command(&Map); } - if (print->answer || shell_print->answer || columns->answer) { + if (print->answer || csv_print->answer || columns->answer) { num_dblinks = Vect_get_num_dblinks(&Map); if (num_dblinks <= 0) { /* it is ok if a vector map is not connected o an attribute table */ @@ -164,27 +223,51 @@ int main(int argc, char **argv) } else { /* num_dblinks > 0 */ - if (print->answer || shell_print->answer) { - if (!(shell_print->answer)) { + if (print->answer || csv_print->answer) { + if (format == PLAIN) { fprintf(stdout, _("Vector map <%s> is connected by:\n"), input); } + if (!skip_header && format == CSV) { + /* CSV Header */ + fprintf(stdout, "%s%s%s%s%s%s%s%s%s%s%s\n", "layer", sep, + "layer_name", sep, "table", sep, "key", sep, + "database", sep, "driver"); + } for (i = 0; i < num_dblinks; i++) { if ((fi = Vect_get_dblink(&Map, i)) == NULL) G_fatal_error(_("Database connection not defined")); - if (shell_print->answer) { - if (fi->name) - fprintf(stdout, "%d/%s%s%s%s%s%s%s%s%s\n", - fi->number, fi->name, sep, fi->table, sep, - fi->key, sep, fi->database, sep, - fi->driver); - else - fprintf(stdout, "%d%s%s%s%s%s%s%s%s\n", fi->number, - sep, fi->table, sep, fi->key, sep, - fi->database, sep, fi->driver); - } - else { + switch (format) { + case CSV: + if (skip_header) { + /* For Backward compatibility */ + if (fi->name) + fprintf(stdout, "%d/%s%s%s%s%s%s%s%s%s\n", + fi->number, fi->name, sep, fi->table, + sep, fi->key, sep, fi->database, sep, + fi->driver); + else + fprintf(stdout, "%d%s%s%s%s%s%s%s%s\n", + fi->number, sep, fi->table, sep, + fi->key, sep, fi->database, sep, + fi->driver); + } + else { + if (fi->name) + fprintf(stdout, "%d%s%s%s%s%s%s%s%s%s%s\n", + fi->number, sep, fi->name, sep, + fi->table, sep, fi->key, sep, + fi->database, sep, fi->driver); + else + fprintf(stdout, "%d%s%s%s%s%s%s%s%s%s%s\n", + fi->number, sep, "", sep, fi->table, + sep, fi->key, sep, fi->database, sep, + fi->driver); + } + break; + + case PLAIN: if (fi->name) { fprintf(stdout, _("layer <%d/%s> table <%s> in database " @@ -201,6 +284,34 @@ int main(int argc, char **argv) fi->number, fi->table, fi->database, fi->driver, fi->key); } + break; + + case JSON: + conn_value = G_json_value_init_object(); + if (conn_value == NULL) { + G_fatal_error(_("Failed to initialize JSON object. " + "Out of memory?")); + } + conn_object = G_json_object(conn_value); + + G_json_object_set_number(conn_object, "layer", + fi->number); + if (fi->name) + G_json_object_set_string(conn_object, "layer_name", + fi->name); + else + G_json_object_set_null(conn_object, "layer_name"); + + G_json_object_set_string(conn_object, "table", + fi->table); + G_json_object_set_string(conn_object, "key", fi->key); + G_json_object_set_string(conn_object, "database", + fi->database); + G_json_object_set_string(conn_object, "driver", + fi->driver); + + G_json_array_append_value(root_array, conn_value); + break; } } } /* end print */ @@ -229,18 +340,70 @@ int main(int argc, char **argv) G_fatal_error(_("Unable to describe table <%s>"), fi->table); + if (!skip_header && format != JSON) { + /* CSV Header */ + fprintf(stdout, "%s|%s\n", "sql_type", "name"); + } + ncols = db_get_table_number_of_columns(table); for (col = 0; col < ncols; col++) { - fprintf( - stdout, "%s|%s\n", - db_sqltype_name(db_get_column_sqltype( - db_get_table_column(table, col))), - db_get_column_name(db_get_table_column(table, col))); + switch (format) { + case PLAIN: + case CSV: + fprintf(stdout, "%s|%s\n", + db_sqltype_name(db_get_column_sqltype( + db_get_table_column(table, col))), + db_get_column_name( + db_get_table_column(table, col))); + break; + + case JSON: + conn_value = G_json_value_init_object(); + if (conn_value == NULL) { + G_fatal_error(_("Failed to initialize JSON object. " + "Out of memory?")); + } + conn_object = G_json_object(conn_value); + + G_json_object_set_string( + conn_object, "name", + db_get_column_name( + db_get_table_column(table, col))); + + int sql_type = db_get_column_sqltype( + db_get_table_column(table, col)); + G_json_object_set_string(conn_object, "sql_type", + db_sqltype_name(sql_type)); + + int c_type = db_sqltype_to_Ctype(sql_type); + G_json_object_set_boolean(conn_object, "is_number", + (c_type == DB_C_TYPE_INT || + c_type == DB_C_TYPE_DOUBLE)); + + G_json_array_append_value(root_array, conn_value); + break; + } } db_close_database(driver); db_shutdown_driver(driver); } + + if (format == JSON) { + char *json_string = + G_json_serialize_to_string_pretty(root_value); + if (!json_string) { + G_json_value_free(root_value); + G_fatal_error( + _("Failed to serialize JSON to pretty format.")); + } + + puts(json_string); + + G_json_free_serialized_string(json_string); + G_json_value_free(root_value); + } + } /* end else num_dblinks */ } /* end print/columns */ else { /* define new dbln settings or delete */ diff --git a/vector/v.db.connect/testsuite/test_v_db_connect.py b/vector/v.db.connect/testsuite/test_v_db_connect.py index 3c0e5a4fd07..ebdbd30eec5 100644 --- a/vector/v.db.connect/testsuite/test_v_db_connect.py +++ b/vector/v.db.connect/testsuite/test_v_db_connect.py @@ -1,6 +1,6 @@ from grass.gunittest.case import TestCase from grass.gunittest.main import test -from grass.script.core import read_command +from grass.script.core import read_command, parse_command class TestVDbConnect(TestCase): @@ -9,7 +9,7 @@ def setUpClass(cls): cls.runModule("db.connect", flags="c") def test_plain_output(self): - """Test default plain text output""" + """Test default and explicit plain text outputs""" actual = read_command("v.db.connect", map="bridges", flags="p").splitlines() self.assertEqual(len(actual), 2) self.assertEqual(actual[0], "Vector map is connected by:") @@ -19,14 +19,46 @@ def test_plain_output(self): r"layer <1\/bridges> table in database <.+sqlite\.db> through driver with key ", ) + # Repeat check using explicit plain format + actual_plain = read_command( + "v.db.connect", map="bridges", flags="p", format="plain" + ).splitlines() + self.assertEqual(actual_plain, actual) + def test_csv_output(self): """Test -g flag CSV output""" actual = read_command("v.db.connect", map="bridges", flags="g") # Since the database path is system-dependent, we verify it using a regular expression self.assertRegex(actual, r"1\/bridges\|bridges\|cat\|.+sqlite\.db\|sqlite") + # Repeat check using explicit CSV format + actual = read_command( + "v.db.connect", map="bridges", flags="p", format="csv" + ).splitlines() + self.assertEqual(len(actual), 2) + self.assertEqual(actual[0], "layer|layer_name|table|key|database|driver") + self.assertRegex(actual[1], r"1\|bridges\|bridges\|cat\|.+sqlite\.db\|sqlite") + + # Repeat check using explicit CSV format and flag -g + actual_csv = read_command( + "v.db.connect", map="bridges", flags="g", format="csv" + ).splitlines() + self.assertEqual(actual_csv, actual) + + def test_json_output(self): + """Test JSON output fields and values""" + actual = parse_command("v.db.connect", map="bridges", flags="p", format="json") + self.assertEqual(len(actual), 1) + self.assertEqual(actual[0]["driver"], "sqlite") + self.assertEqual(actual[0]["key"], "cat") + self.assertEqual(actual[0]["layer"], 1) + self.assertEqual(actual[0]["layer_name"], "bridges") + self.assertEqual(actual[0]["table"], "bridges") + # Since the database path is system-dependent, we verify it using a regular expression + self.assertRegex(actual[0]["database"], r".+sqlite\.db") + def test_columns_csv(self): - """Test -c flag CSV output""" + """Test -c flag with CSV and plain formats""" expected = [ "INTEGER|cat", "INTEGER|OBJECTID", @@ -43,9 +75,47 @@ def test_columns_csv(self): "DOUBLE PRECISION|CO_", "CHARACTER|CO_NAME", ] + + # Check using -c flag actual = read_command("v.db.connect", map="bridges", flags="c").splitlines() self.assertEqual(actual, expected) + # Prepend the CSV header + expected.insert(0, "sql_type|name") + + # Repeat check using explicit plain format + actual = read_command( + "v.db.connect", map="bridges", flags="c", format="plain" + ).splitlines() + self.assertEqual(actual, expected) + + # Repeat check using explicit CSV format + actual = read_command( + "v.db.connect", map="bridges", flags="c", format="csv" + ).splitlines() + self.assertEqual(actual, expected) + + def test_columns_json(self): + """Test -c flag with JSON format""" + actual = parse_command("v.db.connect", map="bridges", flags="c", format="json") + expected = [ + {"name": "cat", "sql_type": "INTEGER", "is_number": True}, + {"name": "OBJECTID", "sql_type": "INTEGER", "is_number": True}, + {"name": "BRIDGES__1", "sql_type": "DOUBLE PRECISION", "is_number": True}, + {"name": "SIPS_ID", "sql_type": "CHARACTER", "is_number": False}, + {"name": "TYPE", "sql_type": "CHARACTER", "is_number": False}, + {"name": "CLASSIFICA", "sql_type": "CHARACTER", "is_number": False}, + {"name": "BRIDGE_NUM", "sql_type": "DOUBLE PRECISION", "is_number": True}, + {"name": "FEATURE_IN", "sql_type": "CHARACTER", "is_number": False}, + {"name": "FACILITY_C", "sql_type": "CHARACTER", "is_number": False}, + {"name": "LOCATION", "sql_type": "CHARACTER", "is_number": False}, + {"name": "YEAR_BUILT", "sql_type": "DOUBLE PRECISION", "is_number": True}, + {"name": "WIDTH", "sql_type": "DOUBLE PRECISION", "is_number": True}, + {"name": "CO_", "sql_type": "DOUBLE PRECISION", "is_number": True}, + {"name": "CO_NAME", "sql_type": "CHARACTER", "is_number": False}, + ] + self.assertEqual(actual, expected) + if __name__ == "__main__": test() diff --git a/vector/v.db.connect/v.db.connect.md b/vector/v.db.connect/v.db.connect.md index 6b79d8def73..e20e172ca4d 100644 --- a/vector/v.db.connect/v.db.connect.md +++ b/vector/v.db.connect/v.db.connect.md @@ -25,6 +25,9 @@ to your map, it is advisable to make a copy from those tables first and connect the copied tables to the vector map (see also [v.overlay](v.overlay.md)). +The **-g** flag is deprecated and will be removed in a future release. Please +use **format=csv** option with the **-p** flag instead. + ## EXAMPLE Note: The default database backend setting is SQLite. @@ -43,6 +46,38 @@ Print column types and names of table linked to vector map. v.db.connect -c map=roads ``` +### Print database connection using Python + +Print all database connection parameters for vector map. + +```python +import grass.script as gs + +data = gs.parse_command("v.db.connect", map="roadsmajor", flags="p", format="json") +print(data) +``` + +Possible output: + +```text +[{'layer': 1, 'layer_name': 'roadsmajor', 'table': 'roadsmajor', 'key': 'cat', 'database': '/grassdata/nc_spm_08_grass7/PERMANENT/sqlite/sqlite.db', 'driver': 'sqlite'}] +``` + +Print column types and names of table linked to vector map. + +```python +import grass.script as gs + +data = gs.parse_command("v.db.connect", map="roadsmajor", flags="c", format="json") +print(data) +``` + +Possible output: + +```text +[{'name': 'cat', 'sql_type': 'INTEGER', 'is_number': True}, {'name': 'MAJORRDS_', 'sql_type': 'DOUBLE PRECISION', 'is_number': True}, {'name': 'ROAD_NAME', 'sql_type': 'CHARACTER', 'is_number': False}, {'name': 'MULTILANE', 'sql_type': 'CHARACTER', 'is_number': False}, {'name': 'PROPYEAR', 'sql_type': 'INTEGER', 'is_number': True}, {'name': 'OBJECTID', 'sql_type': 'INTEGER', 'is_number': True}, {'name': 'SHAPE_LEN', 'sql_type': 'DOUBLE PRECISION', 'is_number': True}] +``` + ### Connect vector map to database (DBF driver) Connect vector map to DBF table without or with variables.