github.com/PDOK/gokoala@v0.50.6/internal/ogc/features/datasources/geopackage/metadata.go (about)

     1  package geopackage
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"log"
     7  
     8  	"github.com/PDOK/gokoala/config"
     9  
    10  	"github.com/jmoiron/sqlx"
    11  )
    12  
    13  // Read metadata about gpkg and sqlite driver
    14  func readDriverMetadata(db *sqlx.DB) (string, error) {
    15  	type pragma struct {
    16  		UserVersion string `db:"user_version"`
    17  	}
    18  	type metadata struct {
    19  		Sqlite     string `db:"sqlite"`
    20  		Spatialite string `db:"spatialite"`
    21  		Arch       string `db:"arch"`
    22  	}
    23  
    24  	var m metadata
    25  	err := db.QueryRowx(`
    26  select sqlite_version() as sqlite,
    27  spatialite_version() as spatialite,
    28  spatialite_target_cpu() as arch`).StructScan(&m)
    29  	if err != nil {
    30  		return "", err
    31  	}
    32  
    33  	var gpkgVersion pragma
    34  	_ = db.QueryRowx(`pragma user_version`).StructScan(&gpkgVersion)
    35  	if gpkgVersion.UserVersion == "" {
    36  		gpkgVersion.UserVersion = "unknown"
    37  	}
    38  
    39  	return fmt.Sprintf("geopackage version: %s, sqlite version: %s, spatialite version: %s on %s",
    40  		gpkgVersion.UserVersion, m.Sqlite, m.Spatialite, m.Arch), nil
    41  }
    42  
    43  // Read gpkg_contents table. This table contains metadata about feature tables. The result is a mapping from
    44  // collection ID -> feature table metadata. We match each feature table to the collection ID by looking at the
    45  // 'identifier' column. Also in case there's no exact match between 'collection ID' and 'identifier' we use
    46  // the explicitly configured table name.
    47  func readGpkgContents(collections config.GeoSpatialCollections, db *sqlx.DB) (map[string]*featureTable, error) {
    48  	query := `
    49  select
    50  	c.table_name, c.data_type, c.identifier, c.description, c.last_change,
    51  	c.min_x, c.min_y, c.max_x, c.max_y, c.srs_id, gc.column_name, gc.geometry_type_name
    52  from
    53  	gpkg_contents c join gpkg_geometry_columns gc on c.table_name == gc.table_name
    54  where
    55  	c.data_type = 'features' and
    56  	c.min_x is not null`
    57  
    58  	rows, err := db.Queryx(query)
    59  	if err != nil {
    60  		return nil, fmt.Errorf("failed to retrieve gpkg_contents using query: %v\n, error: %w", query, err)
    61  	}
    62  	defer rows.Close()
    63  
    64  	result := make(map[string]*featureTable, 10)
    65  	for rows.Next() {
    66  		row := featureTable{
    67  			ColumnsWithDateType: make(map[string]string),
    68  		}
    69  		if err = rows.StructScan(&row); err != nil {
    70  			return nil, fmt.Errorf("failed to read gpkg_contents record, error: %w", err)
    71  		}
    72  		if row.TableName == "" {
    73  			return nil, fmt.Errorf("feature table name is blank, error: %w", err)
    74  		}
    75  		if err = readFeatureTableInfo(db, row); err != nil {
    76  			return nil, fmt.Errorf("failed to read feature table metadata, error: %w", err)
    77  		}
    78  
    79  		for _, collection := range collections {
    80  			if row.Identifier == collection.ID {
    81  				result[collection.ID] = &row
    82  			} else if hasMatchingTableName(collection, row) {
    83  				result[collection.ID] = &row
    84  			}
    85  		}
    86  	}
    87  
    88  	if err = rows.Err(); err != nil {
    89  		return nil, err
    90  	}
    91  	if len(result) == 0 {
    92  		return nil, errors.New("no records found in gpkg_contents, can't serve features")
    93  	}
    94  	uniqueTables := make(map[string]struct{})
    95  	for _, table := range result {
    96  		uniqueTables[table.TableName] = struct{}{}
    97  	}
    98  	if len(uniqueTables) != len(result) {
    99  		log.Printf("Warning: found %d unique table names for %d collections, "+
   100  			"usually each collection is backed by its own unique table\n", len(uniqueTables), len(result))
   101  	}
   102  	return result, nil
   103  }
   104  
   105  func readFeatureTableInfo(db *sqlx.DB, table featureTable) error {
   106  	rows, err := db.Queryx(fmt.Sprintf("select name, type from pragma_table_info('%s')", table.TableName))
   107  	if err != nil {
   108  		return err
   109  	}
   110  	defer rows.Close()
   111  
   112  	for rows.Next() {
   113  		var colName, colType string
   114  		err = rows.Scan(&colName, &colType)
   115  		if err != nil {
   116  			return err
   117  		}
   118  		table.ColumnsWithDateType[colName] = colType
   119  	}
   120  	return nil
   121  }
   122  
   123  func hasMatchingTableName(collection config.GeoSpatialCollection, row featureTable) bool {
   124  	return collection.Features != nil && collection.Features.TableName != nil &&
   125  		row.Identifier == *collection.Features.TableName
   126  }