From ca9d8cffd0ac5f2624a2b699938dbaa46204eb78 Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Wed, 3 Sep 2025 23:23:23 -0400 Subject: [PATCH 1/9] allow views and tables to define feature IDs using "id" I was jumping between fid and featureID, however looking deeper, it looks like you use "id" for functions. --- internal/data/db_sql.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/data/db_sql.go b/internal/data/db_sql.go index 06afea8b..517cb143 100644 --- a/internal/data/db_sql.go +++ b/internal/data/db_sql.go @@ -31,7 +31,7 @@ const sqlTables = `SELECT a.attname AS geometry_column, postgis_typmod_srid(a.atttypmod) AS srid, postgis_typmod_type(a.atttypmod) AS geometry_type, - coalesce(ia.attname, '') AS id_column, + coalesce(ia.attname, fid.attname, '') AS id_column, ( SELECT array_agg(ARRAY[sa.attname, st.typname, coalesce(da.description,''), sa.attnum::text]::text[] ORDER BY sa.attnum) FROM pg_attribute sa @@ -50,6 +50,7 @@ LEFT JOIN pg_description d ON (c.oid = d.objoid AND d.objsubid = 0) LEFT JOIN pg_index i ON (c.oid = i.indrelid AND i.indisprimary AND i.indnatts = 1) LEFT JOIN pg_attribute ia ON (ia.attrelid = i.indexrelid) +LEFT JOIN pg_attribute fid ON (fid.attrelid = c.oid AND fid.attname = 'id' AND fid.attnum > 0 AND NOT fid.attisdropped) LEFT JOIN pg_type it ON (ia.atttypid = it.oid AND it.typname in ('int2', 'int4', 'int8')) WHERE c.relkind IN ('r', 'v', 'm', 'p', 'f') AND t.typname IN ('geometry', 'geography') From 3029e5a5749b29a8e6633edfe1f9e218af272adc Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Tue, 16 Sep 2025 10:59:55 -0400 Subject: [PATCH 2/9] configure id column via config --- internal/conf/config.go | 2 ++ internal/data/catalog_db.go | 2 +- internal/data/catalog_db_fun.go | 2 +- internal/data/db_sql.go | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/conf/config.go b/internal/conf/config.go index fd81b0ff..efbdd940 100644 --- a/internal/conf/config.go +++ b/internal/conf/config.go @@ -44,6 +44,7 @@ func setDefaultConfig() { viper.SetDefault("Database.TableIncludes", []string{}) viper.SetDefault("Database.TableExcludes", []string{}) viper.SetDefault("Database.FunctionIncludes", []string{"postgisftw"}) + viper.SetDefault("Database.IdColumn", "id") viper.SetDefault("Paging.LimitDefault", 10) viper.SetDefault("Paging.LimitMax", 1000) @@ -94,6 +95,7 @@ type Database struct { TableIncludes []string TableExcludes []string FunctionIncludes []string + IdColumn string } // Metadata config diff --git a/internal/data/catalog_db.go b/internal/data/catalog_db.go index 281b42f3..b6ca011d 100644 --- a/internal/data/catalog_db.go +++ b/internal/data/catalog_db.go @@ -262,7 +262,7 @@ func tablesSorted(tableMap map[string]*Table) []*Table { func (cat *catalogDB) readTables(db *pgxpool.Pool) map[string]*Table { log.Debugf("Load table catalog:\n%v", sqlTables) - rows, err := db.Query(context.Background(), sqlTables) + rows, err := db.Query(context.Background(), sqlTables, conf.Configuration.Database.IdColumn) if err != nil { log.Fatal(err) } diff --git a/internal/data/catalog_db_fun.go b/internal/data/catalog_db_fun.go index 1afe2fe0..b5a0d6cd 100644 --- a/internal/data/catalog_db_fun.go +++ b/internal/data/catalog_db_fun.go @@ -191,7 +191,7 @@ func (cat *catalogDB) FunctionFeatures(ctx context.Context, name string, args ma return nil, errArg } propCols := removeNames(param.Columns, fn.GeometryColumn, "") - idColIndex := indexOfName(propCols, FunctionIDColumnName) + idColIndex := indexOfName(propCols, conf.Configuration.Database.IdColumn) sql, argValues := sqlGeomFunction(fn, args, propCols, param) log.Debugf("Function features query: %v", sql) log.Debugf("Function %v Args: %v", name, argValues) diff --git a/internal/data/db_sql.go b/internal/data/db_sql.go index 517cb143..1deeafba 100644 --- a/internal/data/db_sql.go +++ b/internal/data/db_sql.go @@ -50,7 +50,7 @@ LEFT JOIN pg_description d ON (c.oid = d.objoid AND d.objsubid = 0) LEFT JOIN pg_index i ON (c.oid = i.indrelid AND i.indisprimary AND i.indnatts = 1) LEFT JOIN pg_attribute ia ON (ia.attrelid = i.indexrelid) -LEFT JOIN pg_attribute fid ON (fid.attrelid = c.oid AND fid.attname = 'id' AND fid.attnum > 0 AND NOT fid.attisdropped) +LEFT JOIN pg_attribute fid ON (fid.attrelid = c.oid AND fid.attname = $1 AND fid.attnum > 0 AND NOT fid.attisdropped) LEFT JOIN pg_type it ON (ia.atttypid = it.oid AND it.typname in ('int2', 'int4', 'int8')) WHERE c.relkind IN ('r', 'v', 'm', 'p', 'f') AND t.typname IN ('geometry', 'geography') From 7546a9cc767fcfe764960adac014505d749947a2 Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Tue, 16 Sep 2025 12:25:35 -0400 Subject: [PATCH 3/9] id column from config --- internal/data/catalog_db_fun.go | 1 - internal/service/handler.go | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/data/catalog_db_fun.go b/internal/data/catalog_db_fun.go index b5a0d6cd..7b4b0c9d 100644 --- a/internal/data/catalog_db_fun.go +++ b/internal/data/catalog_db_fun.go @@ -26,7 +26,6 @@ import ( ) // FunctionIDColumnName is the name for a function-supplied ID -const FunctionIDColumnName = "id" const SchemaPostGISFTW = "postgisftw" diff --git a/internal/service/handler.go b/internal/service/handler.go index a0d5fe46..ad7173a8 100644 --- a/internal/service/handler.go +++ b/internal/service/handler.go @@ -636,7 +636,7 @@ func writeFunItemsHTML(w http.ResponseWriter, name string, query string, urlBase context.Group = "Functions" context.Title = fn.ID context.Function = fn - context.IDColumn = data.FunctionIDColumnName + context.IDColumn = conf.Configuration.Database.IdColumn // features are not needed for items page (page queries for them) return writeHTML(w, nil, context, ui.PageFunctionItems()) From a32ddb87bff7a91cc601488a374d9958bcdf0b2a Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Wed, 17 Sep 2025 17:48:27 -0400 Subject: [PATCH 4/9] docs --- config/pg_featureserv.toml.example | 3 +++ hugo/content/installation/configuration.md | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/config/pg_featureserv.toml.example b/config/pg_featureserv.toml.example index cb35560f..321fb6fc 100644 --- a/config/pg_featureserv.toml.example +++ b/config/pg_featureserv.toml.example @@ -68,6 +68,9 @@ WriteTimeoutSec = 30 # Publish functions from these schemas (default is publish postgisftw) # FunctionIncludes = [ "postgisftw", "schema2" ] +# Designate a column as the feature ID (where primary key is not available e.g. views/functions) +IdColumn = "id" + [Paging] # The default number of features in a response LimitDefault = 20 diff --git a/hugo/content/installation/configuration.md b/hugo/content/installation/configuration.md index f0b3273e..a441769d 100644 --- a/hugo/content/installation/configuration.md +++ b/hugo/content/installation/configuration.md @@ -119,6 +119,9 @@ WriteTimeoutSec = 30 # Publish functions from these schemas (default is publish postgisftw) # FunctionIncludes = [ "postgisftw", "schema2" ] +# Designate a column as the feature ID (where primary key is not available e.g. views/functions) +IdColumn = "id" + [Paging] # The default number of features in a response LimitDefault = 20 @@ -243,6 +246,12 @@ Overrides items specified in `TableIncludes`. A list of the schemas to publish functions from. The default is to publish functions in the `postgisftw` schema. +#### IdColumn + +The column to use as the feature ID in cases where a primary key is not available. The default is `id`. + +> NOTE: The presence of a primary key will supercede this setting. + #### LimitDefault The default number of features in a response, From 2258c7f717bea67afb7b683f527fb011aa6cdbe7 Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Thu, 18 Sep 2025 16:26:28 -0400 Subject: [PATCH 5/9] fix inconsistency in config --- config/pg_featureserv.toml.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/pg_featureserv.toml.example b/config/pg_featureserv.toml.example index 321fb6fc..311e5187 100644 --- a/config/pg_featureserv.toml.example +++ b/config/pg_featureserv.toml.example @@ -69,7 +69,7 @@ WriteTimeoutSec = 30 # FunctionIncludes = [ "postgisftw", "schema2" ] # Designate a column as the feature ID (where primary key is not available e.g. views/functions) -IdColumn = "id" +# IdColumn = "id" [Paging] # The default number of features in a response From 03b36457ec616fc43593dfbcec3caf3dcb03ec98 Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Mon, 22 Sep 2025 15:06:57 -0400 Subject: [PATCH 6/9] perform id fallback in go instead of sql --- internal/data/catalog_db.go | 9 ++++++++- internal/data/db_sql.go | 3 +-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/data/catalog_db.go b/internal/data/catalog_db.go index b6ca011d..2408e585 100644 --- a/internal/data/catalog_db.go +++ b/internal/data/catalog_db.go @@ -262,7 +262,7 @@ func tablesSorted(tableMap map[string]*Table) []*Table { func (cat *catalogDB) readTables(db *pgxpool.Pool) map[string]*Table { log.Debugf("Load table catalog:\n%v", sqlTables) - rows, err := db.Query(context.Background(), sqlTables, conf.Configuration.Database.IdColumn) + rows, err := db.Query(context.Background(), sqlTables) if err != nil { log.Fatal(err) } @@ -354,6 +354,13 @@ func scanTable(rows pgx.Rows) *Table { colDesc[i] = props.Elements[elmPos+2].String } + // default ID column if primary key not defined. check if conf.Configuration.Database.IdColumn is among columns + if idColumn == "" && conf.Configuration.Database.IdColumn != "" { + if _, ok := datatypes[conf.Configuration.Database.IdColumn]; ok { + idColumn = conf.Configuration.Database.IdColumn + } + } + // Synthesize a title for now title := id // synthesize a description if none provided diff --git a/internal/data/db_sql.go b/internal/data/db_sql.go index 1deeafba..06afea8b 100644 --- a/internal/data/db_sql.go +++ b/internal/data/db_sql.go @@ -31,7 +31,7 @@ const sqlTables = `SELECT a.attname AS geometry_column, postgis_typmod_srid(a.atttypmod) AS srid, postgis_typmod_type(a.atttypmod) AS geometry_type, - coalesce(ia.attname, fid.attname, '') AS id_column, + coalesce(ia.attname, '') AS id_column, ( SELECT array_agg(ARRAY[sa.attname, st.typname, coalesce(da.description,''), sa.attnum::text]::text[] ORDER BY sa.attnum) FROM pg_attribute sa @@ -50,7 +50,6 @@ LEFT JOIN pg_description d ON (c.oid = d.objoid AND d.objsubid = 0) LEFT JOIN pg_index i ON (c.oid = i.indrelid AND i.indisprimary AND i.indnatts = 1) LEFT JOIN pg_attribute ia ON (ia.attrelid = i.indexrelid) -LEFT JOIN pg_attribute fid ON (fid.attrelid = c.oid AND fid.attname = $1 AND fid.attnum > 0 AND NOT fid.attisdropped) LEFT JOIN pg_type it ON (ia.atttypid = it.oid AND it.typname in ('int2', 'int4', 'int8')) WHERE c.relkind IN ('r', 'v', 'm', 'p', 'f') AND t.typname IN ('geometry', 'geography') From ae2880b83f091809944f672d98450b3eb436da94 Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Mon, 22 Sep 2025 15:08:44 -0400 Subject: [PATCH 7/9] cleanup --- hugo/content/installation/configuration.md | 2 +- internal/data/catalog_db.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hugo/content/installation/configuration.md b/hugo/content/installation/configuration.md index a441769d..420cde0e 100644 --- a/hugo/content/installation/configuration.md +++ b/hugo/content/installation/configuration.md @@ -120,7 +120,7 @@ WriteTimeoutSec = 30 # FunctionIncludes = [ "postgisftw", "schema2" ] # Designate a column as the feature ID (where primary key is not available e.g. views/functions) -IdColumn = "id" +# IdColumn = "id" [Paging] # The default number of features in a response diff --git a/internal/data/catalog_db.go b/internal/data/catalog_db.go index 2408e585..b92d4bc9 100644 --- a/internal/data/catalog_db.go +++ b/internal/data/catalog_db.go @@ -354,7 +354,7 @@ func scanTable(rows pgx.Rows) *Table { colDesc[i] = props.Elements[elmPos+2].String } - // default ID column if primary key not defined. check if conf.Configuration.Database.IdColumn is among columns + // detect ID column if primary key not defined. if idColumn == "" && conf.Configuration.Database.IdColumn != "" { if _, ok := datatypes[conf.Configuration.Database.IdColumn]; ok { idColumn = conf.Configuration.Database.IdColumn From a4b2e287f9436d3ef31d878866e6b1e88ef783a0 Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Mon, 22 Sep 2025 15:24:53 -0400 Subject: [PATCH 8/9] move function ID logic earlier --- demo/initdb/02-functions.sql | 5 +++-- internal/data/catalog_db_fun.go | 10 +++++++++- internal/service/handler.go | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/demo/initdb/02-functions.sql b/demo/initdb/02-functions.sql index 1b185954..49ea88e9 100644 --- a/demo/initdb/02-functions.sql +++ b/demo/initdb/02-functions.sql @@ -19,7 +19,7 @@ BEGIN dlat := (lat_max - lat_min) / num_y; RETURN QUERY SELECT - x.x::text || '_' || y.y::text AS fid, + x.x::text || '_' || y.y::text AS id, ST_MakeEnvelope( lon_min + (x.x - 1) * dlon, lat_min + (y.y - 1) * dlat, lon_min + x.x * dlon, lat_min + y.y * dlat, 4326 @@ -35,7 +35,7 @@ COMMENT ON FUNCTION postgisftw.us_grid IS 'Generates a grid of rectangles coveri CREATE OR REPLACE FUNCTION postgisftw.us_grid_noid( num_x integer DEFAULT 10, num_y integer DEFAULT 10) -RETURNS TABLE(geom geometry) +RETURNS TABLE(value text, geom geometry) AS $$ DECLARE lon_min CONSTANT numeric := -128; @@ -49,6 +49,7 @@ BEGIN dlat := (lat_max - lat_min) / num_y; RETURN QUERY SELECT + x.x::text || '_' || y.y::text AS value, ST_MakeEnvelope( lon_min + (x.x - 1) * dlon, lat_min + (y.y - 1) * dlat, lon_min + x.x * dlon, lat_min + y.y * dlat, 4326 diff --git a/internal/data/catalog_db_fun.go b/internal/data/catalog_db_fun.go index 7b4b0c9d..651880eb 100644 --- a/internal/data/catalog_db_fun.go +++ b/internal/data/catalog_db_fun.go @@ -115,6 +115,13 @@ func scanFunctionDef(rows pgx.Rows) *Function { geomCol := geometryColumn(outNames, datatypes) + idColumn := "" + if conf.Configuration.Database.IdColumn != "" { + if _, ok := datatypes[conf.Configuration.Database.IdColumn]; ok { + idColumn = conf.Configuration.Database.IdColumn + } + } + funDef := Function{ ID: id, Schema: schema, @@ -130,6 +137,7 @@ func scanFunctionDef(rows pgx.Rows) *Function { OutJSONTypes: outJSONTypes, Types: datatypes, GeometryColumn: geomCol, + IDColumn: idColumn, } //fmt.Printf("DEBUG: Function definitions: %v\n", funDef) return &funDef @@ -190,7 +198,7 @@ func (cat *catalogDB) FunctionFeatures(ctx context.Context, name string, args ma return nil, errArg } propCols := removeNames(param.Columns, fn.GeometryColumn, "") - idColIndex := indexOfName(propCols, conf.Configuration.Database.IdColumn) + idColIndex := indexOfName(propCols, fn.IDColumn) sql, argValues := sqlGeomFunction(fn, args, propCols, param) log.Debugf("Function features query: %v", sql) log.Debugf("Function %v Args: %v", name, argValues) diff --git a/internal/service/handler.go b/internal/service/handler.go index ad7173a8..6cbf8161 100644 --- a/internal/service/handler.go +++ b/internal/service/handler.go @@ -636,7 +636,7 @@ func writeFunItemsHTML(w http.ResponseWriter, name string, query string, urlBase context.Group = "Functions" context.Title = fn.ID context.Function = fn - context.IDColumn = conf.Configuration.Database.IdColumn + context.IDColumn = fn.IDColumn // features are not needed for items page (page queries for them) return writeHTML(w, nil, context, ui.PageFunctionItems()) From 94e54b05eed09dc71a8eecc58930201a62c3e37e Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Mon, 22 Sep 2025 15:28:51 -0400 Subject: [PATCH 9/9] capital D --- config/pg_featureserv.toml.example | 2 +- hugo/content/installation/configuration.md | 4 ++-- internal/conf/config.go | 4 ++-- internal/data/catalog_db.go | 6 +++--- internal/data/catalog_db_fun.go | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/config/pg_featureserv.toml.example b/config/pg_featureserv.toml.example index 311e5187..536ae64c 100644 --- a/config/pg_featureserv.toml.example +++ b/config/pg_featureserv.toml.example @@ -69,7 +69,7 @@ WriteTimeoutSec = 30 # FunctionIncludes = [ "postgisftw", "schema2" ] # Designate a column as the feature ID (where primary key is not available e.g. views/functions) -# IdColumn = "id" +# IDColumn = "id" [Paging] # The default number of features in a response diff --git a/hugo/content/installation/configuration.md b/hugo/content/installation/configuration.md index 420cde0e..77da281c 100644 --- a/hugo/content/installation/configuration.md +++ b/hugo/content/installation/configuration.md @@ -120,7 +120,7 @@ WriteTimeoutSec = 30 # FunctionIncludes = [ "postgisftw", "schema2" ] # Designate a column as the feature ID (where primary key is not available e.g. views/functions) -# IdColumn = "id" +# IDColumn = "id" [Paging] # The default number of features in a response @@ -246,7 +246,7 @@ Overrides items specified in `TableIncludes`. A list of the schemas to publish functions from. The default is to publish functions in the `postgisftw` schema. -#### IdColumn +#### IDColumn The column to use as the feature ID in cases where a primary key is not available. The default is `id`. diff --git a/internal/conf/config.go b/internal/conf/config.go index efbdd940..d668d17f 100644 --- a/internal/conf/config.go +++ b/internal/conf/config.go @@ -44,7 +44,7 @@ func setDefaultConfig() { viper.SetDefault("Database.TableIncludes", []string{}) viper.SetDefault("Database.TableExcludes", []string{}) viper.SetDefault("Database.FunctionIncludes", []string{"postgisftw"}) - viper.SetDefault("Database.IdColumn", "id") + viper.SetDefault("Database.IDColumn", "id") viper.SetDefault("Paging.LimitDefault", 10) viper.SetDefault("Paging.LimitMax", 1000) @@ -95,7 +95,7 @@ type Database struct { TableIncludes []string TableExcludes []string FunctionIncludes []string - IdColumn string + IDColumn string } // Metadata config diff --git a/internal/data/catalog_db.go b/internal/data/catalog_db.go index b92d4bc9..25bd38eb 100644 --- a/internal/data/catalog_db.go +++ b/internal/data/catalog_db.go @@ -355,9 +355,9 @@ func scanTable(rows pgx.Rows) *Table { } // detect ID column if primary key not defined. - if idColumn == "" && conf.Configuration.Database.IdColumn != "" { - if _, ok := datatypes[conf.Configuration.Database.IdColumn]; ok { - idColumn = conf.Configuration.Database.IdColumn + if idColumn == "" && conf.Configuration.Database.IDColumn != "" { + if _, ok := datatypes[conf.Configuration.Database.IDColumn]; ok { + idColumn = conf.Configuration.Database.IDColumn } } diff --git a/internal/data/catalog_db_fun.go b/internal/data/catalog_db_fun.go index 651880eb..f5f51a9e 100644 --- a/internal/data/catalog_db_fun.go +++ b/internal/data/catalog_db_fun.go @@ -116,9 +116,9 @@ func scanFunctionDef(rows pgx.Rows) *Function { geomCol := geometryColumn(outNames, datatypes) idColumn := "" - if conf.Configuration.Database.IdColumn != "" { - if _, ok := datatypes[conf.Configuration.Database.IdColumn]; ok { - idColumn = conf.Configuration.Database.IdColumn + if conf.Configuration.Database.IDColumn != "" { + if _, ok := datatypes[conf.Configuration.Database.IDColumn]; ok { + idColumn = conf.Configuration.Database.IDColumn } }