diff --git a/vector/v.db.select/main.c b/vector/v.db.select/main.c index 18e11a81990..cf10332fdf6 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.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}, 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
     }
   ]