From 543092a78880128fabc6047ec9589a2fa47aba18 Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Tue, 18 Jul 2023 09:38:15 -0400 Subject: [PATCH 1/2] v.db.select: Add column names and types to JSON The JSON output now has column names and types under info/columns. The example in documentation does not include the unclear note about booleans (perhaps a discussion for another place). A command to reproduce the example output is now included. Documentation and test of the new functionality are included. --- vector/v.db.select/main.c | 28 ++++++++++- .../testsuite/test_v_db_select_json_csv.py | 17 ++++++- vector/v.db.select/v.db.select.html | 46 ++++++++++++++++--- 3 files changed, 81 insertions(+), 10 deletions(-) diff --git a/vector/v.db.select/main.c b/vector/v.db.select/main.c index 18e11a81990..4b848e02e0e 100644 --- a/vector/v.db.select/main.c +++ b/vector/v.db.select/main.c @@ -337,8 +337,32 @@ int main(int argc, char **argv) if (format == JSON) { if (flags.region->answer) fprintf(stdout, "{\"extent\":\n"); - else - fprintf(stdout, "{\"records\":[\n"); + else { + fprintf(stdout, "{\"info\":\n{\"columns\":[\n"); + for (col = 0; col < ncols; col++) { + column = db_get_table_column(table, col); + if (col) + fprintf(stdout, "},\n"); + fprintf(stdout, "{\"name\": \"%s\",", + db_get_column_name(column)); + int sql_type = db_get_column_sqltype(column); + fprintf(stdout, "\"sql_type\": \"%s\",", + db_sqltype_name(sql_type)); + + int c_type = db_sqltype_to_Ctype(sql_type); + fprintf(stdout, "\"is_number\":"); + /* Same rules as for quoting, i.e., number only as + * JSON or Python would see it and not numeric which may + * include, e.g., date. */ + if (c_type == DB_C_TYPE_INT || c_type == DB_C_TYPE_DOUBLE) + fprintf(stdout, "true"); + else + fprintf(stdout, "false"); + } + + fprintf(stdout, "}\n]},\n"); + fprintf(stdout, "\"records\":[\n"); + } } /* fetch the data */ diff --git a/vector/v.db.select/testsuite/test_v_db_select_json_csv.py b/vector/v.db.select/testsuite/test_v_db_select_json_csv.py index fc517cbfcde..07e008a23b5 100644 --- a/vector/v.db.select/testsuite/test_v_db_select_json_csv.py +++ b/vector/v.db.select/testsuite/test_v_db_select_json_csv.py @@ -3,7 +3,7 @@ # MODULE: Test of v.db.select # AUTHOR(S): Vaclav Petras # PURPOSE: Test parsing and structure of CSV and JSON outputs -# COPYRIGHT: (C) 2021 by Vaclav Petras the GRASS Development Team +# COPYRIGHT: (C) 2021-2023 by Vaclav Petras the GRASS Development Team # # This program is free software under the GNU General Public # License (>=v2). Read the file COPYING that comes with GRASS @@ -173,6 +173,21 @@ def test_json_loads(self): """Load JSON with difficult values""" text = gs.read_command("v.db.select", map=self.vector_points, format="json") data = json.loads(text) + + column_info = data["info"]["columns"] + self.assertEqual(column_info[0]["name"], "cat") + self.assertEqual(column_info[0]["sql_type"], "INTEGER") + self.assertEqual(column_info[0]["is_number"], True) + self.assertEqual(column_info[1]["name"], "x") + self.assertEqual(column_info[1]["sql_type"], "DOUBLE PRECISION") + self.assertEqual(column_info[1]["is_number"], True) + self.assertEqual(column_info[4]["name"], "owner_id") + self.assertEqual(column_info[4]["sql_type"], "INTEGER") + self.assertEqual(column_info[4]["is_number"], True) + self.assertEqual(column_info[5]["name"], "place_name") + self.assertEqual(column_info[5]["sql_type"], "TEXT") + self.assertEqual(column_info[5]["is_number"], False) + data = data["records"] self.assertIsNone(data[2]["place_name"]) self.assertEqual(data[3]["place_name"], 'The "Great" Place') diff --git a/vector/v.db.select/v.db.select.html b/vector/v.db.select/v.db.select.html index ac59d80a435..e75beb5336b 100644 --- a/vector/v.db.select/v.db.select.html +++ b/vector/v.db.select/v.db.select.html @@ -105,24 +105,56 @@

JSON

the columns were defined in the database.

