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

     1  package geopackage
     2  
     3  import (
     4  	"context"
     5  	"path"
     6  	"runtime"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/PDOK/gokoala/config"
    11  
    12  	"github.com/PDOK/gokoala/internal/ogc/features/datasources"
    13  	"github.com/PDOK/gokoala/internal/ogc/features/domain"
    14  	"github.com/go-spatial/geom/encoding/geojson"
    15  	"github.com/stretchr/testify/assert"
    16  )
    17  
    18  var pwd string
    19  
    20  func init() {
    21  	_, filename, _, _ := runtime.Caller(0)
    22  	pwd = path.Dir(filename)
    23  }
    24  
    25  func newAddressesGeoPackage() geoPackageBackend {
    26  	loadDriver()
    27  	return newLocalGeoPackage(&config.GeoPackageLocal{
    28  		GeoPackageCommon: config.GeoPackageCommon{
    29  			Fid:                       "feature_id",
    30  			QueryTimeout:              config.Duration{Duration: 15 * time.Second},
    31  			MaxBBoxSizeToUseWithRTree: 30000,
    32  			InMemoryCacheSize:         -2000,
    33  		},
    34  		File: pwd + "/testdata/bag.gpkg",
    35  	})
    36  }
    37  
    38  func newTemporalAddressesGeoPackage() geoPackageBackend {
    39  	loadDriver()
    40  	return newLocalGeoPackage(&config.GeoPackageLocal{
    41  		GeoPackageCommon: config.GeoPackageCommon{
    42  			Fid:                       "feature_id",
    43  			QueryTimeout:              config.Duration{Duration: 15 * time.Second},
    44  			MaxBBoxSizeToUseWithRTree: 30000,
    45  			InMemoryCacheSize:         -2000,
    46  		},
    47  		File: pwd + "/testdata/bag-temporal.gpkg",
    48  	})
    49  }
    50  
    51  func TestNewGeoPackage(t *testing.T) {
    52  	type args struct {
    53  		config     config.GeoPackage
    54  		collection config.GeoSpatialCollections
    55  	}
    56  	tests := []struct {
    57  		name                        string
    58  		args                        args
    59  		wantNrOfFeatureTablesInGpkg int
    60  	}{
    61  		{
    62  			name: "open local geopackage",
    63  			args: args{
    64  				config: config.GeoPackage{
    65  					Local: &config.GeoPackageLocal{
    66  						GeoPackageCommon: config.GeoPackageCommon{
    67  							Fid:               "feature_id",
    68  							InMemoryCacheSize: -2000,
    69  						},
    70  						File: pwd + "/testdata/bag.gpkg",
    71  					},
    72  				},
    73  				collection: []config.GeoSpatialCollection{
    74  					{
    75  						ID:       "ligplaatsen",
    76  						Features: &config.CollectionEntryFeatures{},
    77  					},
    78  				},
    79  			},
    80  			wantNrOfFeatureTablesInGpkg: 1, // 3 in geopackage, but we define only 1 collection
    81  		},
    82  	}
    83  	for _, tt := range tests {
    84  		t.Run(tt.name, func(t *testing.T) {
    85  			assert.Equalf(t, tt.wantNrOfFeatureTablesInGpkg, len(NewGeoPackage(tt.args.collection, tt.args.config).featureTableByCollectionID), "NewGeoPackage(%v)", tt.args.config)
    86  		})
    87  	}
    88  }
    89  
    90  func TestGeoPackage_GetFeatures(t *testing.T) {
    91  	type fields struct {
    92  		backend          geoPackageBackend
    93  		fidColumn        string
    94  		featureTableByID map[string]*featureTable
    95  		queryTimeout     time.Duration
    96  	}
    97  	type args struct {
    98  		ctx         context.Context
    99  		collection  string
   100  		queryParams datasources.FeaturesCriteria
   101  	}
   102  	refDate, _ := time.Parse(time.RFC3339, "2023-12-31T00:00:00Z")
   103  	tests := []struct {
   104  		name       string
   105  		fields     fields
   106  		args       args
   107  		wantFC     *domain.FeatureCollection
   108  		wantCursor domain.Cursors
   109  		wantErr    bool
   110  	}{
   111  		{
   112  			name: "get first page of features",
   113  			fields: fields{
   114  				backend:          newAddressesGeoPackage(),
   115  				fidColumn:        "feature_id",
   116  				featureTableByID: map[string]*featureTable{"ligplaatsen": {TableName: "ligplaatsen", GeometryColumnName: "geom"}},
   117  				queryTimeout:     60 * time.Second,
   118  			},
   119  			args: args{
   120  				ctx:        context.Background(),
   121  				collection: "ligplaatsen",
   122  				queryParams: datasources.FeaturesCriteria{
   123  					Cursor: domain.DecodedCursor{FID: 0, FiltersChecksum: []byte{}},
   124  					Limit:  2,
   125  				},
   126  			},
   127  			wantFC: &domain.FeatureCollection{
   128  				NumberReturned: 2,
   129  				Features: []*domain.Feature{
   130  					{
   131  						Feature: geojson.Feature{
   132  							Properties: map[string]any{
   133  								"straatnaam": "Van Diemenkade",
   134  								"nummer_id":  "0363200000454013",
   135  							},
   136  						},
   137  					},
   138  					{
   139  						Feature: geojson.Feature{
   140  							Properties: map[string]any{
   141  								"straatnaam": "Realengracht",
   142  								"nummer_id":  "0363200000398886",
   143  							},
   144  						},
   145  					},
   146  				},
   147  			},
   148  			wantCursor: domain.Cursors{
   149  				Prev: "|",
   150  				Next: "Dv4|", // 3838
   151  			},
   152  			wantErr: false,
   153  		},
   154  		{
   155  			name: "get second page of features",
   156  			fields: fields{
   157  				backend:          newAddressesGeoPackage(),
   158  				fidColumn:        "feature_id",
   159  				featureTableByID: map[string]*featureTable{"ligplaatsen": {TableName: "ligplaatsen", GeometryColumnName: "geom"}},
   160  				queryTimeout:     5 * time.Second,
   161  			},
   162  			args: args{
   163  				ctx:        context.Background(),
   164  				collection: "ligplaatsen",
   165  				queryParams: datasources.FeaturesCriteria{
   166  					Cursor: domain.DecodedCursor{
   167  						FID:             3838, // see next cursor from test above
   168  						FiltersChecksum: []byte{},
   169  					},
   170  					Limit: 3,
   171  				},
   172  			},
   173  			wantFC: &domain.FeatureCollection{
   174  				NumberReturned: 3,
   175  				Features: []*domain.Feature{
   176  					{
   177  						Feature: geojson.Feature{
   178  							Properties: map[string]any{
   179  								"straatnaam": "Realengracht",
   180  								"nummer_id":  "0363200000398887",
   181  							},
   182  						},
   183  					},
   184  					{
   185  						Feature: geojson.Feature{
   186  							Properties: map[string]any{
   187  								"straatnaam": "Realengracht",
   188  								"nummer_id":  "0363200000398888",
   189  							},
   190  						},
   191  					},
   192  					{
   193  						Feature: geojson.Feature{
   194  							Properties: map[string]any{
   195  								"straatnaam": "Realengracht",
   196  								"nummer_id":  "0363200000398889",
   197  							},
   198  						},
   199  					},
   200  				},
   201  			},
   202  			wantCursor: domain.Cursors{
   203  				Prev: "|",
   204  				Next: "DwE|",
   205  			},
   206  			wantErr: false,
   207  		},
   208  		{
   209  			name: "get first page of features with reference date",
   210  			fields: fields{
   211  				backend:          newTemporalAddressesGeoPackage(),
   212  				fidColumn:        "feature_id",
   213  				featureTableByID: map[string]*featureTable{"ligplaatsen": {TableName: "ligplaatsen", GeometryColumnName: "geom"}},
   214  				queryTimeout:     60 * time.Second,
   215  			},
   216  			args: args{
   217  				ctx:        context.Background(),
   218  				collection: "ligplaatsen",
   219  				queryParams: datasources.FeaturesCriteria{
   220  					Cursor: domain.DecodedCursor{FID: 0, FiltersChecksum: []byte{}},
   221  					Limit:  2,
   222  					TemporalCriteria: datasources.TemporalCriteria{
   223  						ReferenceDate:     refDate,
   224  						StartDateProperty: "datum_strt",
   225  						EndDateProperty:   "datum_eind",
   226  					},
   227  				},
   228  			},
   229  			wantFC: &domain.FeatureCollection{
   230  				NumberReturned: 2,
   231  				Features: []*domain.Feature{
   232  					{
   233  						Feature: geojson.Feature{
   234  							Properties: map[string]any{
   235  								"straatnaam": "Van Diemenkade",
   236  								"nummer_id":  "0363200000454013",
   237  							},
   238  						},
   239  					},
   240  					{
   241  						Feature: geojson.Feature{
   242  							Properties: map[string]any{
   243  								"straatnaam": "Realengracht",
   244  								"nummer_id":  "0363200000398886",
   245  							},
   246  						},
   247  					},
   248  				},
   249  			},
   250  			wantCursor: domain.Cursors{
   251  				Prev: "|",
   252  				Next: "Dv4|", // 3838
   253  			},
   254  			wantErr: false,
   255  		},
   256  		{
   257  			name: "fail on non existing collection",
   258  			fields: fields{
   259  				backend:          newAddressesGeoPackage(),
   260  				fidColumn:        "feature_id",
   261  				featureTableByID: map[string]*featureTable{"ligplaatsen": {TableName: "ligplaatsen", GeometryColumnName: "geom"}},
   262  				queryTimeout:     5 * time.Second,
   263  			},
   264  			args: args{
   265  				ctx:        context.Background(),
   266  				collection: "vakantiehuizen", // not in gpkg
   267  				queryParams: datasources.FeaturesCriteria{
   268  					Cursor: domain.DecodedCursor{FID: 0, FiltersChecksum: []byte{}},
   269  					Limit:  10,
   270  				},
   271  			},
   272  			wantFC:     nil,
   273  			wantCursor: domain.Cursors{},
   274  			wantErr:    true, // should fail
   275  		},
   276  	}
   277  	for _, tt := range tests {
   278  		t.Run(tt.name, func(t *testing.T) {
   279  			g := &GeoPackage{
   280  				backend:                    tt.fields.backend,
   281  				fidColumn:                  tt.fields.fidColumn,
   282  				featureTableByCollectionID: tt.fields.featureTableByID,
   283  				queryTimeout:               tt.fields.queryTimeout,
   284  			}
   285  			g.preparedStmtCache = NewCache()
   286  
   287  			fc, cursor, err := g.GetFeatures(tt.args.ctx, tt.args.collection, tt.args.queryParams)
   288  			if err != nil {
   289  				if !tt.wantErr {
   290  					t.Errorf("GetFeatures, error %v, wantErr %v", err, tt.wantErr)
   291  				}
   292  				return
   293  			}
   294  			assert.Equal(t, tt.wantFC.NumberReturned, fc.NumberReturned)
   295  			assert.Equal(t, len(tt.wantFC.Features), fc.NumberReturned)
   296  			for i, wantedFeature := range tt.wantFC.Features {
   297  				assert.Equal(t, wantedFeature.Properties["straatnaam"], fc.Features[i].Properties["straatnaam"])
   298  				assert.Equal(t, wantedFeature.Properties["nummer_id"], fc.Features[i].Properties["nummer_id"])
   299  			}
   300  			assert.Equal(t, tt.wantCursor.Prev, cursor.Prev)
   301  			assert.Equal(t, tt.wantCursor.Next, cursor.Next)
   302  		})
   303  	}
   304  }
   305  
   306  func TestGeoPackage_GetFeature(t *testing.T) {
   307  	type fields struct {
   308  		backend          geoPackageBackend
   309  		fidColumn        string
   310  		featureTableByID map[string]*featureTable
   311  		queryTimeout     time.Duration
   312  	}
   313  	type args struct {
   314  		ctx        context.Context
   315  		collection string
   316  		featureID  int64
   317  	}
   318  	tests := []struct {
   319  		name    string
   320  		fields  fields
   321  		args    args
   322  		want    *domain.Feature
   323  		wantErr bool
   324  	}{
   325  		{
   326  			name: "get feature",
   327  			fields: fields{
   328  				backend:          newAddressesGeoPackage(),
   329  				fidColumn:        "feature_id",
   330  				featureTableByID: map[string]*featureTable{"ligplaatsen": {TableName: "ligplaatsen", GeometryColumnName: "geom"}},
   331  				queryTimeout:     5 * time.Second,
   332  			},
   333  			args: args{
   334  				ctx:        context.Background(),
   335  				collection: "ligplaatsen",
   336  				featureID:  3837,
   337  			},
   338  			want: &domain.Feature{
   339  				ID:    0,
   340  				Links: nil,
   341  				Feature: geojson.Feature{
   342  					Properties: map[string]any{
   343  						"straatnaam": "Realengracht",
   344  						"nummer_id":  "0363200000398886",
   345  					},
   346  				},
   347  			},
   348  			wantErr: false,
   349  		},
   350  		{
   351  			name: "get non existing feature",
   352  			fields: fields{
   353  				backend:          newAddressesGeoPackage(),
   354  				fidColumn:        "feature_id",
   355  				featureTableByID: map[string]*featureTable{"ligplaatsen": {TableName: "ligplaatsen", GeometryColumnName: "geom"}},
   356  				queryTimeout:     5 * time.Second,
   357  			},
   358  			args: args{
   359  				ctx:        context.Background(),
   360  				collection: "ligplaatsen",
   361  				featureID:  999991111111111111,
   362  			},
   363  			want:    nil,
   364  			wantErr: false, // not an error situation
   365  		},
   366  		{
   367  			name: "fail on non existing collection",
   368  			fields: fields{
   369  				backend:          newAddressesGeoPackage(),
   370  				fidColumn:        "feature_id",
   371  				featureTableByID: map[string]*featureTable{"ligplaatsen": {TableName: "ligplaatsen", GeometryColumnName: "geom"}},
   372  				queryTimeout:     5 * time.Second,
   373  			},
   374  			args: args{
   375  				ctx:        context.Background(),
   376  				collection: "vakantieparken", // not in gpkg
   377  				featureID:  3837,
   378  			},
   379  			want:    nil,
   380  			wantErr: true,
   381  		},
   382  	}
   383  	for _, tt := range tests {
   384  		t.Run(tt.name, func(t *testing.T) {
   385  			g := &GeoPackage{
   386  				backend:                    tt.fields.backend,
   387  				fidColumn:                  tt.fields.fidColumn,
   388  				featureTableByCollectionID: tt.fields.featureTableByID,
   389  				queryTimeout:               tt.fields.queryTimeout,
   390  			}
   391  			got, err := g.GetFeature(tt.args.ctx, tt.args.collection, tt.args.featureID)
   392  			if err != nil {
   393  				if !tt.wantErr {
   394  					t.Errorf("GetFeature, error %v, wantErr %v", err, tt.wantErr)
   395  				}
   396  				return
   397  			}
   398  			if tt.want != nil {
   399  				assert.Equal(t, tt.want.Properties["straatnaam"], got.Properties["straatnaam"])
   400  				assert.Equal(t, tt.want.Properties["nummer_id"], got.Properties["nummer_id"])
   401  			}
   402  		})
   403  	}
   404  }
   405  
   406  func TestGeoPackage_Warmup(t *testing.T) {
   407  	t.Run("warmup", func(t *testing.T) {
   408  		g := &GeoPackage{
   409  			backend:                    newAddressesGeoPackage(),
   410  			fidColumn:                  "feature_id",
   411  			featureTableByCollectionID: map[string]*featureTable{"ligplaatsen": {TableName: "ligplaatsen", GeometryColumnName: "geom"}},
   412  			queryTimeout:               5 * time.Second,
   413  		}
   414  		collections :=
   415  			[]config.GeoSpatialCollection{
   416  				{
   417  					ID:       "ligplaatsen",
   418  					Features: &config.CollectionEntryFeatures{},
   419  				},
   420  			}
   421  		err := warmUpFeatureTables(collections, g.featureTableByCollectionID, g.backend.getDB())
   422  		assert.NoError(t, err)
   423  	})
   424  }