mirror of
https://github.com/maplibre/martin.git
synced 2024-12-19 04:41:46 +03:00
Retrieve table/view comments as tilejson (#871)
This commit is contained in:
parent
da05f43048
commit
fa1bf4bdbd
@ -1,3 +1,61 @@
|
|||||||
# Table Sources
|
# Table Sources
|
||||||
|
|
||||||
Table Source is a database table which can be used to query [vector tiles](https://github.com/mapbox/vector-tile-spec). If a [PostgreSQL connection string](pg-connections.md) is given, Martin will publish all tables as data sources if they have at least one geometry column. If geometry column SRID is 0, a default SRID must be set, or else that geo-column/table will be ignored. All non-geometry table columns will be published as vector tile feature tags (properties).
|
Table Source is a database table which can be used to query [vector tiles](https://github.com/mapbox/vector-tile-spec). If a [PostgreSQL connection string](pg-connections.md) is given, Martin will publish all tables as data sources if they have at least one geometry column. If geometry column SRID is 0, a default SRID must be set, or else that geo-column/table will be ignored. All non-geometry table columns will be published as vector tile feature tags (properties).
|
||||||
|
|
||||||
|
# Modifying Tilejson
|
||||||
|
|
||||||
|
Martin will automatically generate a `TileJSON` manifest for each table source. It will contain the `name`, `description`, `minzoom`, `maxzoom`, `bounds` and `vector_layer` information.
|
||||||
|
For example, if there is a table `public.table_source`:
|
||||||
|
the default `TileJSON` might look like this (note that URL will be automatically adjusted to match the request host):
|
||||||
|
|
||||||
|
The table:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE "public"."table_source" ( "gid" int4 NOT NULL, "geom" "public"."geometry" );
|
||||||
|
```
|
||||||
|
|
||||||
|
The TileJSON:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tilejson": "3.0.0",
|
||||||
|
"tiles": [
|
||||||
|
"http://localhost:3000/table_source/{z}/{x}/{y}"
|
||||||
|
],
|
||||||
|
"vector_layers": [
|
||||||
|
{
|
||||||
|
"id": "table_source",
|
||||||
|
"fields": {
|
||||||
|
"gid": "int4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bounds": [
|
||||||
|
-2.0,
|
||||||
|
-1.0,
|
||||||
|
142.84131509869133,
|
||||||
|
45.0
|
||||||
|
],
|
||||||
|
"description": "public.table_source.geom",
|
||||||
|
"name": "table_source"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
By default the `description` and `name` is database identifies about this table, and the bounds is queried from database. You can fine tune these by adjusting `auto_publish` section in [configuration file](https://maplibre.org/martin/config-file.html#config-example).
|
||||||
|
|
||||||
|
## TileJSON in SQL Comments
|
||||||
|
|
||||||
|
Other than adjusting `auto_publish` section in configuration file, you can fine tune the `TileJSON` on the database side directly: Add a valid JSON as an SQL comment on the table.
|
||||||
|
|
||||||
|
Martin will merge table comment into the generated TileJSON using JSON Merge patch. The following example update description and adds attribution, version, foo(even a nested DIY field) fields to the TileJSON.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
DO $do$ BEGIN
|
||||||
|
EXECUTE 'COMMENT ON TABLE table_source IS $tj$' || $$
|
||||||
|
{
|
||||||
|
"version": "1.2.3",
|
||||||
|
"attribution": "osm",
|
||||||
|
"description": "a description from table comment",
|
||||||
|
"foo": {"bar": "foo"}
|
||||||
|
}
|
||||||
|
$$::json || '$tj$';
|
||||||
|
END $do$;
|
||||||
|
```
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
use log::error;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tilejson::{Bounds, TileJSON};
|
use tilejson::{Bounds, TileJSON};
|
||||||
|
|
||||||
use crate::config::UnrecognizedValues;
|
use crate::config::UnrecognizedValues;
|
||||||
use crate::pg::config::PgInfo;
|
use crate::pg::config::PgInfo;
|
||||||
use crate::pg::utils::InfoMap;
|
use crate::pg::utils::{patch_json, InfoMap};
|
||||||
|
|
||||||
pub type FuncInfoSources = InfoMap<FunctionInfo>;
|
pub type FuncInfoSources = InfoMap<FunctionInfo>;
|
||||||
|
|
||||||
@ -67,34 +66,6 @@ impl FunctionInfo {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge the `self.tilejson` from the function comment into the generated tilejson (param)
|
|
||||||
fn merge_json(&self, tilejson: TileJSON) -> TileJSON {
|
|
||||||
let Some(tj) = &self.tilejson else {
|
|
||||||
// Nothing to merge in, keep the original
|
|
||||||
return tilejson;
|
|
||||||
};
|
|
||||||
// Not the most efficient, but this is only executed once per source:
|
|
||||||
// * Convert the TileJSON struct to a serde_json::Value
|
|
||||||
// * Merge the self.tilejson into the value
|
|
||||||
// * Convert the merged value back to a TileJSON struct
|
|
||||||
// * In case of errors, return the original tilejson
|
|
||||||
let mut tilejson2 = match serde_json::to_value(tilejson.clone()) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to serialize tilejson, unable to merge function comment: {e}");
|
|
||||||
return tilejson;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
json_patch::merge(&mut tilejson2, tj);
|
|
||||||
match serde_json::from_value(tilejson2.clone()) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to deserialize merged function comment tilejson: {e}");
|
|
||||||
tilejson
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PgInfo for FunctionInfo {
|
impl PgInfo for FunctionInfo {
|
||||||
@ -111,6 +82,6 @@ impl PgInfo for FunctionInfo {
|
|||||||
tilejson.minzoom = self.minzoom;
|
tilejson.minzoom = self.minzoom;
|
||||||
tilejson.maxzoom = self.maxzoom;
|
tilejson.maxzoom = self.maxzoom;
|
||||||
tilejson.bounds = self.bounds;
|
tilejson.bounds = self.bounds;
|
||||||
self.merge_json(tilejson)
|
patch_json(tilejson, &self.tilejson)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ use tilejson::{Bounds, TileJSON, VectorLayer};
|
|||||||
|
|
||||||
use crate::config::UnrecognizedValues;
|
use crate::config::UnrecognizedValues;
|
||||||
use crate::pg::config::PgInfo;
|
use crate::pg::config::PgInfo;
|
||||||
use crate::pg::utils::InfoMap;
|
use crate::pg::utils::{patch_json, InfoMap};
|
||||||
use crate::utils::sorted_opt_map;
|
use crate::utils::sorted_opt_map;
|
||||||
|
|
||||||
pub type TableInfoSources = InfoMap<TableInfo>;
|
pub type TableInfoSources = InfoMap<TableInfo>;
|
||||||
@ -81,6 +81,10 @@ pub struct TableInfo {
|
|||||||
|
|
||||||
#[serde(flatten, skip_serializing)]
|
#[serde(flatten, skip_serializing)]
|
||||||
pub unrecognized: UnrecognizedValues,
|
pub unrecognized: UnrecognizedValues,
|
||||||
|
|
||||||
|
/// TileJSON provider by the SQL comment. Shouldn't be serialized
|
||||||
|
#[serde(skip)]
|
||||||
|
pub tilejson: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PgInfo for TableInfo {
|
impl PgInfo for TableInfo {
|
||||||
@ -106,6 +110,6 @@ impl PgInfo for TableInfo {
|
|||||||
other: HashMap::default(),
|
other: HashMap::default(),
|
||||||
};
|
};
|
||||||
tilejson.vector_layers = Some(vec![layer]);
|
tilejson.vector_layers = Some(vec![layer]);
|
||||||
tilejson
|
patch_json(tilejson, &self.tilejson)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,17 @@ WITH
|
|||||||
geometry_columns.f_table_schema = sic.table_schema AND
|
geometry_columns.f_table_schema = sic.table_schema AND
|
||||||
geometry_columns.f_table_name = sic.table_name AND
|
geometry_columns.f_table_name = sic.table_name AND
|
||||||
geometry_columns.f_geometry_column = sic.column_name
|
geometry_columns.f_geometry_column = sic.column_name
|
||||||
GROUP BY 1, 2, 3, 4, 5, 6)
|
GROUP BY 1, 2, 3, 4, 5, 6),
|
||||||
|
descriptions AS (
|
||||||
|
-- comments on table/views
|
||||||
|
SELECT
|
||||||
|
pg_namespace.nspname AS schema_name,
|
||||||
|
relname AS table_name,
|
||||||
|
CAST(obj_description(relfilenode, 'pg_class') AS VARCHAR) AS description
|
||||||
|
FROM pg_class
|
||||||
|
JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid
|
||||||
|
WHERE relkind = 'r' OR relkind = 'v'
|
||||||
|
)
|
||||||
SELECT schema,
|
SELECT schema,
|
||||||
name,
|
name,
|
||||||
geom,
|
geom,
|
||||||
@ -61,10 +71,14 @@ SELECT schema,
|
|||||||
jsonb_object_agg(columns.column_name, columns.type_name)
|
jsonb_object_agg(columns.column_name, columns.type_name)
|
||||||
FILTER (WHERE columns.column_name IS NOT NULL AND columns.type_name != 'geometry'),
|
FILTER (WHERE columns.column_name IS NOT NULL AND columns.type_name != 'geometry'),
|
||||||
'{}'::jsonb
|
'{}'::jsonb
|
||||||
) as properties
|
) as properties,
|
||||||
|
dc.description
|
||||||
FROM annotated_geometry_columns AS gc
|
FROM annotated_geometry_columns AS gc
|
||||||
LEFT JOIN columns ON
|
LEFT JOIN columns ON
|
||||||
gc.schema = columns.table_schema AND
|
gc.schema = columns.table_schema AND
|
||||||
gc.name = columns.table_name AND
|
gc.name = columns.table_name AND
|
||||||
gc.geom != columns.column_name
|
gc.geom != columns.column_name
|
||||||
GROUP BY gc.schema, gc.name, gc.geom, gc.srid, gc.type, gc.is_view, gc.geom_idx;
|
LEFT JOIN descriptions AS dc on
|
||||||
|
gc.schema = dc.schema_name AND
|
||||||
|
gc.name = dc.table_name
|
||||||
|
GROUP BY gc.schema, gc.name, gc.geom, gc.srid, gc.type, gc.is_view, gc.geom_idx,dc.description;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use log::{info, warn};
|
use log::{debug, info, warn};
|
||||||
use postgis::ewkb;
|
use postgis::ewkb;
|
||||||
use postgres_protocol::escape::{escape_identifier, escape_literal};
|
use postgres_protocol::escape::{escape_identifier, escape_literal};
|
||||||
|
use serde_json::Value;
|
||||||
use tilejson::Bounds;
|
use tilejson::Bounds;
|
||||||
|
|
||||||
use crate::pg::config::PgInfo;
|
use crate::pg::config::PgInfo;
|
||||||
@ -27,10 +28,25 @@ pub async fn query_available_tables(pool: &PgPool) -> Result<SqlTableInfoMapMapM
|
|||||||
|
|
||||||
let mut res = SqlTableInfoMapMapMap::new();
|
let mut res = SqlTableInfoMapMapMap::new();
|
||||||
for row in &rows {
|
for row in &rows {
|
||||||
|
let schema: String = row.get("schema");
|
||||||
|
let table: String = row.get("name");
|
||||||
|
let tilejson = if let Some(text) = row.get("description") {
|
||||||
|
match serde_json::from_str::<Value>(text) {
|
||||||
|
Ok(v) => Some(v),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Unable to deserialize SQL comment on {schema}.{table} as tilejson, the automatically generated tilejson would be used: {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!("Unable to find a SQL comment on {schema}.{table}, the tilejson would be generated automatically");
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let info = TableInfo {
|
let info = TableInfo {
|
||||||
layer_id: None,
|
layer_id: None,
|
||||||
schema: row.get("schema"),
|
schema,
|
||||||
table: row.get("name"),
|
table,
|
||||||
geometry_column: row.get("geom"),
|
geometry_column: row.get("geom"),
|
||||||
geometry_index: row.get("geom_idx"),
|
geometry_index: row.get("geom_idx"),
|
||||||
is_view: row.get("is_view"),
|
is_view: row.get("is_view"),
|
||||||
@ -46,6 +62,7 @@ pub async fn query_available_tables(pool: &PgPool) -> Result<SqlTableInfoMapMapM
|
|||||||
prop_mapping: HashMap::new(),
|
prop_mapping: HashMap::new(),
|
||||||
unrecognized: HashMap::new(),
|
unrecognized: HashMap::new(),
|
||||||
bounds: None,
|
bounds: None,
|
||||||
|
tilejson,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Warn for missing geometry indices. Ignore views since those can't have indices
|
// Warn for missing geometry indices. Ignore views since those can't have indices
|
||||||
|
@ -3,7 +3,7 @@ use std::collections::HashMap;
|
|||||||
use deadpool_postgres::tokio_postgres::types::Json;
|
use deadpool_postgres::tokio_postgres::types::Json;
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use postgis::{ewkb, LineString, Point, Polygon};
|
use postgis::{ewkb, LineString, Point, Polygon};
|
||||||
use tilejson::Bounds;
|
use tilejson::{Bounds, TileJSON};
|
||||||
|
|
||||||
use crate::source::UrlQuery;
|
use crate::source::UrlQuery;
|
||||||
|
|
||||||
@ -20,6 +20,34 @@ pub fn json_to_hashmap(value: &serde_json::Value) -> InfoMap<String> {
|
|||||||
hashmap
|
hashmap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn patch_json(target: TileJSON, patch: &Option<serde_json::Value>) -> TileJSON {
|
||||||
|
let Some(tj) = patch else {
|
||||||
|
// Nothing to merge in, keep the original
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
// Not the most efficient, but this is only executed once per source:
|
||||||
|
// * Convert the TileJSON struct to a serde_json::Value
|
||||||
|
// * Merge the self.tilejson into the value
|
||||||
|
// * Convert the merged value back to a TileJSON struct
|
||||||
|
// * In case of errors, return the original tilejson
|
||||||
|
let mut tilejson2 = match serde_json::to_value(target.clone()) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to serialize tilejson, unable to merge function comment: {e}");
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
json_patch::merge(&mut tilejson2, tj);
|
||||||
|
match serde_json::from_value(tilejson2.clone()) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to deserialize merged function comment tilejson: {e}");
|
||||||
|
target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn query_to_json(query: &UrlQuery) -> Json<InfoMap<serde_json::Value>> {
|
pub fn query_to_json(query: &UrlQuery) -> Json<InfoMap<serde_json::Value>> {
|
||||||
let mut query_as_json = HashMap::new();
|
let mut query_as_json = HashMap::new();
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"tiles": {
|
"tiles": {
|
||||||
"MixPoints": {
|
"MixPoints": {
|
||||||
"content_type": "application/x-protobuf",
|
"content_type": "application/x-protobuf",
|
||||||
"description": "MixedCase.MixPoints.Geom"
|
"description": "a description from comment on table"
|
||||||
},
|
},
|
||||||
"auto_table": {
|
"auto_table": {
|
||||||
"content_type": "application/x-protobuf",
|
"content_type": "application/x-protobuf",
|
||||||
@ -116,8 +116,7 @@
|
|||||||
"content_type": "image/png"
|
"content_type": "image/png"
|
||||||
},
|
},
|
||||||
"table_source": {
|
"table_source": {
|
||||||
"content_type": "application/x-protobuf",
|
"content_type": "application/x-protobuf"
|
||||||
"description": "public.table_source.geom"
|
|
||||||
},
|
},
|
||||||
"table_source_multiple_geom": {
|
"table_source_multiple_geom": {
|
||||||
"content_type": "application/x-protobuf",
|
"content_type": "application/x-protobuf",
|
||||||
|
@ -23,6 +23,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "public.table_source.geom\npublic.points1.geom\npublic.points2.geom",
|
"description": "public.points1.geom\npublic.points2.geom",
|
||||||
"name": "table_source,points1,points2"
|
"name": "table_source,points1,points2"
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "public.table_source.geom",
|
"name": "table_source",
|
||||||
"name": "table_source"
|
"foo": {
|
||||||
|
"bar": "foo"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
17
tests/fixtures/tables/MixPoints.sql
vendored
17
tests/fixtures/tables/MixPoints.sql
vendored
@ -54,3 +54,20 @@ values (1, '02daedc70702ec68753fde38351f5d9d', '0101000020E610000050C4D38CE9DA61
|
|||||||
(30, '404175d17b08782edc9d316c378adc86', '0101000020E6100000F9B5A5ADB7265BC0EE07F81F2F284840');
|
(30, '404175d17b08782edc9d316c378adc86', '0101000020E6100000F9B5A5ADB7265BC0EE07F81F2F284840');
|
||||||
|
|
||||||
CREATE INDEX ON "MixedCase"."MixPoints" USING GIST ("Geom");
|
CREATE INDEX ON "MixedCase"."MixPoints" USING GIST ("Geom");
|
||||||
|
|
||||||
|
DO $do$ BEGIN
|
||||||
|
EXECUTE 'COMMENT ON TABLE "MixedCase"."MixPoints" IS $tj$' || $$
|
||||||
|
{
|
||||||
|
"description": "a description from comment on table",
|
||||||
|
"vector_layers": [
|
||||||
|
{
|
||||||
|
"id":"MixPoints",
|
||||||
|
"fields":{
|
||||||
|
"Gid":"int4",
|
||||||
|
"TABLE":"text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
$$::json || '$tj$';
|
||||||
|
END $do$;
|
||||||
|
9
tests/fixtures/tables/table_source.sql
vendored
9
tests/fixtures/tables/table_source.sql
vendored
@ -33,3 +33,12 @@ INSERT INTO table_source(geom) values (GeomFromEWKT('SRID=4326;POINT(142.8403402
|
|||||||
INSERT INTO table_source(geom) values (GeomFromEWKT('SRID=4326;POINT(142.84131509869133 11.92781306544329)'));
|
INSERT INTO table_source(geom) values (GeomFromEWKT('SRID=4326;POINT(142.84131509869133 11.92781306544329)'));
|
||||||
|
|
||||||
-- DO NOT CREATE INDEX ON GEOMETRY COLUMN -- this table is used in a test case
|
-- DO NOT CREATE INDEX ON GEOMETRY COLUMN -- this table is used in a test case
|
||||||
|
|
||||||
|
DO $do$ BEGIN
|
||||||
|
EXECUTE 'COMMENT ON TABLE table_source IS $tj$' || $$
|
||||||
|
{
|
||||||
|
"description": null,
|
||||||
|
"foo": {"bar": "foo"}
|
||||||
|
}
|
||||||
|
$$::json || '$tj$';
|
||||||
|
END $do$;
|
||||||
|
Loading…
Reference in New Issue
Block a user