-Example with added indentation (note that booleans are not directly supported; -here, an attribute is a string with value no): +The JSON also contains information about columns stored under key +info. Column names and types are under key columns. +Each colum has SQL data type under sql_type in all caps. +A boolean is_number specifies whether the value is a number, i.e., +integer or floating point number. The is_number value +is aded for convenience and it is recommended to rely on the types derived +from the JSON representation or the SQL types. The definition of +is_number may change in the future. + +

+Example with added indentation: + +

 {
+  "info": {
+    "columns": [
+      {
+        "name": "road_name",
+        "sql_type": "CHARACTER",
+        "is_number": false
+      },
+      {
+        "name": "year",
+        "sql_type": "INTEGER",
+        "is_number": true
+      },
+      {
+        "name": "length",
+        "sql_type": "DOUBLE PRECISION",
+        "is_number": true
+      }
+    ]
+  },
   "records": [
     {
-      "cat": 1,
       "road_name": "NC-50",
-      "multilane": "no",
       "year": 2001,
       "length": 4825.369405
     },
     {
-      "cat": 2,
       "road_name": "NC-50",
-      "multilane": "no",
-      "year": 2002,
+      "year": 2001,
       "length": 14392.589058
     }
   ]

From 6cde61e8499fd118bbf4b51d8817083989c39365 Mon Sep 17 00:00:00 2001
From: Vaclav Petras 
Date: Tue, 18 Jul 2023 11:18:54 -0400
Subject: [PATCH 2/2] Spaces are not generated after keys as in records. Fix
 string-based test.

---
 vector/v.db.select/main.c                        |  4 ++--
 vector/v.db.select/testsuite/test_v_db_select.py | 14 +++++++++++++-
 2 files changed, 15 insertions(+), 3 deletions(-)

diff --git a/vector/v.db.select/main.c b/vector/v.db.select/main.c
index 4b848e02e0e..cf10332fdf6 100644
--- a/vector/v.db.select/main.c
+++ b/vector/v.db.select/main.c
@@ -343,10 +343,10 @@ int main(int argc, char **argv)
                 column = db_get_table_column(table, col);
                 if (col)
                     fprintf(stdout, "},\n");
-                fprintf(stdout, "{\"name\": \"%s\",",
+                fprintf(stdout, "{\"name\":\"%s\",",
                         db_get_column_name(column));
                 int sql_type = db_get_column_sqltype(column);
-                fprintf(stdout, "\"sql_type\": \"%s\",",
+                fprintf(stdout, "\"sql_type\":\"%s\",",
                         db_sqltype_name(sql_type));
 
                 int c_type = db_sqltype_to_Ctype(sql_type);
diff --git a/vector/v.db.select/testsuite/test_v_db_select.py b/vector/v.db.select/testsuite/test_v_db_select.py
index 998f1d21432..7591a2c3ae7 100644
--- a/vector/v.db.select/testsuite/test_v_db_select.py
+++ b/vector/v.db.select/testsuite/test_v_db_select.py
@@ -163,7 +163,19 @@
 1290,63600420,109186.835938,1291,1290,Zwe,63600422.4739,109186.832069
 """
 
-out_json = """{"records":[
+out_json = """\
+{"info":
+{"columns":[
+{"name":"cat","sql_type":"INTEGER","is_number":true},
+{"name":"onemap_pro","sql_type":"DOUBLE PRECISION","is_number":true},
+{"name":"PERIMETER","sql_type":"DOUBLE PRECISION","is_number":true},
+{"name":"GEOL250_","sql_type":"INTEGER","is_number":true},
+{"name":"GEOL250_ID","sql_type":"INTEGER","is_number":true},
+{"name":"GEO_NAME","sql_type":"CHARACTER","is_number":false},
+{"name":"SHAPE_area","sql_type":"DOUBLE PRECISION","is_number":true},
+{"name":"SHAPE_len","sql_type":"DOUBLE PRECISION","is_number":true}
+]},
+"records":[
 {"cat":1,"onemap_pro":963738.75,"PERIMETER":4083.97998,"GEOL250_":2,"GEOL250_ID":1,"GEO_NAME":"Zml","SHAPE_area":963738.608571,"SHAPE_len":4083.979839},
 {"cat":2,"onemap_pro":22189124,"PERIMETER":26628.261719,"GEOL250_":3,"GEOL250_ID":2,"GEO_NAME":"Zmf","SHAPE_area":22189123.2296,"SHAPE_len":26628.261112},
 {"cat":3,"onemap_pro":579286.875,"PERIMETER":3335.55835,"GEOL250_":4,"GEOL250_ID":3,"GEO_NAME":"Zml","SHAPE_area":579286.829631,"SHAPE_len":3335.557182},