From ed7704033c8df274d3c0333aae2e6623b1a73e3c Mon Sep 17 00:00:00 2001 From: David Bitner Date: Thu, 12 May 2022 13:44:05 -0500 Subject: [PATCH] Bug fixes for 0.5.1 (#106) * Remove Stable declaration from delete_items function * Python dehydrate marks top-level base_keys as DNM Fixes and tests that dehydration is aware of top-level keys on the base item which should be marked as do-not-merge on the dehydrated item. * Fix test class to use hydration module * update changelog * update tests for changes in fields extension, add simpler testing for sql queries * update version to 0.6.0, update changelog * add triggers to allow deleting collections and cleaning up partitions Co-authored-by: Rob Emanuele Co-authored-by: Matt McFarland --- CHANGELOG.md | 15 + pypgstac/pypgstac/db.py | 10 +- pypgstac/pypgstac/hydration.py | 18 +- pypgstac/pypgstac/load.py | 168 +- .../migrations/pgstac.0.5.1-0.6.0.sql | 798 +++++ pypgstac/pypgstac/migrations/pgstac.0.6.0.sql | 2808 +++++++++++++++++ pypgstac/pypgstac/pypgstac.py | 9 +- pypgstac/pypgstac/version.py | 3 +- pypgstac/tests/conftest.py | 19 +- pypgstac/tests/hydration/__init__.py | 0 pypgstac/tests/hydration/test_dehydrate.py | 398 +-- pypgstac/tests/hydration/test_dehydrate_pg.py | 43 + pypgstac/tests/hydration/test_hydrate.py | 354 ++- pypgstac/tests/hydration/test_hydrate_pg.py | 44 + pypgstac/tests/test_load.py | 17 + scripts/bin/testdb | 20 + sql/001a_jsonutils.sql | 160 + sql/001s_stacutils.sql | 18 +- sql/002_collections.sql | 29 +- sql/003_items.sql | 173 +- sql/004_search.sql | 6 +- sql/999_version.sql | 2 +- test/basic/cql2_searches.sql | 35 + test/basic/cql2_searches.sql.out | 52 + test/basic/cql_searches.sql | 41 + test/basic/cql_searches.sql.out | 63 + test/basic/sqltest.sh | 63 + test/basic/xyz_searches.sql | 13 + test/basic/xyz_searches.sql.out | 22 + test/pgtap.sql | 2 +- test/pgtap/003_items.sql | 33 + test/pgtap/004_search.sql | 267 +- test/pgtap/006_tilesearch.sql | 41 - test/testdata/items.pgcopy | 100 + 34 files changed, 4998 insertions(+), 846 deletions(-) create mode 100644 pypgstac/pypgstac/migrations/pgstac.0.5.1-0.6.0.sql create mode 100644 pypgstac/pypgstac/migrations/pgstac.0.6.0.sql create mode 100644 pypgstac/tests/hydration/__init__.py create mode 100644 pypgstac/tests/hydration/test_dehydrate_pg.py create mode 100644 pypgstac/tests/hydration/test_hydrate_pg.py create mode 100644 test/basic/cql2_searches.sql create mode 100644 test/basic/cql2_searches.sql.out create mode 100644 test/basic/cql_searches.sql create mode 100644 test/basic/cql_searches.sql.out create mode 100755 test/basic/sqltest.sh create mode 100644 test/basic/xyz_searches.sql create mode 100644 test/basic/xyz_searches.sql.out create mode 100644 test/testdata/items.pgcopy diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ffef7b7..6c74bb90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,19 @@ # Changelog +## [v0.6.0] + +### Fixed +- Fix function signatures for transactional functions (delete_item etc) to make sure that they are marked as volatile +- Fix function for getting start/end dates from a stac item +### Changed +- Update hydration/dehydration logic to make sure that it matches hydration/dehydration in pypgstac +- Update fields logic in pgstac to only use full paths and to match logic in stac-fastapi +- Always include id and collection on features regardless of fields setting +### Added +- Add tests to ensure that pgstac and pypgstac hydration logic is equivalent +- Add conf item to search to allow returning results without hydrating. This allows an application using pgstac to shift the CPU load of rehydrating items from the database onto the application server. +- Add "--dehydrated" option to loader to be able to load a dehydrated file (or iterable) of items such as would be output using pg_dump or postgresql copy. +- Add "--chunksize" option to loader that can split the processing of an iterable or file into chunks of n records at a time + ## [v0.5.1] ### Fixed diff --git a/pypgstac/pypgstac/db.py b/pypgstac/pypgstac/db.py index 3b6f032c..2dea2424 100644 --- a/pypgstac/pypgstac/db.py +++ b/pypgstac/pypgstac/db.py @@ -251,11 +251,15 @@ def func(self, function_name: str, *args: Any) -> Generator: """Call a database function.""" placeholders = sql.SQL(", ").join(sql.Placeholder() * len(args)) func = sql.Identifier(function_name) + cleaned_args = [] + for arg in args: + if isinstance(arg, dict): + cleaned_args.append(psycopg.types.json.Jsonb(arg)) + else: + cleaned_args.append(arg) base_query = sql.SQL("SELECT * FROM {}({});").format(func, placeholders) - return self.query(base_query, *args) + return self.query(base_query, cleaned_args) def search(self, query: Union[dict, str, psycopg.types.json.Jsonb] = "{}") -> str: """Search PgStac.""" - if isinstance(query, dict): - query = psycopg.types.json.Jsonb(query) return dumps(next(self.func("search", query))[0]) diff --git a/pypgstac/pypgstac/hydration.py b/pypgstac/pypgstac/hydration.py index 17c8937f..4d151b1f 100644 --- a/pypgstac/pypgstac/hydration.py +++ b/pypgstac/pypgstac/hydration.py @@ -1,3 +1,4 @@ +"""Hydrate data in pypgstac rather than on the database.""" from copy import deepcopy from typing import Any, Dict @@ -11,7 +12,6 @@ def hydrate(base_item: Dict[str, Any], item: Dict[str, Any]) -> Dict[str, Any]: This will not perform a deep copy; values of the original item will be referenced in the return item. """ - # Merge will mutate i, but create deep copies of values in the base item # This will prevent the base item values from being mutated, e.g. by # filtering out fields in `filter_fields`. @@ -103,6 +103,10 @@ def strip(base_value: Dict[str, Any], item_value: Dict[str, Any]) -> Dict[str, A else: # Unequal non-dict values are copied over from the incoming item out[key] = value + + # Mark any top-level keys from the base_item that are not in the incoming item + apply_marked_keys(base_value, item_value, out) + return out return strip(base_item, full_item) @@ -113,13 +117,17 @@ def apply_marked_keys( full_item: Dict[str, Any], dehydrated: Dict[str, Any], ) -> None: - """ + """Mark keys. + Mark any keys that are present on the base item but not in the incoming item as `do-not-merge` on the dehydrated item. This will prevent they key from being rehydrated. This modifies the dehydrated item in-place. """ - marked_keys = [key for key in base_item if key not in full_item.keys()] - marked_dict = {k: DO_NOT_MERGE_MARKER for k in marked_keys} - dehydrated.update(marked_dict) + try: + marked_keys = [key for key in base_item if key not in full_item.keys()] + marked_dict = {k: DO_NOT_MERGE_MARKER for k in marked_keys} + dehydrated.update(marked_dict) + except TypeError: + pass diff --git a/pypgstac/pypgstac/load.py b/pypgstac/pypgstac/load.py index 89de719b..4e87a9cf 100644 --- a/pypgstac/pypgstac/load.py +++ b/pypgstac/pypgstac/load.py @@ -13,14 +13,13 @@ Dict, Iterable, Iterator, - List, Optional, Tuple, Union, Generator, TextIO, ) - +import csv import orjson import psycopg from orjson import JSONDecodeError @@ -42,6 +41,16 @@ logger = logging.getLogger(__name__) +def chunked_iterable(iterable: Iterable, size: Optional[int] = 10000) -> Iterable: + """Chunk an iterable.""" + it = iter(iterable) + while True: + chunk = tuple(itertools.islice(it, size)) + if not chunk: + break + yield chunk + + class Tables(str, Enum): """Available tables for loading.""" @@ -133,6 +142,7 @@ class Loader: """Utilities for loading data.""" db: PgstacDB + _partition_cache: Optional[dict] = None @lru_cache def collection_json(self, collection_id: str) -> Tuple[dict, int, str]: @@ -149,6 +159,7 @@ def collection_json(self, collection_id: str) -> Tuple[dict, int, str]: raise Exception( f"Collection {collection_id} is not present in the database" ) + logger.debug(f"Found {collection_id} with base_item {base_item}") return base_item, key, partition_trunc def load_collections( @@ -270,7 +281,16 @@ def load_partition( ) as copy: for item in items: item.pop("partition") - copy.write_row(list(item.values())) + copy.write_row( + ( + item["id"], + item["collection"], + item["datetime"], + item["end_datetime"], + item["geometry"], + item["content"], + ) + ) logger.debug(cur.statusmessage) logger.debug(f"Rows affected: {cur.rowcount}") elif insert_mode in ( @@ -295,7 +315,16 @@ def load_partition( ) as copy: for item in items: item.pop("partition") - copy.write_row(list(item.values())) + copy.write_row( + ( + item["id"], + item["collection"], + item["datetime"], + item["end_datetime"], + item["geometry"], + item["content"], + ) + ) logger.debug(cur.statusmessage) logger.debug(f"Copied rows: {cur.rowcount}") @@ -369,61 +398,102 @@ def load_partition( f"Copying data for {partition} took {time.perf_counter() - t} seconds" ) + def _partition_update(self, item: dict) -> str: + + p = item.get("partition", None) + if p is None: + _, key, partition_trunc = self.collection_json(item["collection"]) + if partition_trunc == "year": + pd = item["datetime"].replace("-", "")[:4] + p = f"_items_{key}_{pd}" + elif partition_trunc == "month": + pd = item["datetime"].replace("-", "")[:6] + p = f"_items_{key}_{pd}" + else: + p = f"_items_{key}" + item["partition"] = p + + if self._partition_cache is None: + self._partition_cache = {} + + partition = self._partition_cache.get( + item["partition"], + { + "partition": None, + "collection": None, + "mindt": None, + "maxdt": None, + "minedt": None, + "maxedt": None, + }, + ) + + partition["partition"] = item["partition"] + partition["collection"] = item["collection"] + if partition["mindt"] is None or item["datetime"] < partition["mindt"]: + partition["mindt"] = item["datetime"] + + if partition["maxdt"] is None or item["datetime"] > partition["maxdt"]: + partition["maxdt"] = item["datetime"] + + if partition["minedt"] is None or item["end_datetime"] < partition["minedt"]: + partition["minedt"] = item["end_datetime"] + + if partition["maxedt"] is None or item["end_datetime"] > partition["maxedt"]: + partition["maxedt"] = item["end_datetime"] + self._partition_cache[item["partition"]] = partition + + return p + + def read_dehydrated(self, file: Union[Path, str] = "stdin") -> Generator: + if file is None: + file = "stdin" + if isinstance(file, str): + open_file: Any = open_std(file, "r") + with open_file as f: + fields = [ + "id", + "geometry", + "collection", + "datetime", + "end_datetime", + "content", + ] + csvreader = csv.DictReader(f, fields, delimiter="\t") + for item in csvreader: + item["partition"] = self._partition_update(item) + yield item + + def read_hydrated( + self, file: Union[Path, str, Iterator[Any]] = "stdin" + ) -> Generator: + for line in read_json(file): + item = self.format_item(line) + item["partition"] = self._partition_update(item) + yield item + def load_items( self, file: Union[Path, str, Iterator[Any]] = "stdin", insert_mode: Optional[Methods] = Methods.insert, + dehydrated: Optional[bool] = False, + chunksize: Optional[int] = 10000, ) -> None: """Load items json records.""" if file is None: file = "stdin" t = time.perf_counter() - items: List = [] - partitions: dict = {} - for line in read_json(file): - item = self.format_item(line) - items.append(item) - partition = partitions.get( - item["partition"], - { - "partition": None, - "collection": None, - "mindt": None, - "maxdt": None, - "minedt": None, - "maxedt": None, - }, - ) - partition["partition"] = item["partition"] - partition["collection"] = item["collection"] - if partition["mindt"] is None or item["datetime"] < partition["mindt"]: - partition["mindt"] = item["datetime"] - - if partition["maxdt"] is None or item["datetime"] > partition["maxdt"]: - partition["maxdt"] = item["datetime"] - - if ( - partition["minedt"] is None - or item["end_datetime"] < partition["minedt"] - ): - partition["minedt"] = item["end_datetime"] - - if ( - partition["maxedt"] is None - or item["end_datetime"] > partition["maxedt"] - ): - partition["maxedt"] = item["end_datetime"] - partitions[item["partition"]] = partition - logger.debug( - f"Loading and parsing data took {time.perf_counter() - t} seconds." - ) - t = time.perf_counter() - items.sort(key=lambda x: x["partition"]) - logger.debug(f"Sorting data took {time.perf_counter() - t} seconds.") - t = time.perf_counter() + self._partition_cache = {} + + if dehydrated and isinstance(file, str): + items = self.read_dehydrated(file) + else: + items = self.read_hydrated(file) - for k, g in itertools.groupby(items, lambda x: x["partition"]): - self.load_partition(partitions[k], g, insert_mode) + for chunk in chunked_iterable(items, chunksize): + list(chunk).sort(key=lambda x: x["partition"]) + for k, g in itertools.groupby(chunk, lambda x: x["partition"]): + self.load_partition(self._partition_cache[k], g, insert_mode) logger.debug(f"Adding data to database took {time.perf_counter() - t} seconds.") diff --git a/pypgstac/pypgstac/migrations/pgstac.0.5.1-0.6.0.sql b/pypgstac/pypgstac/migrations/pgstac.0.5.1-0.6.0.sql new file mode 100644 index 00000000..ddacaf73 --- /dev/null +++ b/pypgstac/pypgstac/migrations/pgstac.0.5.1-0.6.0.sql @@ -0,0 +1,798 @@ +SET SEARCH_PATH to pgstac, public; +alter table "pgstac"."partitions" drop constraint "partitions_collection_fkey"; + +drop function if exists "pgstac"."content_hydrate"(_item jsonb, _collection jsonb, fields jsonb); + +drop function if exists "pgstac"."content_slim"(_item jsonb, _collection jsonb); + +drop function if exists "pgstac"."key_filter"(k text, val jsonb, INOUT kf jsonb, OUT include boolean); + +drop function if exists "pgstac"."strip_assets"(a jsonb); + +alter table "pgstac"."partitions" add constraint "partitions_collection_fkey" FOREIGN KEY (collection) REFERENCES pgstac.collections(id) ON DELETE CASCADE; + +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION pgstac.content_hydrate(_base_item jsonb, _item jsonb, fields jsonb DEFAULT '{}'::jsonb) + RETURNS jsonb + LANGUAGE sql + IMMUTABLE PARALLEL SAFE +AS $function$ + SELECT merge_jsonb( + jsonb_fields(_item, fields), + jsonb_fields(_base_item, fields) + ); +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.explode_dotpaths(j jsonb) + RETURNS SETOF text[] + LANGUAGE sql + IMMUTABLE PARALLEL SAFE +AS $function$ + SELECT string_to_array(p, '.') as e FROM jsonb_array_elements_text(j) p; +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.explode_dotpaths_recurse(j jsonb) + RETURNS SETOF text[] + LANGUAGE sql + IMMUTABLE PARALLEL SAFE +AS $function$ + WITH RECURSIVE t AS ( + SELECT e FROM explode_dotpaths(j) e + UNION ALL + SELECT e[1:cardinality(e)-1] + FROM t + WHERE cardinality(e)>1 + ) SELECT e FROM t; +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.jsonb_exclude(j jsonb, f jsonb) + RETURNS jsonb + LANGUAGE plpgsql + IMMUTABLE +AS $function$ +DECLARE + excludes jsonb := f-> 'exclude'; + outj jsonb := j; + path text[]; +BEGIN + IF + excludes IS NULL + OR jsonb_array_length(excludes) = 0 + THEN + RETURN j; + ELSE + FOR path IN SELECT explode_dotpaths(excludes) LOOP + outj := outj #- path; + END LOOP; + END IF; + RETURN outj; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.jsonb_fields(j jsonb, f jsonb DEFAULT '{"fields": []}'::jsonb) + RETURNS jsonb + LANGUAGE sql + IMMUTABLE +AS $function$ + SELECT jsonb_exclude(jsonb_include(j, f), f); +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.jsonb_include(j jsonb, f jsonb) + RETURNS jsonb + LANGUAGE plpgsql + IMMUTABLE +AS $function$ +DECLARE + includes jsonb := f-> 'include'; + outj jsonb := '{}'::jsonb; + path text[]; +BEGIN + IF + includes IS NULL + OR jsonb_array_length(includes) = 0 + THEN + RETURN j; + ELSE + includes := includes || '["id","collection"]'::jsonb; + FOR path IN SELECT explode_dotpaths(includes) LOOP + outj := jsonb_set_nested(outj, path, j #> path); + END LOOP; + END IF; + RETURN outj; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.jsonb_set_nested(j jsonb, path text[], val jsonb) + RETURNS jsonb + LANGUAGE plpgsql + IMMUTABLE +AS $function$ +DECLARE +BEGIN + IF cardinality(path) > 1 THEN + FOR i IN 1..(cardinality(path)-1) LOOP + IF j #> path[:i] IS NULL THEN + j := jsonb_set_lax(j, path[:i], '{}', TRUE); + END IF; + END LOOP; + END IF; + RETURN jsonb_set_lax(j, path, val, true); + +END; +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.merge_jsonb(_a jsonb, _b jsonb) + RETURNS jsonb + LANGUAGE sql + IMMUTABLE +AS $function$ + SELECT + CASE + WHEN _a = '"𒍟※"'::jsonb THEN NULL + WHEN _a IS NULL OR jsonb_typeof(_a) = 'null' THEN _b + WHEN jsonb_typeof(_a) = 'object' AND jsonb_typeof(_b) = 'object' THEN + ( + SELECT + jsonb_strip_nulls( + jsonb_object_agg( + key, + merge_jsonb(a.value, b.value) + ) + ) + FROM + jsonb_each(coalesce(_a,'{}'::jsonb)) as a + FULL JOIN + jsonb_each(coalesce(_b,'{}'::jsonb)) as b + USING (key) + ) + WHEN + jsonb_typeof(_a) = 'array' + AND jsonb_typeof(_b) = 'array' + AND jsonb_array_length(_a) = jsonb_array_length(_b) + THEN + ( + SELECT jsonb_agg(m) FROM + ( SELECT + merge_jsonb( + jsonb_array_elements(_a), + jsonb_array_elements(_b) + ) as m + ) as l + ) + ELSE _a + END + ; +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.partitions_delete_trigger_func() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ +DECLARE + q text; +BEGIN + RAISE NOTICE 'Partition Delete Trigger. %', OLD.name; + EXECUTE format($q$ + DROP TABLE IF EXISTS %I CASCADE; + $q$, + OLD.name + ); + RAISE NOTICE 'Dropped partition.'; + RETURN OLD; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.strip_jsonb(_a jsonb, _b jsonb) + RETURNS jsonb + LANGUAGE sql + IMMUTABLE +AS $function$ + SELECT + CASE + + WHEN (_a IS NULL OR jsonb_typeof(_a) = 'null') AND _b IS NOT NULL AND jsonb_typeof(_b) != 'null' THEN '"𒍟※"'::jsonb + WHEN _b IS NULL OR jsonb_typeof(_a) = 'null' THEN _a + WHEN _a = _b AND jsonb_typeof(_a) = 'object' THEN '{}'::jsonb + WHEN _a = _b THEN NULL + WHEN jsonb_typeof(_a) = 'object' AND jsonb_typeof(_b) = 'object' THEN + ( + SELECT + jsonb_strip_nulls( + jsonb_object_agg( + key, + strip_jsonb(a.value, b.value) + ) + ) + FROM + jsonb_each(_a) as a + FULL JOIN + jsonb_each(_b) as b + USING (key) + ) + WHEN + jsonb_typeof(_a) = 'array' + AND jsonb_typeof(_b) = 'array' + AND jsonb_array_length(_a) = jsonb_array_length(_b) + THEN + ( + SELECT jsonb_agg(m) FROM + ( SELECT + strip_jsonb( + jsonb_array_elements(_a), + jsonb_array_elements(_b) + ) as m + ) as l + ) + ELSE _a + END + ; +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.collection_base_item(content jsonb) + RETURNS jsonb + LANGUAGE sql + IMMUTABLE PARALLEL SAFE +AS $function$ + SELECT jsonb_build_object( + 'type', 'Feature', + 'stac_version', content->'stac_version', + 'assets', content->'item_assets', + 'collection', content->'id' + ); +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.collections_trigger_func() + RETURNS trigger + LANGUAGE plpgsql + SET search_path TO 'pgstac', 'public' +AS $function$ +DECLARE + q text; + partition_name text := format('_items_%s', NEW.key); + partition_exists boolean := false; + partition_empty boolean := true; + err_context text; + loadtemp boolean := FALSE; +BEGIN + RAISE NOTICE 'Collection Trigger. % %', NEW.id, NEW.key; + SELECT relid::text INTO partition_name + FROM pg_partition_tree('items') + WHERE relid::text = partition_name; + IF FOUND THEN + partition_exists := true; + partition_empty := table_empty(partition_name); + ELSE + partition_exists := false; + partition_empty := true; + partition_name := format('_items_%s', NEW.key); + END IF; + IF TG_OP = 'UPDATE' AND NEW.partition_trunc IS DISTINCT FROM OLD.partition_trunc AND partition_empty THEN + q := format($q$ + DROP TABLE IF EXISTS %I CASCADE; + $q$, + partition_name + ); + EXECUTE q; + END IF; + IF TG_OP = 'UPDATE' AND NEW.partition_trunc IS DISTINCT FROM OLD.partition_trunc AND partition_exists AND NOT partition_empty THEN + q := format($q$ + CREATE TEMP TABLE changepartitionstaging ON COMMIT DROP AS SELECT * FROM %I; + DROP TABLE IF EXISTS %I CASCADE; + $q$, + partition_name, + partition_name + ); + EXECUTE q; + loadtemp := TRUE; + partition_empty := TRUE; + partition_exists := FALSE; + END IF; + IF TG_OP = 'UPDATE' AND NEW.partition_trunc IS NOT DISTINCT FROM OLD.partition_trunc THEN + RETURN NEW; + END IF; + IF NEW.partition_trunc IS NULL AND partition_empty THEN + RAISE NOTICE '% % % %', + partition_name, + NEW.id, + concat(partition_name,'_id_idx'), + partition_name + ; + q := format($q$ + CREATE TABLE IF NOT EXISTS %I partition OF items FOR VALUES IN (%L); + CREATE UNIQUE INDEX IF NOT EXISTS %I ON %I (id); + $q$, + partition_name, + NEW.id, + concat(partition_name,'_id_idx'), + partition_name + ); + RAISE NOTICE 'q: %', q; + BEGIN + EXECUTE q; + EXCEPTION + WHEN duplicate_table THEN + RAISE NOTICE 'Partition % already exists.', partition_name; + WHEN others THEN + GET STACKED DIAGNOSTICS err_context = PG_EXCEPTION_CONTEXT; + RAISE INFO 'Error Name:%',SQLERRM; + RAISE INFO 'Error State:%', SQLSTATE; + RAISE INFO 'Error Context:%', err_context; + END; + + ALTER TABLE partitions DISABLE TRIGGER partitions_delete_trigger; + DELETE FROM partitions WHERE collection=NEW.id AND name=partition_name; + ALTER TABLE partitions ENABLE TRIGGER partitions_delete_trigger; + + INSERT INTO partitions (collection, name) VALUES (NEW.id, partition_name); + ELSIF partition_empty THEN + q := format($q$ + CREATE TABLE IF NOT EXISTS %I partition OF items FOR VALUES IN (%L) + PARTITION BY RANGE (datetime); + $q$, + partition_name, + NEW.id + ); + RAISE NOTICE 'q: %', q; + BEGIN + EXECUTE q; + EXCEPTION + WHEN duplicate_table THEN + RAISE NOTICE 'Partition % already exists.', partition_name; + WHEN others THEN + GET STACKED DIAGNOSTICS err_context = PG_EXCEPTION_CONTEXT; + RAISE INFO 'Error Name:%',SQLERRM; + RAISE INFO 'Error State:%', SQLSTATE; + RAISE INFO 'Error Context:%', err_context; + END; + ALTER TABLE partitions DISABLE TRIGGER partitions_delete_trigger; + DELETE FROM partitions WHERE collection=NEW.id AND name=partition_name; + ALTER TABLE partitions ENABLE TRIGGER partitions_delete_trigger; + ELSE + RAISE EXCEPTION 'Cannot modify partition % unless empty', partition_name; + END IF; + IF loadtemp THEN + RAISE NOTICE 'Moving data into new partitions.'; + q := format($q$ + WITH p AS ( + SELECT + collection, + datetime as datetime, + end_datetime as end_datetime, + (partition_name( + collection, + datetime + )).partition_name as name + FROM changepartitionstaging + ) + INSERT INTO partitions (collection, datetime_range, end_datetime_range) + SELECT + collection, + tstzrange(min(datetime), max(datetime), '[]') as datetime_range, + tstzrange(min(end_datetime), max(end_datetime), '[]') as end_datetime_range + FROM p + GROUP BY collection, name + ON CONFLICT (name) DO UPDATE SET + datetime_range = EXCLUDED.datetime_range, + end_datetime_range = EXCLUDED.end_datetime_range + ; + INSERT INTO %I SELECT * FROM changepartitionstaging; + DROP TABLE IF EXISTS changepartitionstaging; + $q$, + partition_name + ); + EXECUTE q; + END IF; + RETURN NEW; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.content_hydrate(_item pgstac.items, _collection pgstac.collections, fields jsonb DEFAULT '{}'::jsonb) + RETURNS jsonb + LANGUAGE plpgsql + STABLE PARALLEL SAFE +AS $function$ +DECLARE + geom jsonb; + bbox jsonb; + output jsonb; + content jsonb; + base_item jsonb := _collection.base_item; +BEGIN + IF include_field('geometry', fields) THEN + geom := ST_ASGeoJson(_item.geometry, 20)::jsonb; + END IF; + output := content_hydrate( + jsonb_build_object( + 'id', _item.id, + 'geometry', geom, + 'collection', _item.collection, + 'type', 'Feature' + ) || _item.content, + _collection.base_item, + fields + ); + + RETURN output; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.content_nonhydrated(_item pgstac.items, fields jsonb DEFAULT '{}'::jsonb) + RETURNS jsonb + LANGUAGE plpgsql + STABLE PARALLEL SAFE +AS $function$ +DECLARE + geom jsonb; + bbox jsonb; + output jsonb; +BEGIN + IF include_field('geometry', fields) THEN + geom := ST_ASGeoJson(_item.geometry, 20)::jsonb; + END IF; + output := jsonb_build_object( + 'id', _item.id, + 'geometry', geom, + 'collection', _item.collection, + 'type', 'Feature' + ) || _item.content; + RETURN output; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.content_slim(_item jsonb) + RETURNS jsonb + LANGUAGE sql + IMMUTABLE PARALLEL SAFE +AS $function$ + SELECT strip_jsonb(_item - '{id,geometry,collection,type}'::text[], collection_base_item(_item->>'collection')) - '{id,geometry,collection,type}'::text[]; +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.delete_item(_id text, _collection text DEFAULT NULL::text) + RETURNS void + LANGUAGE plpgsql +AS $function$ +DECLARE +out items%ROWTYPE; +BEGIN + DELETE FROM items WHERE id = _id AND (_collection IS NULL OR collection=_collection) RETURNING * INTO STRICT out; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.include_field(f text, fields jsonb DEFAULT '{}'::jsonb) + RETURNS boolean + LANGUAGE plpgsql + IMMUTABLE +AS $function$ +DECLARE + includes jsonb := fields->'include'; + excludes jsonb := fields->'exclude'; +BEGIN + IF f IS NULL THEN + RETURN NULL; + END IF; + + + IF + jsonb_typeof(excludes) = 'array' + AND jsonb_array_length(excludes)>0 + AND excludes ? f + THEN + RETURN FALSE; + END IF; + + IF + ( + jsonb_typeof(includes) = 'array' + AND jsonb_array_length(includes) > 0 + AND includes ? f + ) OR + ( + includes IS NULL + OR jsonb_typeof(includes) = 'null' + OR jsonb_array_length(includes) = 0 + ) + THEN + RETURN TRUE; + END IF; + + RETURN FALSE; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.partition_name(collection text, dt timestamp with time zone, OUT partition_name text, OUT partition_range tstzrange) + RETURNS record + LANGUAGE plpgsql + STABLE +AS $function$ +DECLARE + c RECORD; + parent_name text; +BEGIN + SELECT * INTO c FROM pgstac.collections WHERE id=collection; + IF NOT FOUND THEN + RAISE EXCEPTION 'Collection % does not exist', collection USING ERRCODE = 'foreign_key_violation', HINT = 'Make sure collection exists before adding items'; + END IF; + parent_name := format('_items_%s', c.key); + + + IF c.partition_trunc = 'year' THEN + partition_name := format('%s_%s', parent_name, to_char(dt,'YYYY')); + ELSIF c.partition_trunc = 'month' THEN + partition_name := format('%s_%s', parent_name, to_char(dt,'YYYYMM')); + ELSE + partition_name := parent_name; + partition_range := tstzrange('-infinity'::timestamptz, 'infinity'::timestamptz, '[]'); + END IF; + IF partition_range IS NULL THEN + partition_range := tstzrange( + date_trunc(c.partition_trunc::text, dt), + date_trunc(c.partition_trunc::text, dt) + concat('1 ', c.partition_trunc)::interval + ); + END IF; + RETURN; + +END; +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.search(_search jsonb DEFAULT '{}'::jsonb) + RETURNS jsonb + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'pgstac', 'public' +AS $function$ +DECLARE + searches searches%ROWTYPE; + _where text; + token_where text; + full_where text; + orderby text; + query text; + token_type text := substr(_search->>'token',1,4); + _limit int := coalesce((_search->>'limit')::int, 10); + curs refcursor; + cntr int := 0; + iter_record items%ROWTYPE; + first_record jsonb; + first_item items%ROWTYPE; + last_item items%ROWTYPE; + last_record jsonb; + out_records jsonb := '[]'::jsonb; + prev_query text; + next text; + prev_id text; + has_next boolean := false; + has_prev boolean := false; + prev text; + total_count bigint; + context jsonb; + collection jsonb; + includes text[]; + excludes text[]; + exit_flag boolean := FALSE; + batches int := 0; + timer timestamptz := clock_timestamp(); + pstart timestamptz; + pend timestamptz; + pcurs refcursor; + search_where search_wheres%ROWTYPE; + id text; +BEGIN +CREATE TEMP TABLE results (content jsonb) ON COMMIT DROP; +-- if ids is set, short circuit and just use direct ids query for each id +-- skip any paging or caching +-- hard codes ordering in the same order as the array of ids +IF _search ? 'ids' THEN + INSERT INTO results + SELECT + CASE WHEN _search->'conf'->>'nohydrate' IS NOT NULL AND (_search->'conf'->>'nohydrate')::boolean = true THEN + content_nonhydrated(items, _search->'fields') + ELSE + content_hydrate(items, _search->'fields') + END + FROM items WHERE + items.id = ANY(to_text_array(_search->'ids')) + AND + CASE WHEN _search ? 'collections' THEN + items.collection = ANY(to_text_array(_search->'collections')) + ELSE TRUE + END + ORDER BY items.datetime desc, items.id desc + ; + SELECT INTO total_count count(*) FROM results; +ELSE + searches := search_query(_search); + _where := searches._where; + orderby := searches.orderby; + search_where := where_stats(_where); + total_count := coalesce(search_where.total_count, search_where.estimated_count); + + IF token_type='prev' THEN + token_where := get_token_filter(_search, null::jsonb); + orderby := sort_sqlorderby(_search, TRUE); + END IF; + IF token_type='next' THEN + token_where := get_token_filter(_search, null::jsonb); + END IF; + + full_where := concat_ws(' AND ', _where, token_where); + RAISE NOTICE 'FULL QUERY % %', full_where, clock_timestamp()-timer; + timer := clock_timestamp(); + + FOR query IN SELECT partition_queries(full_where, orderby, search_where.partitions) LOOP + timer := clock_timestamp(); + query := format('%s LIMIT %s', query, _limit + 1); + RAISE NOTICE 'Partition Query: %', query; + batches := batches + 1; + -- curs = create_cursor(query); + OPEN curs FOR EXECUTE query; + LOOP + FETCH curs into iter_record; + EXIT WHEN NOT FOUND; + cntr := cntr + 1; + + IF _search->'conf'->>'nohydrate' IS NOT NULL AND (_search->'conf'->>'nohydrate')::boolean = true THEN + last_record := content_nonhydrated(iter_record, _search->'fields'); + ELSE + last_record := content_hydrate(iter_record, _search->'fields'); + END IF; + last_item := iter_record; + IF cntr = 1 THEN + first_item := last_item; + first_record := last_record; + END IF; + IF cntr <= _limit THEN + INSERT INTO results (content) VALUES (last_record); + ELSIF cntr > _limit THEN + has_next := true; + exit_flag := true; + EXIT; + END IF; + END LOOP; + CLOSE curs; + RAISE NOTICE 'Query took %.', clock_timestamp()-timer; + timer := clock_timestamp(); + EXIT WHEN exit_flag; + END LOOP; + RAISE NOTICE 'Scanned through % partitions.', batches; +END IF; + +SELECT jsonb_agg(content) INTO out_records FROM results WHERE content is not NULL; + +DROP TABLE results; + + +-- Flip things around if this was the result of a prev token query +IF token_type='prev' THEN + out_records := flip_jsonb_array(out_records); + first_record := last_record; +END IF; + +-- If this query has a token, see if there is data before the first record +IF _search ? 'token' THEN + prev_query := format( + 'SELECT 1 FROM items WHERE %s LIMIT 1', + concat_ws( + ' AND ', + _where, + trim(get_token_filter(_search, to_jsonb(first_item))) + ) + ); + RAISE NOTICE 'Query to get previous record: % --- %', prev_query, first_record; + EXECUTE prev_query INTO has_prev; + IF FOUND and has_prev IS NOT NULL THEN + RAISE NOTICE 'Query results from prev query: %', has_prev; + has_prev := TRUE; + END IF; +END IF; +has_prev := COALESCE(has_prev, FALSE); + +IF has_prev THEN + prev := out_records->0->>'id'; +END IF; +IF has_next OR token_type='prev' THEN + next := out_records->-1->>'id'; +END IF; + +IF context(_search->'conf') != 'off' THEN + context := jsonb_strip_nulls(jsonb_build_object( + 'limit', _limit, + 'matched', total_count, + 'returned', coalesce(jsonb_array_length(out_records), 0) + )); +ELSE + context := jsonb_strip_nulls(jsonb_build_object( + 'limit', _limit, + 'returned', coalesce(jsonb_array_length(out_records), 0) + )); +END IF; + +collection := jsonb_build_object( + 'type', 'FeatureCollection', + 'features', coalesce(out_records, '[]'::jsonb), + 'next', next, + 'prev', prev, + 'context', context +); + +RETURN collection; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.stac_daterange(value jsonb) + RETURNS tstzrange + LANGUAGE plpgsql + IMMUTABLE PARALLEL SAFE + SET "TimeZone" TO 'UTC' +AS $function$ +DECLARE + props jsonb := value; + dt timestamptz; + edt timestamptz; +BEGIN + IF props ? 'properties' THEN + props := props->'properties'; + END IF; + IF + props ? 'start_datetime' + AND props->>'start_datetime' IS NOT NULL + AND props ? 'end_datetime' + AND props->>'end_datetime' IS NOT NULL + THEN + dt := props->>'start_datetime'; + edt := props->>'end_datetime'; + IF dt > edt THEN + RAISE EXCEPTION 'start_datetime must be < end_datetime'; + END IF; + ELSE + dt := props->>'datetime'; + edt := props->>'datetime'; + END IF; + IF dt is NULL OR edt IS NULL THEN + RAISE NOTICE 'DT: %, EDT: %', dt, edt; + RAISE EXCEPTION 'Either datetime (%) or both start_datetime (%) and end_datetime (%) must be set.', props->>'datetime',props->>'start_datetime',props->>'end_datetime'; + END IF; + RETURN tstzrange(dt, edt, '[]'); +END; +$function$ +; + +CREATE OR REPLACE FUNCTION pgstac.update_item(content jsonb) + RETURNS void + LANGUAGE plpgsql + SET search_path TO 'pgstac', 'public' +AS $function$ +DECLARE + old items %ROWTYPE; + out items%ROWTYPE; +BEGIN + PERFORM delete_item(content->>'id', content->>'collection'); + PERFORM create_item(content); +END; +$function$ +; + +CREATE TRIGGER partitions_delete_trigger BEFORE DELETE ON pgstac.partitions FOR EACH ROW EXECUTE FUNCTION pgstac.partitions_delete_trigger_func(); + + + +SELECT set_version('0.6.0'); diff --git a/pypgstac/pypgstac/migrations/pgstac.0.6.0.sql b/pypgstac/pypgstac/migrations/pgstac.0.6.0.sql new file mode 100644 index 00000000..dd013375 --- /dev/null +++ b/pypgstac/pypgstac/migrations/pgstac.0.6.0.sql @@ -0,0 +1,2808 @@ +CREATE EXTENSION IF NOT EXISTS postgis; +CREATE EXTENSION IF NOT EXISTS btree_gist; + +DO $$ + BEGIN + CREATE ROLE pgstac_admin; + CREATE ROLE pgstac_read; + CREATE ROLE pgstac_ingest; + EXCEPTION WHEN duplicate_object THEN + RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE; + END +$$; + +GRANT pgstac_admin TO current_user; + +CREATE SCHEMA IF NOT EXISTS pgstac AUTHORIZATION pgstac_admin; + +ALTER ROLE pgstac_admin SET SEARCH_PATH TO pgstac, public; +ALTER ROLE pgstac_read SET SEARCH_PATH TO pgstac, public; +ALTER ROLE pgstac_ingest SET SEARCH_PATH TO pgstac, public; + +GRANT USAGE ON SCHEMA pgstac to pgstac_read; +ALTER DEFAULT PRIVILEGES IN SCHEMA pgstac GRANT SELECT ON TABLES TO pgstac_read; +ALTER DEFAULT PRIVILEGES IN SCHEMA pgstac GRANT USAGE ON TYPES TO pgstac_read; +ALTER DEFAULT PRIVILEGES IN SCHEMA pgstac GRANT ALL ON SEQUENCES TO pgstac_read; + +GRANT pgstac_read TO pgstac_ingest; +GRANT ALL ON SCHEMA pgstac TO pgstac_ingest; +ALTER DEFAULT PRIVILEGES IN SCHEMA pgstac GRANT ALL ON TABLES TO pgstac_ingest; +ALTER DEFAULT PRIVILEGES IN SCHEMA pgstac GRANT ALL ON FUNCTIONS TO pgstac_ingest; + +SET ROLE pgstac_admin; + +SET SEARCH_PATH TO pgstac, public; + + +CREATE TABLE IF NOT EXISTS migrations ( + version text PRIMARY KEY, + datetime timestamptz DEFAULT clock_timestamp() NOT NULL +); + +CREATE OR REPLACE FUNCTION get_version() RETURNS text AS $$ + SELECT version FROM pgstac.migrations ORDER BY datetime DESC, version DESC LIMIT 1; +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION set_version(text) RETURNS text AS $$ + INSERT INTO pgstac.migrations (version) VALUES ($1) + ON CONFLICT DO NOTHING + RETURNING version; +$$ LANGUAGE SQL; + + +CREATE TABLE IF NOT EXISTS pgstac_settings ( + name text PRIMARY KEY, + value text NOT NULL +); + +INSERT INTO pgstac_settings (name, value) VALUES + ('context', 'off'), + ('context_estimated_count', '100000'), + ('context_estimated_cost', '100000'), + ('context_stats_ttl', '1 day'), + ('default-filter-lang', 'cql2-json'), + ('additional_properties', 'true') +ON CONFLICT DO NOTHING +; + + +CREATE OR REPLACE FUNCTION get_setting(IN _setting text, IN conf jsonb DEFAULT NULL) RETURNS text AS $$ +SELECT COALESCE( + conf->>_setting, + current_setting(concat('pgstac.',_setting), TRUE), + (SELECT value FROM pgstac.pgstac_settings WHERE name=_setting) +); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION context(conf jsonb DEFAULT NULL) RETURNS text AS $$ + SELECT pgstac.get_setting('context', conf); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION context_estimated_count(conf jsonb DEFAULT NULL) RETURNS int AS $$ + SELECT pgstac.get_setting('context_estimated_count', conf)::int; +$$ LANGUAGE SQL; + +DROP FUNCTION IF EXISTS context_estimated_cost(); +CREATE OR REPLACE FUNCTION context_estimated_cost(conf jsonb DEFAULT NULL) RETURNS float AS $$ + SELECT pgstac.get_setting('context_estimated_cost', conf)::float; +$$ LANGUAGE SQL; + +DROP FUNCTION IF EXISTS context_stats_ttl(); +CREATE OR REPLACE FUNCTION context_stats_ttl(conf jsonb DEFAULT NULL) RETURNS interval AS $$ + SELECT pgstac.get_setting('context_stats_ttl', conf)::interval; +$$ LANGUAGE SQL; + + +CREATE OR REPLACE FUNCTION notice(VARIADIC text[]) RETURNS boolean AS $$ +DECLARE +debug boolean := current_setting('pgstac.debug', true); +BEGIN + IF debug THEN + RAISE NOTICE 'NOTICE FROM FUNC: % >>>>> %', concat_ws(' | ', $1), clock_timestamp(); + RETURN TRUE; + END IF; + RETURN FALSE; +END; +$$ LANGUAGE PLPGSQL; + +CREATE OR REPLACE FUNCTION empty_arr(ANYARRAY) RETURNS BOOLEAN AS $$ +SELECT CASE + WHEN $1 IS NULL THEN TRUE + WHEN cardinality($1)<1 THEN TRUE +ELSE FALSE +END; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; + +CREATE OR REPLACE FUNCTION array_intersection(_a ANYARRAY, _b ANYARRAY) RETURNS ANYARRAY AS $$ + SELECT ARRAY ( SELECT unnest(_a) INTERSECT SELECT UNNEST(_b) ); +$$ LANGUAGE SQL IMMUTABLE; + + +CREATE OR REPLACE FUNCTION array_map_ident(_a text[]) + RETURNS text[] AS $$ + SELECT array_agg(quote_ident(v)) FROM unnest(_a) v; +$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE; + +CREATE OR REPLACE FUNCTION array_map_literal(_a text[]) + RETURNS text[] AS $$ + SELECT array_agg(quote_literal(v)) FROM unnest(_a) v; +$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE; + + +CREATE OR REPLACE FUNCTION array_reverse(anyarray) RETURNS anyarray AS $$ +SELECT ARRAY( + SELECT $1[i] + FROM generate_subscripts($1,1) AS s(i) + ORDER BY i DESC +); +$$ LANGUAGE SQL STRICT IMMUTABLE; +CREATE OR REPLACE FUNCTION to_int(jsonb) RETURNS int AS $$ + SELECT floor(($1->>0)::float)::int; +$$ LANGUAGE SQL IMMUTABLE STRICT; + +CREATE OR REPLACE FUNCTION to_float(jsonb) RETURNS float AS $$ + SELECT ($1->>0)::float; +$$ LANGUAGE SQL IMMUTABLE STRICT; + +CREATE OR REPLACE FUNCTION to_tstz(jsonb) RETURNS timestamptz AS $$ + SELECT ($1->>0)::timestamptz; +$$ LANGUAGE SQL IMMUTABLE STRICT SET TIME ZONE 'UTC'; + + +CREATE OR REPLACE FUNCTION to_text(jsonb) RETURNS text AS $$ + SELECT $1->>0; +$$ LANGUAGE SQL IMMUTABLE STRICT; + +CREATE OR REPLACE FUNCTION to_text_array(jsonb) RETURNS text[] AS $$ + SELECT + CASE jsonb_typeof($1) + WHEN 'array' THEN ARRAY(SELECT jsonb_array_elements_text($1)) + ELSE ARRAY[$1->>0] + END + ; +$$ LANGUAGE SQL IMMUTABLE STRICT; + + +CREATE OR REPLACE FUNCTION bbox_geom(_bbox jsonb) RETURNS geometry AS $$ +SELECT CASE jsonb_array_length(_bbox) + WHEN 4 THEN + ST_SetSRID(ST_MakeEnvelope( + (_bbox->>0)::float, + (_bbox->>1)::float, + (_bbox->>2)::float, + (_bbox->>3)::float + ),4326) + WHEN 6 THEN + ST_SetSRID(ST_3DMakeBox( + ST_MakePoint( + (_bbox->>0)::float, + (_bbox->>1)::float, + (_bbox->>2)::float + ), + ST_MakePoint( + (_bbox->>3)::float, + (_bbox->>4)::float, + (_bbox->>5)::float + ) + ),4326) + ELSE null END; +; +$$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE; + +CREATE OR REPLACE FUNCTION geom_bbox(_geom geometry) RETURNS jsonb AS $$ + SELECT jsonb_build_array( + st_xmin(_geom), + st_ymin(_geom), + st_xmax(_geom), + st_ymax(_geom) + ); +$$ LANGUAGE SQL IMMUTABLE STRICT; + +CREATE OR REPLACE FUNCTION flip_jsonb_array(j jsonb) RETURNS jsonb AS $$ + SELECT jsonb_agg(value) FROM (SELECT value FROM jsonb_array_elements(j) WITH ORDINALITY ORDER BY ordinality DESC) as t; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; + + +CREATE OR REPLACE FUNCTION explode_dotpaths(j jsonb) RETURNS SETOF text[] AS $$ + SELECT string_to_array(p, '.') as e FROM jsonb_array_elements_text(j) p; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; + +CREATE OR REPLACE FUNCTION explode_dotpaths_recurse(IN j jsonb) RETURNS SETOF text[] AS $$ + WITH RECURSIVE t AS ( + SELECT e FROM explode_dotpaths(j) e + UNION ALL + SELECT e[1:cardinality(e)-1] + FROM t + WHERE cardinality(e)>1 + ) SELECT e FROM t; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; + + +CREATE OR REPLACE FUNCTION jsonb_set_nested(j jsonb, path text[], val jsonb) RETURNS jsonb AS $$ +DECLARE +BEGIN + IF cardinality(path) > 1 THEN + FOR i IN 1..(cardinality(path)-1) LOOP + IF j #> path[:i] IS NULL THEN + j := jsonb_set_lax(j, path[:i], '{}', TRUE); + END IF; + END LOOP; + END IF; + RETURN jsonb_set_lax(j, path, val, true); + +END; +$$ LANGUAGE PLPGSQL IMMUTABLE; + + + +CREATE OR REPLACE FUNCTION jsonb_include(j jsonb, f jsonb) RETURNS jsonb AS $$ +DECLARE + includes jsonb := f-> 'include'; + outj jsonb := '{}'::jsonb; + path text[]; +BEGIN + IF + includes IS NULL + OR jsonb_array_length(includes) = 0 + THEN + RETURN j; + ELSE + includes := includes || '["id","collection"]'::jsonb; + FOR path IN SELECT explode_dotpaths(includes) LOOP + outj := jsonb_set_nested(outj, path, j #> path); + END LOOP; + END IF; + RETURN outj; +END; +$$ LANGUAGE PLPGSQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION jsonb_exclude(j jsonb, f jsonb) RETURNS jsonb AS $$ +DECLARE + excludes jsonb := f-> 'exclude'; + outj jsonb := j; + path text[]; +BEGIN + IF + excludes IS NULL + OR jsonb_array_length(excludes) = 0 + THEN + RETURN j; + ELSE + FOR path IN SELECT explode_dotpaths(excludes) LOOP + outj := outj #- path; + END LOOP; + END IF; + RETURN outj; +END; +$$ LANGUAGE PLPGSQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION jsonb_fields(j jsonb, f jsonb DEFAULT '{"fields":[]}') RETURNS jsonb AS $$ + SELECT jsonb_exclude(jsonb_include(j, f), f); +$$ LANGUAGE SQL IMMUTABLE; + + +CREATE OR REPLACE FUNCTION merge_jsonb(_a jsonb, _b jsonb) RETURNS jsonb AS $$ + SELECT + CASE + WHEN _a = '"𒍟※"'::jsonb THEN NULL + WHEN _a IS NULL OR jsonb_typeof(_a) = 'null' THEN _b + WHEN jsonb_typeof(_a) = 'object' AND jsonb_typeof(_b) = 'object' THEN + ( + SELECT + jsonb_strip_nulls( + jsonb_object_agg( + key, + merge_jsonb(a.value, b.value) + ) + ) + FROM + jsonb_each(coalesce(_a,'{}'::jsonb)) as a + FULL JOIN + jsonb_each(coalesce(_b,'{}'::jsonb)) as b + USING (key) + ) + WHEN + jsonb_typeof(_a) = 'array' + AND jsonb_typeof(_b) = 'array' + AND jsonb_array_length(_a) = jsonb_array_length(_b) + THEN + ( + SELECT jsonb_agg(m) FROM + ( SELECT + merge_jsonb( + jsonb_array_elements(_a), + jsonb_array_elements(_b) + ) as m + ) as l + ) + ELSE _a + END + ; +$$ LANGUAGE SQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION strip_jsonb(_a jsonb, _b jsonb) RETURNS jsonb AS $$ + SELECT + CASE + + WHEN (_a IS NULL OR jsonb_typeof(_a) = 'null') AND _b IS NOT NULL AND jsonb_typeof(_b) != 'null' THEN '"𒍟※"'::jsonb + WHEN _b IS NULL OR jsonb_typeof(_a) = 'null' THEN _a + WHEN _a = _b AND jsonb_typeof(_a) = 'object' THEN '{}'::jsonb + WHEN _a = _b THEN NULL + WHEN jsonb_typeof(_a) = 'object' AND jsonb_typeof(_b) = 'object' THEN + ( + SELECT + jsonb_strip_nulls( + jsonb_object_agg( + key, + strip_jsonb(a.value, b.value) + ) + ) + FROM + jsonb_each(_a) as a + FULL JOIN + jsonb_each(_b) as b + USING (key) + ) + WHEN + jsonb_typeof(_a) = 'array' + AND jsonb_typeof(_b) = 'array' + AND jsonb_array_length(_a) = jsonb_array_length(_b) + THEN + ( + SELECT jsonb_agg(m) FROM + ( SELECT + strip_jsonb( + jsonb_array_elements(_a), + jsonb_array_elements(_b) + ) as m + ) as l + ) + ELSE _a + END + ; +$$ LANGUAGE SQL IMMUTABLE; +/* looks for a geometry in a stac item first from geometry and falling back to bbox */ +CREATE OR REPLACE FUNCTION stac_geom(value jsonb) RETURNS geometry AS $$ +SELECT + CASE + WHEN value ? 'intersects' THEN + ST_GeomFromGeoJSON(value->>'intersects') + WHEN value ? 'geometry' THEN + ST_GeomFromGeoJSON(value->>'geometry') + WHEN value ? 'bbox' THEN + pgstac.bbox_geom(value->'bbox') + ELSE NULL + END as geometry +; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; + + + +CREATE OR REPLACE FUNCTION stac_daterange( + value jsonb +) RETURNS tstzrange AS $$ +DECLARE + props jsonb := value; + dt timestamptz; + edt timestamptz; +BEGIN + IF props ? 'properties' THEN + props := props->'properties'; + END IF; + IF + props ? 'start_datetime' + AND props->>'start_datetime' IS NOT NULL + AND props ? 'end_datetime' + AND props->>'end_datetime' IS NOT NULL + THEN + dt := props->>'start_datetime'; + edt := props->>'end_datetime'; + IF dt > edt THEN + RAISE EXCEPTION 'start_datetime must be < end_datetime'; + END IF; + ELSE + dt := props->>'datetime'; + edt := props->>'datetime'; + END IF; + IF dt is NULL OR edt IS NULL THEN + RAISE NOTICE 'DT: %, EDT: %', dt, edt; + RAISE EXCEPTION 'Either datetime (%) or both start_datetime (%) and end_datetime (%) must be set.', props->>'datetime',props->>'start_datetime',props->>'end_datetime'; + END IF; + RETURN tstzrange(dt, edt, '[]'); +END; +$$ LANGUAGE PLPGSQL IMMUTABLE PARALLEL SAFE SET TIMEZONE='UTC'; + +CREATE OR REPLACE FUNCTION stac_datetime(value jsonb) RETURNS timestamptz AS $$ + SELECT lower(stac_daterange(value)); +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE SET TIMEZONE='UTC'; + +CREATE OR REPLACE FUNCTION stac_end_datetime(value jsonb) RETURNS timestamptz AS $$ + SELECT upper(stac_daterange(value)); +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE SET TIMEZONE='UTC'; + + +CREATE TABLE IF NOT EXISTS stac_extensions( + name text PRIMARY KEY, + url text, + enbabled_by_default boolean NOT NULL DEFAULT TRUE, + enableable boolean NOT NULL DEFAULT TRUE +); + +INSERT INTO stac_extensions (name, url) VALUES + ('fields', 'https://api.stacspec.org/v1.0.0-beta.5/item-search#fields'), + ('sort','https://api.stacspec.org/v1.0.0-beta.5/item-search#sort'), + ('context','https://api.stacspec.org/v1.0.0-beta.5/item-search#context'), + ('filter', 'https://api.stacspec.org/v1.0.0-beta.5/item-search#filter'), + ('query', 'https://api.stacspec.org/v1.0.0-beta.5/item-search#query') +ON CONFLICT (name) DO UPDATE SET url=EXCLUDED.url; + + + +CREATE OR REPLACE FUNCTION collection_base_item(content jsonb) RETURNS jsonb AS $$ + SELECT jsonb_build_object( + 'type', 'Feature', + 'stac_version', content->'stac_version', + 'assets', content->'item_assets', + 'collection', content->'id' + ); +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; + +CREATE TYPE partition_trunc_strategy AS ENUM ('year', 'month'); + +CREATE TABLE IF NOT EXISTS collections ( + key bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + id text GENERATED ALWAYS AS (content->>'id') STORED UNIQUE, + content JSONB NOT NULL, + base_item jsonb GENERATED ALWAYS AS (pgstac.collection_base_item(content)) STORED, + partition_trunc partition_trunc_strategy +); + + +CREATE OR REPLACE FUNCTION collection_base_item(cid text) RETURNS jsonb AS $$ + SELECT pgstac.collection_base_item(content) FROM pgstac.collections WHERE id = cid LIMIT 1; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; + + + +CREATE OR REPLACE FUNCTION table_empty(text) RETURNS boolean AS $$ +DECLARE + retval boolean; +BEGIN + EXECUTE format($q$ + SELECT NOT EXISTS (SELECT 1 FROM %I LIMIT 1) + $q$, + $1 + ) INTO retval; + RETURN retval; +END; +$$ LANGUAGE PLPGSQL; + + +CREATE OR REPLACE FUNCTION collections_trigger_func() RETURNS TRIGGER AS $$ +DECLARE + q text; + partition_name text := format('_items_%s', NEW.key); + partition_exists boolean := false; + partition_empty boolean := true; + err_context text; + loadtemp boolean := FALSE; +BEGIN + RAISE NOTICE 'Collection Trigger. % %', NEW.id, NEW.key; + SELECT relid::text INTO partition_name + FROM pg_partition_tree('items') + WHERE relid::text = partition_name; + IF FOUND THEN + partition_exists := true; + partition_empty := table_empty(partition_name); + ELSE + partition_exists := false; + partition_empty := true; + partition_name := format('_items_%s', NEW.key); + END IF; + IF TG_OP = 'UPDATE' AND NEW.partition_trunc IS DISTINCT FROM OLD.partition_trunc AND partition_empty THEN + q := format($q$ + DROP TABLE IF EXISTS %I CASCADE; + $q$, + partition_name + ); + EXECUTE q; + END IF; + IF TG_OP = 'UPDATE' AND NEW.partition_trunc IS DISTINCT FROM OLD.partition_trunc AND partition_exists AND NOT partition_empty THEN + q := format($q$ + CREATE TEMP TABLE changepartitionstaging ON COMMIT DROP AS SELECT * FROM %I; + DROP TABLE IF EXISTS %I CASCADE; + $q$, + partition_name, + partition_name + ); + EXECUTE q; + loadtemp := TRUE; + partition_empty := TRUE; + partition_exists := FALSE; + END IF; + IF TG_OP = 'UPDATE' AND NEW.partition_trunc IS NOT DISTINCT FROM OLD.partition_trunc THEN + RETURN NEW; + END IF; + IF NEW.partition_trunc IS NULL AND partition_empty THEN + RAISE NOTICE '% % % %', + partition_name, + NEW.id, + concat(partition_name,'_id_idx'), + partition_name + ; + q := format($q$ + CREATE TABLE IF NOT EXISTS %I partition OF items FOR VALUES IN (%L); + CREATE UNIQUE INDEX IF NOT EXISTS %I ON %I (id); + $q$, + partition_name, + NEW.id, + concat(partition_name,'_id_idx'), + partition_name + ); + RAISE NOTICE 'q: %', q; + BEGIN + EXECUTE q; + EXCEPTION + WHEN duplicate_table THEN + RAISE NOTICE 'Partition % already exists.', partition_name; + WHEN others THEN + GET STACKED DIAGNOSTICS err_context = PG_EXCEPTION_CONTEXT; + RAISE INFO 'Error Name:%',SQLERRM; + RAISE INFO 'Error State:%', SQLSTATE; + RAISE INFO 'Error Context:%', err_context; + END; + + ALTER TABLE partitions DISABLE TRIGGER partitions_delete_trigger; + DELETE FROM partitions WHERE collection=NEW.id AND name=partition_name; + ALTER TABLE partitions ENABLE TRIGGER partitions_delete_trigger; + + INSERT INTO partitions (collection, name) VALUES (NEW.id, partition_name); + ELSIF partition_empty THEN + q := format($q$ + CREATE TABLE IF NOT EXISTS %I partition OF items FOR VALUES IN (%L) + PARTITION BY RANGE (datetime); + $q$, + partition_name, + NEW.id + ); + RAISE NOTICE 'q: %', q; + BEGIN + EXECUTE q; + EXCEPTION + WHEN duplicate_table THEN + RAISE NOTICE 'Partition % already exists.', partition_name; + WHEN others THEN + GET STACKED DIAGNOSTICS err_context = PG_EXCEPTION_CONTEXT; + RAISE INFO 'Error Name:%',SQLERRM; + RAISE INFO 'Error State:%', SQLSTATE; + RAISE INFO 'Error Context:%', err_context; + END; + ALTER TABLE partitions DISABLE TRIGGER partitions_delete_trigger; + DELETE FROM partitions WHERE collection=NEW.id AND name=partition_name; + ALTER TABLE partitions ENABLE TRIGGER partitions_delete_trigger; + ELSE + RAISE EXCEPTION 'Cannot modify partition % unless empty', partition_name; + END IF; + IF loadtemp THEN + RAISE NOTICE 'Moving data into new partitions.'; + q := format($q$ + WITH p AS ( + SELECT + collection, + datetime as datetime, + end_datetime as end_datetime, + (partition_name( + collection, + datetime + )).partition_name as name + FROM changepartitionstaging + ) + INSERT INTO partitions (collection, datetime_range, end_datetime_range) + SELECT + collection, + tstzrange(min(datetime), max(datetime), '[]') as datetime_range, + tstzrange(min(end_datetime), max(end_datetime), '[]') as end_datetime_range + FROM p + GROUP BY collection, name + ON CONFLICT (name) DO UPDATE SET + datetime_range = EXCLUDED.datetime_range, + end_datetime_range = EXCLUDED.end_datetime_range + ; + INSERT INTO %I SELECT * FROM changepartitionstaging; + DROP TABLE IF EXISTS changepartitionstaging; + $q$, + partition_name + ); + EXECUTE q; + END IF; + RETURN NEW; +END; +$$ LANGUAGE PLPGSQL SET SEARCH_PATH TO pgstac, public; + +CREATE TRIGGER collections_trigger AFTER INSERT OR UPDATE ON collections FOR EACH ROW +EXECUTE FUNCTION collections_trigger_func(); + +CREATE OR REPLACE FUNCTION partition_collection(collection text, strategy partition_trunc_strategy) RETURNS text AS $$ + UPDATE collections SET partition_trunc=strategy WHERE id=collection RETURNING partition_trunc; +$$ LANGUAGE SQL; + +CREATE TABLE IF NOT EXISTS partitions ( + collection text REFERENCES collections(id) ON DELETE CASCADE, + name text PRIMARY KEY, + partition_range tstzrange NOT NULL DEFAULT tstzrange('-infinity'::timestamptz,'infinity'::timestamptz, '[]'), + datetime_range tstzrange, + end_datetime_range tstzrange, + CONSTRAINT prange EXCLUDE USING GIST ( + collection WITH =, + partition_range WITH && + ) +) WITH (FILLFACTOR=90); +CREATE INDEX partitions_range_idx ON partitions USING GIST(partition_range); + +CREATE OR REPLACE FUNCTION partitions_delete_trigger_func() RETURNS TRIGGER AS $$ +DECLARE + q text; +BEGIN + RAISE NOTICE 'Partition Delete Trigger. %', OLD.name; + EXECUTE format($q$ + DROP TABLE IF EXISTS %I CASCADE; + $q$, + OLD.name + ); + RAISE NOTICE 'Dropped partition.'; + RETURN OLD; +END; +$$ LANGUAGE PLPGSQL; + +CREATE TRIGGER partitions_delete_trigger BEFORE DELETE ON partitions FOR EACH ROW +EXECUTE FUNCTION partitions_delete_trigger_func(); + +CREATE OR REPLACE FUNCTION partition_name( + IN collection text, + IN dt timestamptz, + OUT partition_name text, + OUT partition_range tstzrange +) AS $$ +DECLARE + c RECORD; + parent_name text; +BEGIN + SELECT * INTO c FROM pgstac.collections WHERE id=collection; + IF NOT FOUND THEN + RAISE EXCEPTION 'Collection % does not exist', collection USING ERRCODE = 'foreign_key_violation', HINT = 'Make sure collection exists before adding items'; + END IF; + parent_name := format('_items_%s', c.key); + + + IF c.partition_trunc = 'year' THEN + partition_name := format('%s_%s', parent_name, to_char(dt,'YYYY')); + ELSIF c.partition_trunc = 'month' THEN + partition_name := format('%s_%s', parent_name, to_char(dt,'YYYYMM')); + ELSE + partition_name := parent_name; + partition_range := tstzrange('-infinity'::timestamptz, 'infinity'::timestamptz, '[]'); + END IF; + IF partition_range IS NULL THEN + partition_range := tstzrange( + date_trunc(c.partition_trunc::text, dt), + date_trunc(c.partition_trunc::text, dt) + concat('1 ', c.partition_trunc)::interval + ); + END IF; + RETURN; + +END; +$$ LANGUAGE PLPGSQL STABLE; + + + +CREATE OR REPLACE FUNCTION partitions_trigger_func() RETURNS TRIGGER AS $$ +DECLARE + q text; + cq text; + parent_name text; + partition_trunc text; + partition_name text := NEW.name; + partition_exists boolean := false; + partition_empty boolean := true; + partition_range tstzrange; + datetime_range tstzrange; + end_datetime_range tstzrange; + err_context text; + mindt timestamptz := lower(NEW.datetime_range); + maxdt timestamptz := upper(NEW.datetime_range); + minedt timestamptz := lower(NEW.end_datetime_range); + maxedt timestamptz := upper(NEW.end_datetime_range); + t_mindt timestamptz; + t_maxdt timestamptz; + t_minedt timestamptz; + t_maxedt timestamptz; +BEGIN + RAISE NOTICE 'Partitions Trigger. %', NEW; + datetime_range := NEW.datetime_range; + end_datetime_range := NEW.end_datetime_range; + + SELECT + format('_items_%s', key), + c.partition_trunc::text + INTO + parent_name, + partition_trunc + FROM pgstac.collections c + WHERE c.id = NEW.collection; + SELECT (pgstac.partition_name(NEW.collection, mindt)).* INTO partition_name, partition_range; + NEW.name := partition_name; + + IF partition_range IS NULL OR partition_range = 'empty'::tstzrange THEN + partition_range := tstzrange('-infinity'::timestamptz, 'infinity'::timestamptz, '[]'); + END IF; + + NEW.partition_range := partition_range; + IF TG_OP = 'UPDATE' THEN + mindt := least(mindt, lower(OLD.datetime_range)); + maxdt := greatest(maxdt, upper(OLD.datetime_range)); + minedt := least(minedt, lower(OLD.end_datetime_range)); + maxedt := greatest(maxedt, upper(OLD.end_datetime_range)); + NEW.datetime_range := tstzrange(mindt, maxdt, '[]'); + NEW.end_datetime_range := tstzrange(minedt, maxedt, '[]'); + END IF; + IF TG_OP = 'INSERT' THEN + + IF partition_range != tstzrange('-infinity'::timestamptz, 'infinity'::timestamptz, '[]') THEN + + RAISE NOTICE '% % %', partition_name, parent_name, partition_range; + q := format($q$ + CREATE TABLE IF NOT EXISTS %I partition OF %I FOR VALUES FROM (%L) TO (%L); + CREATE UNIQUE INDEX IF NOT EXISTS %I ON %I (id); + $q$, + partition_name, + parent_name, + lower(partition_range), + upper(partition_range), + format('%s_pkey', partition_name), + partition_name + ); + BEGIN + EXECUTE q; + EXCEPTION + WHEN duplicate_table THEN + RAISE NOTICE 'Partition % already exists.', partition_name; + WHEN others THEN + GET STACKED DIAGNOSTICS err_context = PG_EXCEPTION_CONTEXT; + RAISE INFO 'Error Name:%',SQLERRM; + RAISE INFO 'Error State:%', SQLSTATE; + RAISE INFO 'Error Context:%', err_context; + END; + END IF; + + END IF; + + -- Update constraints + EXECUTE format($q$ + SELECT + min(datetime), + max(datetime), + min(end_datetime), + max(end_datetime) + FROM %I; + $q$, partition_name) + INTO t_mindt, t_maxdt, t_minedt, t_maxedt; + mindt := least(mindt, t_mindt); + maxdt := greatest(maxdt, t_maxdt); + minedt := least(mindt, minedt, t_minedt); + maxedt := greatest(maxdt, maxedt, t_maxedt); + + mindt := date_trunc(coalesce(partition_trunc, 'year'), mindt); + maxdt := date_trunc(coalesce(partition_trunc, 'year'), maxdt - '1 second'::interval) + concat('1 ',coalesce(partition_trunc, 'year'))::interval; + minedt := date_trunc(coalesce(partition_trunc, 'year'), minedt); + maxedt := date_trunc(coalesce(partition_trunc, 'year'), maxedt - '1 second'::interval) + concat('1 ',coalesce(partition_trunc, 'year'))::interval; + + + IF mindt IS NOT NULL AND maxdt IS NOT NULL AND minedt IS NOT NULL AND maxedt IS NOT NULL THEN + NEW.datetime_range := tstzrange(mindt, maxdt, '[]'); + NEW.end_datetime_range := tstzrange(minedt, maxedt, '[]'); + IF + TG_OP='UPDATE' + AND OLD.datetime_range @> NEW.datetime_range + AND OLD.end_datetime_range @> NEW.end_datetime_range + THEN + RAISE NOTICE 'Range unchanged, not updating constraints.'; + ELSE + + RAISE NOTICE ' + SETTING CONSTRAINTS + mindt: %, maxdt: % + minedt: %, maxedt: % + ', mindt, maxdt, minedt, maxedt; + IF partition_trunc IS NULL THEN + cq := format($q$ + ALTER TABLE %7$I + DROP CONSTRAINT IF EXISTS %1$I, + DROP CONSTRAINT IF EXISTS %2$I, + ADD CONSTRAINT %1$I + CHECK ( + (datetime >= %3$L) + AND (datetime <= %4$L) + AND (end_datetime >= %5$L) + AND (end_datetime <= %6$L) + ) NOT VALID + ; + ALTER TABLE %7$I + VALIDATE CONSTRAINT %1$I; + $q$, + format('%s_dt', partition_name), + format('%s_edt', partition_name), + mindt, + maxdt, + minedt, + maxedt, + partition_name + ); + ELSE + cq := format($q$ + ALTER TABLE %5$I + DROP CONSTRAINT IF EXISTS %1$I, + DROP CONSTRAINT IF EXISTS %2$I, + ADD CONSTRAINT %2$I + CHECK ((end_datetime >= %3$L) AND (end_datetime <= %4$L)) NOT VALID + ; + ALTER TABLE %5$I + VALIDATE CONSTRAINT %2$I; + $q$, + format('%s_dt', partition_name), + format('%s_edt', partition_name), + minedt, + maxedt, + partition_name + ); + + END IF; + RAISE NOTICE 'Altering Constraints. %', cq; + EXECUTE cq; + END IF; + ELSE + NEW.datetime_range = NULL; + NEW.end_datetime_range = NULL; + + cq := format($q$ + ALTER TABLE %3$I + DROP CONSTRAINT IF EXISTS %1$I, + DROP CONSTRAINT IF EXISTS %2$I, + ADD CONSTRAINT %1$I + CHECK ((datetime IS NULL AND end_datetime IS NULL)) NOT VALID + ; + ALTER TABLE %3$I + VALIDATE CONSTRAINT %1$I; + $q$, + format('%s_dt', partition_name), + format('%s_edt', partition_name), + partition_name + ); + EXECUTE cq; + END IF; + + RETURN NEW; + +END; +$$ LANGUAGE PLPGSQL; + +CREATE TRIGGER partitions_trigger BEFORE INSERT OR UPDATE ON partitions FOR EACH ROW +EXECUTE FUNCTION partitions_trigger_func(); + + +CREATE OR REPLACE FUNCTION create_collection(data jsonb) RETURNS VOID AS $$ + INSERT INTO collections (content) + VALUES (data) + ; +$$ LANGUAGE SQL SET SEARCH_PATH TO pgstac, public; + +CREATE OR REPLACE FUNCTION update_collection(data jsonb) RETURNS VOID AS $$ +DECLARE + out collections%ROWTYPE; +BEGIN + UPDATE collections SET content=data WHERE id = data->>'id' RETURNING * INTO STRICT out; +END; +$$ LANGUAGE PLPGSQL SET SEARCH_PATH TO pgstac,public; + +CREATE OR REPLACE FUNCTION upsert_collection(data jsonb) RETURNS VOID AS $$ + INSERT INTO collections (content) + VALUES (data) + ON CONFLICT (id) DO + UPDATE + SET content=EXCLUDED.content + ; +$$ LANGUAGE SQL SET SEARCH_PATH TO pgstac, public; + +CREATE OR REPLACE FUNCTION delete_collection(_id text) RETURNS VOID AS $$ +DECLARE + out collections%ROWTYPE; +BEGIN + DELETE FROM collections WHERE id = _id RETURNING * INTO STRICT out; +END; +$$ LANGUAGE PLPGSQL SET SEARCH_PATH TO pgstac,public; + + +CREATE OR REPLACE FUNCTION get_collection(id text) RETURNS jsonb AS $$ + SELECT content FROM collections + WHERE id=$1 + ; +$$ LANGUAGE SQL SET SEARCH_PATH TO pgstac, public; + +CREATE OR REPLACE FUNCTION all_collections() RETURNS jsonb AS $$ + SELECT jsonb_agg(content) FROM collections; +; +$$ LANGUAGE SQL SET SEARCH_PATH TO pgstac, public; +CREATE TABLE queryables ( + id bigint GENERATED ALWAYS AS identity PRIMARY KEY, + name text UNIQUE NOT NULL, + collection_ids text[], -- used to determine what partitions to create indexes on + definition jsonb, + property_path text, + property_wrapper text, + property_index_type text +); +CREATE INDEX queryables_name_idx ON queryables (name); +CREATE INDEX queryables_property_wrapper_idx ON queryables (property_wrapper); + + +INSERT INTO queryables (name, definition) VALUES +('id', '{"title": "Item ID","description": "Item identifier","$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id"}'), +('datetime','{"description": "Datetime","type": "string","title": "Acquired","format": "date-time","pattern": "(\\+00:00|Z)$"}') +ON CONFLICT DO NOTHING; + + + +INSERT INTO queryables (name, definition, property_wrapper, property_index_type) VALUES +('eo:cloud_cover','{"$ref": "https://stac-extensions.github.io/eo/v1.0.0/schema.json#/definitions/fieldsproperties/eo:cloud_cover"}','to_int','BTREE') +ON CONFLICT DO NOTHING; + +CREATE OR REPLACE FUNCTION array_to_path(arr text[]) RETURNS text AS $$ + SELECT string_agg( + quote_literal(v), + '->' + ) FROM unnest(arr) v; +$$ LANGUAGE SQL IMMUTABLE STRICT; + + + + +CREATE OR REPLACE FUNCTION queryable( + IN dotpath text, + OUT path text, + OUT expression text, + OUT wrapper text +) AS $$ +DECLARE + q RECORD; + path_elements text[]; +BEGIN + IF dotpath IN ('id', 'geometry', 'datetime', 'end_datetime', 'collection') THEN + path := dotpath; + expression := dotpath; + wrapper := NULL; + RETURN; + END IF; + SELECT * INTO q FROM queryables WHERE name=dotpath; + IF q.property_wrapper IS NULL THEN + IF q.definition->>'type' = 'number' THEN + wrapper := 'to_float'; + ELSIF q.definition->>'format' = 'date-time' THEN + wrapper := 'to_tstz'; + ELSE + wrapper := 'to_text'; + END IF; + ELSE + wrapper := q.property_wrapper; + END IF; + IF q.property_path IS NOT NULL THEN + path := q.property_path; + ELSE + path_elements := string_to_array(dotpath, '.'); + IF path_elements[1] IN ('links', 'assets', 'stac_version', 'stac_extensions') THEN + path := format('content->%s', array_to_path(path_elements)); + ELSIF path_elements[1] = 'properties' THEN + path := format('content->%s', array_to_path(path_elements)); + ELSE + path := format($F$content->'properties'->%s$F$, array_to_path(path_elements)); + END IF; + END IF; + expression := format('%I(%s)', wrapper, path); + RETURN; +END; +$$ LANGUAGE PLPGSQL STABLE STRICT; + +CREATE OR REPLACE FUNCTION create_queryable_indexes() RETURNS VOID AS $$ +DECLARE + queryable RECORD; + q text; +BEGIN + FOR queryable IN + SELECT + queryables.id as qid, + CASE WHEN collections.key IS NULL THEN 'items' ELSE format('_items_%s',collections.key) END AS part, + property_index_type, + expression + FROM + queryables + LEFT JOIN collections ON (collections.id = ANY (queryables.collection_ids)) + JOIN LATERAL queryable(queryables.name) ON (queryables.property_index_type IS NOT NULL) + LOOP + q := format( + $q$ + CREATE INDEX IF NOT EXISTS %I ON %I USING %s ((%s)); + $q$, + format('%s_%s_idx', queryable.part, queryable.qid), + queryable.part, + COALESCE(queryable.property_index_type, 'to_text'), + queryable.expression + ); + RAISE NOTICE '%',q; + EXECUTE q; + END LOOP; + RETURN; +END; +$$ LANGUAGE PLPGSQL; + +CREATE OR REPLACE FUNCTION queryables_trigger_func() RETURNS TRIGGER AS $$ +DECLARE +BEGIN +PERFORM create_queryable_indexes(); +RETURN NEW; +END; +$$ LANGUAGE PLPGSQL; + +CREATE TRIGGER queryables_trigger AFTER INSERT OR UPDATE ON queryables +FOR EACH STATEMENT EXECUTE PROCEDURE queryables_trigger_func(); + +CREATE TRIGGER queryables_collection_trigger AFTER INSERT OR UPDATE ON collections +FOR EACH STATEMENT EXECUTE PROCEDURE queryables_trigger_func(); +CREATE OR REPLACE FUNCTION parse_dtrange( + _indate jsonb, + relative_base timestamptz DEFAULT date_trunc('hour', CURRENT_TIMESTAMP) +) RETURNS tstzrange AS $$ +DECLARE + timestrs text[]; + s timestamptz; + e timestamptz; +BEGIN + timestrs := + CASE + WHEN _indate ? 'timestamp' THEN + ARRAY[_indate->>'timestamp'] + WHEN _indate ? 'interval' THEN + to_text_array(_indate->'interval') + WHEN jsonb_typeof(_indate) = 'array' THEN + to_text_array(_indate) + ELSE + regexp_split_to_array( + _indate->>0, + '/' + ) + END; + RAISE NOTICE 'TIMESTRS %', timestrs; + IF cardinality(timestrs) = 1 THEN + IF timestrs[1] ILIKE 'P%' THEN + RETURN tstzrange(relative_base - upper(timestrs[1])::interval, relative_base, '[)'); + END IF; + s := timestrs[1]::timestamptz; + RETURN tstzrange(s, s, '[]'); + END IF; + + IF cardinality(timestrs) != 2 THEN + RAISE EXCEPTION 'Timestamp cannot have more than 2 values'; + END IF; + + IF timestrs[1] = '..' THEN + s := '-infinity'::timestamptz; + e := timestrs[2]::timestamptz; + RETURN tstzrange(s,e,'[)'); + END IF; + + IF timestrs[2] = '..' THEN + s := timestrs[1]::timestamptz; + e := 'infinity'::timestamptz; + RETURN tstzrange(s,e,'[)'); + END IF; + + IF timestrs[1] ILIKE 'P%' AND timestrs[2] NOT ILIKE 'P%' THEN + e := timestrs[2]::timestamptz; + s := e - upper(timestrs[1])::interval; + RETURN tstzrange(s,e,'[)'); + END IF; + + IF timestrs[2] ILIKE 'P%' AND timestrs[1] NOT ILIKE 'P%' THEN + s := timestrs[1]::timestamptz; + e := s + upper(timestrs[2])::interval; + RETURN tstzrange(s,e,'[)'); + END IF; + + s := timestrs[1]::timestamptz; + e := timestrs[2]::timestamptz; + + RETURN tstzrange(s,e,'[)'); + + RETURN NULL; + +END; +$$ LANGUAGE PLPGSQL STABLE STRICT PARALLEL SAFE SET TIME ZONE 'UTC'; + +CREATE OR REPLACE FUNCTION parse_dtrange( + _indate text, + relative_base timestamptz DEFAULT CURRENT_TIMESTAMP +) RETURNS tstzrange AS $$ + SELECT parse_dtrange(to_jsonb(_indate), relative_base); +$$ LANGUAGE SQL STABLE STRICT PARALLEL SAFE; + + +CREATE OR REPLACE FUNCTION temporal_op_query(op text, args jsonb) RETURNS text AS $$ +DECLARE + ll text := 'datetime'; + lh text := 'end_datetime'; + rrange tstzrange; + rl text; + rh text; + outq text; +BEGIN + rrange := parse_dtrange(args->1); + RAISE NOTICE 'Constructing temporal query OP: %, ARGS: %, RRANGE: %', op, args, rrange; + op := lower(op); + rl := format('%L::timestamptz', lower(rrange)); + rh := format('%L::timestamptz', upper(rrange)); + outq := CASE op + WHEN 't_before' THEN 'lh < rl' + WHEN 't_after' THEN 'll > rh' + WHEN 't_meets' THEN 'lh = rl' + WHEN 't_metby' THEN 'll = rh' + WHEN 't_overlaps' THEN 'll < rl AND rl < lh < rh' + WHEN 't_overlappedby' THEN 'rl < ll < rh AND lh > rh' + WHEN 't_starts' THEN 'll = rl AND lh < rh' + WHEN 't_startedby' THEN 'll = rl AND lh > rh' + WHEN 't_during' THEN 'll > rl AND lh < rh' + WHEN 't_contains' THEN 'll < rl AND lh > rh' + WHEN 't_finishes' THEN 'll > rl AND lh = rh' + WHEN 't_finishedby' THEN 'll < rl AND lh = rh' + WHEN 't_equals' THEN 'll = rl AND lh = rh' + WHEN 't_disjoint' THEN 'NOT (ll <= rh AND lh >= rl)' + WHEN 't_intersects' THEN 'll <= rh AND lh >= rl' + WHEN 'anyinteracts' THEN 'll <= rh AND lh >= rl' + END; + outq := regexp_replace(outq, '\mll\M', ll); + outq := regexp_replace(outq, '\mlh\M', lh); + outq := regexp_replace(outq, '\mrl\M', rl); + outq := regexp_replace(outq, '\mrh\M', rh); + outq := format('(%s)', outq); + RETURN outq; +END; +$$ LANGUAGE PLPGSQL STABLE STRICT; + + + +CREATE OR REPLACE FUNCTION spatial_op_query(op text, args jsonb) RETURNS text AS $$ +DECLARE + geom text; + j jsonb := args->1; +BEGIN + op := lower(op); + RAISE NOTICE 'Constructing spatial query OP: %, ARGS: %', op, args; + IF op NOT IN ('s_equals','s_disjoint','s_touches','s_within','s_overlaps','s_crosses','s_intersects','intersects','s_contains') THEN + RAISE EXCEPTION 'Spatial Operator % Not Supported', op; + END IF; + op := regexp_replace(op, '^s_', 'st_'); + IF op = 'intersects' THEN + op := 'st_intersects'; + END IF; + -- Convert geometry to WKB string + IF j ? 'type' AND j ? 'coordinates' THEN + geom := st_geomfromgeojson(j)::text; + ELSIF jsonb_typeof(j) = 'array' THEN + geom := bbox_geom(j)::text; + END IF; + + RETURN format('%s(geometry, %L::geometry)', op, geom); +END; +$$ LANGUAGE PLPGSQL; + +CREATE OR REPLACE FUNCTION query_to_cql2(q jsonb) RETURNS jsonb AS $$ +-- Translates anything passed in through the deprecated "query" into equivalent CQL2 +WITH t AS ( + SELECT key as property, value as ops + FROM jsonb_each(q) +), t2 AS ( + SELECT property, (jsonb_each(ops)).* + FROM t WHERE jsonb_typeof(ops) = 'object' + UNION ALL + SELECT property, 'eq', ops + FROM t WHERE jsonb_typeof(ops) != 'object' +) +SELECT + jsonb_strip_nulls(jsonb_build_object( + 'op', 'and', + 'args', jsonb_agg( + jsonb_build_object( + 'op', key, + 'args', jsonb_build_array( + jsonb_build_object('property',property), + value + ) + ) + ) + ) +) as qcql FROM t2 +; +$$ LANGUAGE SQL IMMUTABLE STRICT; + + +CREATE OR REPLACE FUNCTION cql1_to_cql2(j jsonb) RETURNS jsonb AS $$ +DECLARE + args jsonb; + ret jsonb; +BEGIN + RAISE NOTICE 'CQL1_TO_CQL2: %', j; + IF j ? 'filter' THEN + RETURN cql1_to_cql2(j->'filter'); + END IF; + IF j ? 'property' THEN + RETURN j; + END IF; + IF jsonb_typeof(j) = 'array' THEN + SELECT jsonb_agg(cql1_to_cql2(el)) INTO args FROM jsonb_array_elements(j) el; + RETURN args; + END IF; + IF jsonb_typeof(j) = 'number' THEN + RETURN j; + END IF; + IF jsonb_typeof(j) = 'string' THEN + RETURN j; + END IF; + + IF jsonb_typeof(j) = 'object' THEN + SELECT jsonb_build_object( + 'op', key, + 'args', cql1_to_cql2(value) + ) INTO ret + FROM jsonb_each(j) + WHERE j IS NOT NULL; + RETURN ret; + END IF; + RETURN NULL; +END; +$$ LANGUAGE PLPGSQL IMMUTABLE STRICT; + +CREATE TABLE cql2_ops ( + op text PRIMARY KEY, + template text, + types text[] +); +INSERT INTO cql2_ops (op, template, types) VALUES + ('eq', '%s = %s', NULL), + ('lt', '%s < %s', NULL), + ('lte', '%s <= %s', NULL), + ('gt', '%s > %s', NULL), + ('gte', '%s >= %s', NULL), + ('le', '%s <= %s', NULL), + ('ge', '%s >= %s', NULL), + ('=', '%s = %s', NULL), + ('<', '%s < %s', NULL), + ('<=', '%s <= %s', NULL), + ('>', '%s > %s', NULL), + ('>=', '%s >= %s', NULL), + ('like', '%s LIKE %s', NULL), + ('ilike', '%s ILIKE %s', NULL), + ('+', '%s + %s', NULL), + ('-', '%s - %s', NULL), + ('*', '%s * %s', NULL), + ('/', '%s / %s', NULL), + ('in', '%s = ANY (%s)', NULL), + ('not', 'NOT (%s)', NULL), + ('between', '%s BETWEEN (%2$s)[1] AND (%2$s)[2]', NULL), + ('isnull', '%s IS NULL', NULL) +ON CONFLICT (op) DO UPDATE + SET + template = EXCLUDED.template; +; + + +CREATE OR REPLACE FUNCTION cql2_query(j jsonb, wrapper text DEFAULT NULL) RETURNS text AS $$ +#variable_conflict use_variable +DECLARE + args jsonb := j->'args'; + arg jsonb; + op text := lower(j->>'op'); + cql2op RECORD; + literal text; +BEGIN + IF j IS NULL OR (op IS NOT NULL AND args IS NULL) THEN + RETURN NULL; + END IF; + RAISE NOTICE 'CQL2_QUERY: %', j; + IF j ? 'filter' THEN + RETURN cql2_query(j->'filter'); + END IF; + + IF j ? 'upper' THEN + RETURN format('upper(%s)', cql2_query(j->'upper')); + END IF; + + IF j ? 'lower' THEN + RETURN format('lower(%s)', cql2_query(j->'lower')); + END IF; + + -- Temporal Query + IF op ilike 't_%' or op = 'anyinteracts' THEN + RETURN temporal_op_query(op, args); + END IF; + + -- If property is a timestamp convert it to text to use with + -- general operators + IF j ? 'timestamp' THEN + RETURN format('%L::timestamptz', to_tstz(j->'timestamp')); + END IF; + IF j ? 'interval' THEN + RAISE EXCEPTION 'Please use temporal operators when using intervals.'; + RETURN NONE; + END IF; + + -- Spatial Query + IF op ilike 's_%' or op = 'intersects' THEN + RETURN spatial_op_query(op, args); + END IF; + + + IF op = 'in' THEN + RETURN format( + '%s = ANY (%L)', + cql2_query(args->0), + to_text_array(args->1) + ); + END IF; + + + + IF op = 'between' THEN + SELECT (queryable(a->>'property')).wrapper INTO wrapper + FROM jsonb_array_elements(args) a + WHERE a ? 'property' LIMIT 1; + + RETURN format( + '%s BETWEEN %s and %s', + cql2_query(args->0, wrapper), + cql2_query(args->1->0, wrapper), + cql2_query(args->1->1, wrapper) + ); + END IF; + + -- Make sure that args is an array and run cql2_query on + -- each element of the array + RAISE NOTICE 'ARGS PRE: %', args; + IF j ? 'args' THEN + IF jsonb_typeof(args) != 'array' THEN + args := jsonb_build_array(args); + END IF; + + SELECT (queryable(a->>'property')).wrapper INTO wrapper + FROM jsonb_array_elements(args) a + WHERE a ? 'property' LIMIT 1; + + SELECT jsonb_agg(cql2_query(a, wrapper)) + INTO args + FROM jsonb_array_elements(args) a; + END IF; + RAISE NOTICE 'ARGS: %', args; + + IF op IN ('and', 'or') THEN + RETURN + format( + '(%s)', + array_to_string(to_text_array(args), format(' %s ', upper(op))) + ); + END IF; + + -- Look up template from cql2_ops + IF j ? 'op' THEN + SELECT * INTO cql2op FROM cql2_ops WHERE cql2_ops.op ilike op; + IF FOUND THEN + -- If specific index set in queryables for a property cast other arguments to that type + RETURN format( + cql2op.template, + VARIADIC (to_text_array(args)) + ); + ELSE + RAISE EXCEPTION 'Operator % Not Supported.', op; + END IF; + END IF; + + + IF j ? 'property' THEN + RETURN (queryable(j->>'property')).expression; + END IF; + + IF wrapper IS NOT NULL THEN + EXECUTE format('SELECT %I(%L)', wrapper, j) INTO literal; + RAISE NOTICE '% % %',wrapper, j, literal; + RETURN format('%I(%L)', wrapper, j); + END IF; + + RETURN quote_literal(to_text(j)); +END; +$$ LANGUAGE PLPGSQL STABLE; + + +CREATE OR REPLACE FUNCTION paging_dtrange( + j jsonb +) RETURNS tstzrange AS $$ +DECLARE + op text; + filter jsonb := j->'filter'; + dtrange tstzrange := tstzrange('-infinity'::timestamptz,'infinity'::timestamptz); + sdate timestamptz := '-infinity'::timestamptz; + edate timestamptz := 'infinity'::timestamptz; + jpitem jsonb; +BEGIN + + IF j ? 'datetime' THEN + dtrange := parse_dtrange(j->'datetime'); + sdate := lower(dtrange); + edate := upper(dtrange); + END IF; + IF NOT (filter @? '$.**.op ? (@ == "or" || @ == "not")') THEN + FOR jpitem IN SELECT j FROM jsonb_path_query(filter,'strict $.** ? (@.args[*].property == "datetime")'::jsonpath) j LOOP + op := lower(jpitem->>'op'); + dtrange := parse_dtrange(jpitem->'args'->1); + IF op IN ('<=', 'lt', 'lte', '<', 'le', 't_before') THEN + sdate := greatest(sdate,'-infinity'); + edate := least(edate, upper(dtrange)); + ELSIF op IN ('>=', '>', 'gt', 'gte', 'ge', 't_after') THEN + edate := least(edate, 'infinity'); + sdate := greatest(sdate, lower(dtrange)); + ELSIF op IN ('=', 'eq') THEN + edate := least(edate, upper(dtrange)); + sdate := greatest(sdate, lower(dtrange)); + END IF; + RAISE NOTICE '2 OP: %, ARGS: %, DTRANGE: %, SDATE: %, EDATE: %', op, jpitem->'args'->1, dtrange, sdate, edate; + END LOOP; + END IF; + IF sdate > edate THEN + RETURN 'empty'::tstzrange; + END IF; + RETURN tstzrange(sdate,edate, '[]'); +END; +$$ LANGUAGE PLPGSQL STABLE STRICT SET TIME ZONE 'UTC'; + +CREATE OR REPLACE FUNCTION paging_collections( + IN j jsonb +) RETURNS text[] AS $$ +DECLARE + filter jsonb := j->'filter'; + jpitem jsonb; + op text; + args jsonb; + arg jsonb; + collections text[]; +BEGIN + IF j ? 'collections' THEN + collections := to_text_array(j->'collections'); + END IF; + IF NOT (filter @? '$.**.op ? (@ == "or" || @ == "not")') THEN + FOR jpitem IN SELECT j FROM jsonb_path_query(filter,'strict $.** ? (@.args[*].property == "collection")'::jsonpath) j LOOP + RAISE NOTICE 'JPITEM: %', jpitem; + op := jpitem->>'op'; + args := jpitem->'args'; + IF op IN ('=', 'eq', 'in') THEN + FOR arg IN SELECT a FROM jsonb_array_elements(args) a LOOP + IF jsonb_typeof(arg) IN ('string', 'array') THEN + RAISE NOTICE 'arg: %, collections: %', arg, collections; + IF collections IS NULL OR collections = '{}'::text[] THEN + collections := to_text_array(arg); + ELSE + collections := array_intersection(collections, to_text_array(arg)); + END IF; + END IF; + END LOOP; + END IF; + END LOOP; + END IF; + IF collections = '{}'::text[] THEN + RETURN NULL; + END IF; + RETURN collections; +END; +$$ LANGUAGE PLPGSQL STABLE STRICT; +CREATE TABLE items ( + id text NOT NULL, + geometry geometry NOT NULL, + collection text NOT NULL, + datetime timestamptz NOT NULL, + end_datetime timestamptz NOT NULL, + content JSONB NOT NULL +) +PARTITION BY LIST (collection) +; + +CREATE INDEX "datetime_idx" ON items USING BTREE (datetime DESC, end_datetime ASC); +CREATE INDEX "geometry_idx" ON items USING GIST (geometry); + +CREATE STATISTICS datetime_stats (dependencies) on datetime, end_datetime from items; + + +ALTER TABLE items ADD CONSTRAINT items_collections_fk FOREIGN KEY (collection) REFERENCES collections(id) ON DELETE CASCADE DEFERRABLE; + + +CREATE OR REPLACE FUNCTION content_slim(_item jsonb) RETURNS jsonb AS $$ + SELECT strip_jsonb(_item - '{id,geometry,collection,type}'::text[], collection_base_item(_item->>'collection')) - '{id,geometry,collection,type}'::text[]; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; + +CREATE OR REPLACE FUNCTION content_dehydrate(content jsonb) RETURNS items AS $$ + SELECT + content->>'id' as id, + stac_geom(content) as geometry, + content->>'collection' as collection, + stac_datetime(content) as datetime, + stac_end_datetime(content) as end_datetime, + content_slim(content) as content + ; +$$ LANGUAGE SQL STABLE; + +CREATE OR REPLACE FUNCTION include_field(f text, fields jsonb DEFAULT '{}'::jsonb) RETURNS boolean AS $$ +DECLARE + includes jsonb := fields->'include'; + excludes jsonb := fields->'exclude'; +BEGIN + IF f IS NULL THEN + RETURN NULL; + END IF; + + + IF + jsonb_typeof(excludes) = 'array' + AND jsonb_array_length(excludes)>0 + AND excludes ? f + THEN + RETURN FALSE; + END IF; + + IF + ( + jsonb_typeof(includes) = 'array' + AND jsonb_array_length(includes) > 0 + AND includes ? f + ) OR + ( + includes IS NULL + OR jsonb_typeof(includes) = 'null' + OR jsonb_array_length(includes) = 0 + ) + THEN + RETURN TRUE; + END IF; + + RETURN FALSE; +END; +$$ LANGUAGE PLPGSQL IMMUTABLE; + +DROP FUNCTION IF EXISTS content_hydrate(jsonb, jsonb, jsonb); +CREATE OR REPLACE FUNCTION content_hydrate( + _base_item jsonb, + _item jsonb, + fields jsonb DEFAULT '{}'::jsonb +) RETURNS jsonb AS $$ + SELECT merge_jsonb( + jsonb_fields(_item, fields), + jsonb_fields(_base_item, fields) + ); +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; + + + +CREATE OR REPLACE FUNCTION content_hydrate(_item items, _collection collections, fields jsonb DEFAULT '{}'::jsonb) RETURNS jsonb AS $$ +DECLARE + geom jsonb; + bbox jsonb; + output jsonb; + content jsonb; + base_item jsonb := _collection.base_item; +BEGIN + IF include_field('geometry', fields) THEN + geom := ST_ASGeoJson(_item.geometry, 20)::jsonb; + END IF; + output := content_hydrate( + jsonb_build_object( + 'id', _item.id, + 'geometry', geom, + 'collection', _item.collection, + 'type', 'Feature' + ) || _item.content, + _collection.base_item, + fields + ); + + RETURN output; +END; +$$ LANGUAGE PLPGSQL STABLE PARALLEL SAFE; + +CREATE OR REPLACE FUNCTION content_nonhydrated( + _item items, + fields jsonb DEFAULT '{}'::jsonb +) RETURNS jsonb AS $$ +DECLARE + geom jsonb; + bbox jsonb; + output jsonb; +BEGIN + IF include_field('geometry', fields) THEN + geom := ST_ASGeoJson(_item.geometry, 20)::jsonb; + END IF; + output := jsonb_build_object( + 'id', _item.id, + 'geometry', geom, + 'collection', _item.collection, + 'type', 'Feature' + ) || _item.content; + RETURN output; +END; +$$ LANGUAGE PLPGSQL STABLE PARALLEL SAFE; + +CREATE OR REPLACE FUNCTION content_hydrate(_item items, fields jsonb DEFAULT '{}'::jsonb) RETURNS jsonb AS $$ + SELECT content_hydrate( + _item, + (SELECT c FROM collections c WHERE id=_item.collection LIMIT 1), + fields + ); +$$ LANGUAGE SQL STABLE; + + +CREATE UNLOGGED TABLE items_staging ( + content JSONB NOT NULL +); +CREATE UNLOGGED TABLE items_staging_ignore ( + content JSONB NOT NULL +); +CREATE UNLOGGED TABLE items_staging_upsert ( + content JSONB NOT NULL +); + +CREATE OR REPLACE FUNCTION items_staging_triggerfunc() RETURNS TRIGGER AS $$ +DECLARE + p record; + _partitions text[]; + ts timestamptz := clock_timestamp(); +BEGIN + RAISE NOTICE 'Creating Partitions. %', clock_timestamp() - ts; + WITH ranges AS ( + SELECT + n.content->>'collection' as collection, + stac_daterange(n.content->'properties') as dtr + FROM newdata n + ), p AS ( + SELECT + collection, + lower(dtr) as datetime, + upper(dtr) as end_datetime, + (partition_name( + collection, + lower(dtr) + )).partition_name as name + FROM ranges + ) + INSERT INTO partitions (collection, datetime_range, end_datetime_range) + SELECT + collection, + tstzrange(min(datetime), max(datetime), '[]') as datetime_range, + tstzrange(min(end_datetime), max(end_datetime), '[]') as end_datetime_range + FROM p + GROUP BY collection, name + ON CONFLICT (name) DO UPDATE SET + datetime_range = EXCLUDED.datetime_range, + end_datetime_range = EXCLUDED.end_datetime_range + ; + + RAISE NOTICE 'Doing the insert. %', clock_timestamp() - ts; + IF TG_TABLE_NAME = 'items_staging' THEN + INSERT INTO items + SELECT + (content_dehydrate(content)).* + FROM newdata; + DELETE FROM items_staging; + ELSIF TG_TABLE_NAME = 'items_staging_ignore' THEN + INSERT INTO items + SELECT + (content_dehydrate(content)).* + FROM newdata + ON CONFLICT DO NOTHING; + DELETE FROM items_staging_ignore; + ELSIF TG_TABLE_NAME = 'items_staging_upsert' THEN + WITH staging_formatted AS ( + SELECT (content_dehydrate(content)).* FROM newdata + ), deletes AS ( + DELETE FROM items i USING staging_formatted s + WHERE + i.id = s.id + AND i.collection = s.collection + AND i IS DISTINCT FROM s + RETURNING i.id, i.collection + ) + INSERT INTO items + SELECT s.* FROM + staging_formatted s + JOIN deletes d + USING (id, collection); + DELETE FROM items_staging_upsert; + END IF; + RAISE NOTICE 'Done. %', clock_timestamp() - ts; + + RETURN NULL; + +END; +$$ LANGUAGE PLPGSQL; + + +CREATE TRIGGER items_staging_insert_trigger AFTER INSERT ON items_staging REFERENCING NEW TABLE AS newdata + FOR EACH STATEMENT EXECUTE PROCEDURE items_staging_triggerfunc(); + +CREATE TRIGGER items_staging_insert_ignore_trigger AFTER INSERT ON items_staging_ignore REFERENCING NEW TABLE AS newdata + FOR EACH STATEMENT EXECUTE PROCEDURE items_staging_triggerfunc(); + +CREATE TRIGGER items_staging_insert_upsert_trigger AFTER INSERT ON items_staging_upsert REFERENCING NEW TABLE AS newdata + FOR EACH STATEMENT EXECUTE PROCEDURE items_staging_triggerfunc(); + + + + +CREATE OR REPLACE FUNCTION item_by_id(_id text, _collection text DEFAULT NULL) RETURNS items AS +$$ +DECLARE + i items%ROWTYPE; +BEGIN + SELECT * INTO i FROM items WHERE id=_id AND (_collection IS NULL OR collection=_collection) LIMIT 1; + RETURN i; +END; +$$ LANGUAGE PLPGSQL STABLE SECURITY DEFINER SET SEARCH_PATH TO pgstac, public; + +CREATE OR REPLACE FUNCTION get_item(_id text, _collection text DEFAULT NULL) RETURNS jsonb AS $$ + SELECT content_hydrate(items) FROM items WHERE id=_id AND (_collection IS NULL OR collection=_collection); +$$ LANGUAGE SQL STABLE SECURITY DEFINER SET SEARCH_PATH TO pgstac, public; + +CREATE OR REPLACE FUNCTION delete_item(_id text, _collection text DEFAULT NULL) RETURNS VOID AS $$ +DECLARE +out items%ROWTYPE; +BEGIN + DELETE FROM items WHERE id = _id AND (_collection IS NULL OR collection=_collection) RETURNING * INTO STRICT out; +END; +$$ LANGUAGE PLPGSQL; + +--/* +CREATE OR REPLACE FUNCTION create_item(data jsonb) RETURNS VOID AS $$ + INSERT INTO items_staging (content) VALUES (data); +$$ LANGUAGE SQL SET SEARCH_PATH TO pgstac,public; + + +CREATE OR REPLACE FUNCTION update_item(content jsonb) RETURNS VOID AS $$ +DECLARE + old items %ROWTYPE; + out items%ROWTYPE; +BEGIN + PERFORM delete_item(content->>'id', content->>'collection'); + PERFORM create_item(content); +END; +$$ LANGUAGE PLPGSQL SET SEARCH_PATH TO pgstac,public; + +CREATE OR REPLACE FUNCTION upsert_item(data jsonb) RETURNS VOID AS $$ + INSERT INTO items_staging_upsert (content) VALUES (data); +$$ LANGUAGE SQL SET SEARCH_PATH TO pgstac,public; + +CREATE OR REPLACE FUNCTION create_items(data jsonb) RETURNS VOID AS $$ + INSERT INTO items_staging (content) + SELECT * FROM jsonb_array_elements(data); +$$ LANGUAGE SQL SET SEARCH_PATH TO pgstac,public; + +CREATE OR REPLACE FUNCTION upsert_items(data jsonb) RETURNS VOID AS $$ + INSERT INTO items_staging_upsert (content) + SELECT * FROM jsonb_array_elements(data); +$$ LANGUAGE SQL SET SEARCH_PATH TO pgstac,public; + + +CREATE OR REPLACE FUNCTION collection_bbox(id text) RETURNS jsonb AS $$ + SELECT (replace(replace(replace(st_extent(geometry)::text,'BOX(','[['),')',']]'),' ',','))::jsonb + FROM items WHERE collection=$1; + ; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE SET SEARCH_PATH TO pgstac, public; + +CREATE OR REPLACE FUNCTION collection_temporal_extent(id text) RETURNS jsonb AS $$ + SELECT to_jsonb(array[array[min(datetime)::text, max(datetime)::text]]) + FROM items WHERE collection=$1; +; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE SET SEARCH_PATH TO pgstac, public; + +CREATE OR REPLACE FUNCTION update_collection_extents() RETURNS VOID AS $$ +UPDATE collections SET + content = content || + jsonb_build_object( + 'extent', jsonb_build_object( + 'spatial', jsonb_build_object( + 'bbox', collection_bbox(collections.id) + ), + 'temporal', jsonb_build_object( + 'interval', collection_temporal_extent(collections.id) + ) + ) + ) +; +$$ LANGUAGE SQL; +CREATE VIEW partition_steps AS +SELECT + name, + date_trunc('month',lower(datetime_range)) as sdate, + date_trunc('month', upper(datetime_range)) + '1 month'::interval as edate + FROM partitions WHERE datetime_range IS NOT NULL AND datetime_range != 'empty'::tstzrange + ORDER BY datetime_range ASC +; + +CREATE OR REPLACE FUNCTION chunker( + IN _where text, + OUT s timestamptz, + OUT e timestamptz +) RETURNS SETOF RECORD AS $$ +DECLARE + explain jsonb; +BEGIN + IF _where IS NULL THEN + _where := ' TRUE '; + END IF; + EXECUTE format('EXPLAIN (format json) SELECT 1 FROM items WHERE %s;', _where) + INTO explain; + + RETURN QUERY + WITH t AS ( + SELECT j->>0 as p FROM + jsonb_path_query( + explain, + 'strict $.**."Relation Name" ? (@ != null)' + ) j + ), + parts AS ( + SELECT sdate, edate FROM t JOIN partition_steps ON (t.p = name) + ), + times AS ( + SELECT sdate FROM parts + UNION + SELECT edate FROM parts + ), + uniq AS ( + SELECT DISTINCT sdate FROM times ORDER BY sdate + ), + last AS ( + SELECT sdate, lead(sdate, 1) over () as edate FROM uniq + ) + SELECT sdate, edate FROM last WHERE edate IS NOT NULL; +END; +$$ LANGUAGE PLPGSQL; + +CREATE OR REPLACE FUNCTION partition_queries( + IN _where text DEFAULT 'TRUE', + IN _orderby text DEFAULT 'datetime DESC, id DESC', + IN partitions text[] DEFAULT NULL +) RETURNS SETOF text AS $$ +DECLARE + query text; + sdate timestamptz; + edate timestamptz; +BEGIN +IF _where IS NULL OR trim(_where) = '' THEN + _where = ' TRUE '; +END IF; +RAISE NOTICE 'Getting chunks for % %', _where, _orderby; +IF _orderby ILIKE 'datetime d%' THEN + FOR sdate, edate IN SELECT * FROM chunker(_where) ORDER BY 1 DESC LOOP + RETURN NEXT format($q$ + SELECT * FROM items + WHERE + datetime >= %L AND datetime < %L + AND (%s) + ORDER BY %s + $q$, + sdate, + edate, + _where, + _orderby + ); + END LOOP; +ELSIF _orderby ILIKE 'datetime a%' THEN + FOR sdate, edate IN SELECT * FROM chunker(_where) ORDER BY 1 ASC LOOP + RETURN NEXT format($q$ + SELECT * FROM items + WHERE + datetime >= %L AND datetime < %L + AND (%s) + ORDER BY %s + $q$, + sdate, + edate, + _where, + _orderby + ); + END LOOP; +ELSE + query := format($q$ + SELECT * FROM items + WHERE %s + ORDER BY %s + $q$, _where, _orderby + ); + + RETURN NEXT query; + RETURN; +END IF; + +RETURN; +END; +$$ LANGUAGE PLPGSQL SET SEARCH_PATH TO pgstac,public; + +CREATE OR REPLACE FUNCTION partition_query_view( + IN _where text DEFAULT 'TRUE', + IN _orderby text DEFAULT 'datetime DESC, id DESC', + IN _limit int DEFAULT 10 +) RETURNS text AS $$ + WITH p AS ( + SELECT * FROM partition_queries(_where, _orderby) p + ) + SELECT + CASE WHEN EXISTS (SELECT 1 FROM p) THEN + (SELECT format($q$ + SELECT * FROM ( + %s + ) total LIMIT %s + $q$, + string_agg( + format($q$ SELECT * FROM ( %s ) AS sub $q$, p), + ' + UNION ALL + ' + ), + _limit + )) + ELSE NULL + END FROM p; +$$ LANGUAGE SQL IMMUTABLE; + + + + +CREATE OR REPLACE FUNCTION stac_search_to_where(j jsonb) RETURNS text AS $$ +DECLARE + where_segments text[]; + _where text; + dtrange tstzrange; + collections text[]; + geom geometry; + sdate timestamptz; + edate timestamptz; + filterlang text; + filter jsonb := j->'filter'; +BEGIN + IF j ? 'ids' THEN + where_segments := where_segments || format('id = ANY (%L) ', to_text_array(j->'ids')); + END IF; + + IF j ? 'collections' THEN + collections := to_text_array(j->'collections'); + where_segments := where_segments || format('collection = ANY (%L) ', collections); + END IF; + + IF j ? 'datetime' THEN + dtrange := parse_dtrange(j->'datetime'); + sdate := lower(dtrange); + edate := upper(dtrange); + + where_segments := where_segments || format(' datetime <= %L::timestamptz AND end_datetime >= %L::timestamptz ', + edate, + sdate + ); + END IF; + + geom := stac_geom(j); + IF geom IS NOT NULL THEN + where_segments := where_segments || format('st_intersects(geometry, %L)',geom); + END IF; + + filterlang := COALESCE( + j->>'filter-lang', + get_setting('default-filter-lang', j->'conf') + ); + IF NOT filter @? '$.**.op' THEN + filterlang := 'cql-json'; + END IF; + + IF filterlang NOT IN ('cql-json','cql2-json') AND j ? 'filter' THEN + RAISE EXCEPTION '% is not a supported filter-lang. Please use cql-json or cql2-json.', filterlang; + END IF; + + IF j ? 'query' AND j ? 'filter' THEN + RAISE EXCEPTION 'Can only use either query or filter at one time.'; + END IF; + + IF j ? 'query' THEN + filter := query_to_cql2(j->'query'); + ELSIF filterlang = 'cql-json' THEN + filter := cql1_to_cql2(filter); + END IF; + RAISE NOTICE 'FILTER: %', filter; + where_segments := where_segments || cql2_query(filter); + IF cardinality(where_segments) < 1 THEN + RETURN ' TRUE '; + END IF; + + _where := array_to_string(array_remove(where_segments, NULL), ' AND '); + + IF _where IS NULL OR BTRIM(_where) = '' THEN + RETURN ' TRUE '; + END IF; + RETURN _where; + +END; +$$ LANGUAGE PLPGSQL STABLE; + + +CREATE OR REPLACE FUNCTION parse_sort_dir(_dir text, reverse boolean default false) RETURNS text AS $$ + WITH t AS ( + SELECT COALESCE(upper(_dir), 'ASC') as d + ) SELECT + CASE + WHEN NOT reverse THEN d + WHEN d = 'ASC' THEN 'DESC' + WHEN d = 'DESC' THEN 'ASC' + END + FROM t; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; + +CREATE OR REPLACE FUNCTION sort_dir_to_op(_dir text, prev boolean default false) RETURNS text AS $$ + WITH t AS ( + SELECT COALESCE(upper(_dir), 'ASC') as d + ) SELECT + CASE + WHEN d = 'ASC' AND prev THEN '<=' + WHEN d = 'DESC' AND prev THEN '>=' + WHEN d = 'ASC' THEN '>=' + WHEN d = 'DESC' THEN '<=' + END + FROM t; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; + + +CREATE OR REPLACE FUNCTION sort_sqlorderby( + _search jsonb DEFAULT NULL, + reverse boolean DEFAULT FALSE +) RETURNS text AS $$ + WITH sortby AS ( + SELECT coalesce(_search->'sortby','[{"field":"datetime", "direction":"desc"}]') as sort + ), withid AS ( + SELECT CASE + WHEN sort @? '$[*] ? (@.field == "id")' THEN sort + ELSE sort || '[{"field":"id", "direction":"desc"}]'::jsonb + END as sort + FROM sortby + ), withid_rows AS ( + SELECT jsonb_array_elements(sort) as value FROM withid + ),sorts AS ( + SELECT + coalesce( + -- field_orderby((items_path(value->>'field')).path_txt), + (queryable(value->>'field')).expression + ) as key, + parse_sort_dir(value->>'direction', reverse) as dir + FROM withid_rows + ) + SELECT array_to_string( + array_agg(concat(key, ' ', dir)), + ', ' + ) FROM sorts; +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION get_sort_dir(sort_item jsonb) RETURNS text AS $$ + SELECT CASE WHEN sort_item->>'direction' ILIKE 'desc%' THEN 'DESC' ELSE 'ASC' END; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; + + +CREATE OR REPLACE FUNCTION get_token_filter(_search jsonb = '{}'::jsonb, token_rec jsonb DEFAULT NULL) RETURNS text AS $$ +DECLARE + token_id text; + filters text[] := '{}'::text[]; + prev boolean := TRUE; + field text; + dir text; + sort record; + orfilters text[] := '{}'::text[]; + andfilters text[] := '{}'::text[]; + output text; + token_where text; +BEGIN + RAISE NOTICE 'Getting Token Filter. % %', _search, token_rec; + -- If no token provided return NULL + IF token_rec IS NULL THEN + IF NOT (_search ? 'token' AND + ( + (_search->>'token' ILIKE 'prev:%') + OR + (_search->>'token' ILIKE 'next:%') + ) + ) THEN + RETURN NULL; + END IF; + prev := (_search->>'token' ILIKE 'prev:%'); + token_id := substr(_search->>'token', 6); + SELECT to_jsonb(items) INTO token_rec + FROM items WHERE id=token_id; + END IF; + RAISE NOTICE 'TOKEN ID: % %', token_rec, token_rec->'id'; + + CREATE TEMP TABLE sorts ( + _row int GENERATED ALWAYS AS IDENTITY NOT NULL, + _field text PRIMARY KEY, + _dir text NOT NULL, + _val text + ) ON COMMIT DROP; + + -- Make sure we only have distinct columns to sort with taking the first one we get + INSERT INTO sorts (_field, _dir) + SELECT + (queryable(value->>'field')).expression, + get_sort_dir(value) + FROM + jsonb_array_elements(coalesce(_search->'sortby','[{"field":"datetime","direction":"desc"}]')) + ON CONFLICT DO NOTHING + ; + RAISE NOTICE 'sorts 1: %', (SELECT jsonb_agg(to_json(sorts)) FROM sorts); + -- Get the first sort direction provided. As the id is a primary key, if there are any + -- sorts after id they won't do anything, so make sure that id is the last sort item. + SELECT _dir INTO dir FROM sorts ORDER BY _row ASC LIMIT 1; + IF EXISTS (SELECT 1 FROM sorts WHERE _field = 'id') THEN + DELETE FROM sorts WHERE _row > (SELECT _row FROM sorts WHERE _field = 'id' ORDER BY _row ASC); + ELSE + INSERT INTO sorts (_field, _dir) VALUES ('id', dir); + END IF; + + -- Add value from looked up item to the sorts table + UPDATE sorts SET _val=quote_literal(token_rec->>_field); + + -- Check if all sorts are the same direction and use row comparison + -- to filter + RAISE NOTICE 'sorts 2: %', (SELECT jsonb_agg(to_json(sorts)) FROM sorts); + + IF (SELECT count(DISTINCT _dir) FROM sorts) = 1 THEN + SELECT format( + '(%s) %s (%s)', + concat_ws(', ', VARIADIC array_agg(quote_ident(_field))), + CASE WHEN (prev AND dir = 'ASC') OR (NOT prev AND dir = 'DESC') THEN '<' ELSE '>' END, + concat_ws(', ', VARIADIC array_agg(_val)) + ) INTO output FROM sorts + WHERE token_rec ? _field + ; + ELSE + FOR sort IN SELECT * FROM sorts ORDER BY _row asc LOOP + RAISE NOTICE 'SORT: %', sort; + IF sort._row = 1 THEN + orfilters := orfilters || format('(%s %s %s)', + quote_ident(sort._field), + CASE WHEN (prev AND sort._dir = 'ASC') OR (NOT prev AND sort._dir = 'DESC') THEN '<' ELSE '>' END, + sort._val + ); + ELSE + orfilters := orfilters || format('(%s AND %s %s %s)', + array_to_string(andfilters, ' AND '), + quote_ident(sort._field), + CASE WHEN (prev AND sort._dir = 'ASC') OR (NOT prev AND sort._dir = 'DESC') THEN '<' ELSE '>' END, + sort._val + ); + + END IF; + andfilters := andfilters || format('%s = %s', + quote_ident(sort._field), + sort._val + ); + END LOOP; + output := array_to_string(orfilters, ' OR '); + END IF; + DROP TABLE IF EXISTS sorts; + token_where := concat('(',coalesce(output,'true'),')'); + IF trim(token_where) = '' THEN + token_where := NULL; + END IF; + RAISE NOTICE 'TOKEN_WHERE: |%|',token_where; + RETURN token_where; + END; +$$ LANGUAGE PLPGSQL; + +CREATE OR REPLACE FUNCTION search_tohash(jsonb) RETURNS jsonb AS $$ + SELECT $1 - '{token,limit,context,includes,excludes}'::text[]; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; + +CREATE OR REPLACE FUNCTION search_hash(jsonb, jsonb) RETURNS text AS $$ + SELECT md5(concat(search_tohash($1)::text,$2::text)); +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; + +CREATE TABLE IF NOT EXISTS searches( + hash text GENERATED ALWAYS AS (search_hash(search, metadata)) STORED PRIMARY KEY, + search jsonb NOT NULL, + _where text, + orderby text, + lastused timestamptz DEFAULT now(), + usecount bigint DEFAULT 0, + metadata jsonb DEFAULT '{}'::jsonb NOT NULL +); +CREATE TABLE IF NOT EXISTS search_wheres( + id bigint generated always as identity primary key, + _where text NOT NULL, + lastused timestamptz DEFAULT now(), + usecount bigint DEFAULT 0, + statslastupdated timestamptz, + estimated_count bigint, + estimated_cost float, + time_to_estimate float, + total_count bigint, + time_to_count float, + partitions text[] +); + +CREATE INDEX IF NOT EXISTS search_wheres_partitions ON search_wheres USING GIN (partitions); +CREATE UNIQUE INDEX IF NOT EXISTS search_wheres_where ON search_wheres ((md5(_where))); + +CREATE OR REPLACE FUNCTION where_stats(inwhere text, updatestats boolean default false, conf jsonb default null) RETURNS search_wheres AS $$ +DECLARE + t timestamptz; + i interval; + explain_json jsonb; + partitions text[]; + sw search_wheres%ROWTYPE; + inwhere_hash text := md5(inwhere); + _context text := lower(context(conf)); + _stats_ttl interval := context_stats_ttl(conf); + _estimated_cost float := context_estimated_cost(conf); + _estimated_count int := context_estimated_count(conf); +BEGIN + IF _context = 'off' THEN + sw._where := inwhere; + return sw; + END IF; + + SELECT * INTO sw FROM search_wheres WHERE md5(_where)=inwhere_hash FOR UPDATE; + + -- Update statistics if explicitly set, if statistics do not exist, or statistics ttl has expired + IF NOT updatestats THEN + RAISE NOTICE 'Checking if update is needed for: % .', inwhere; + RAISE NOTICE 'Stats Last Updated: %', sw.statslastupdated; + RAISE NOTICE 'TTL: %, Age: %', _stats_ttl, now() - sw.statslastupdated; + RAISE NOTICE 'Context: %, Existing Total: %', _context, sw.total_count; + IF + sw.statslastupdated IS NULL + OR (now() - sw.statslastupdated) > _stats_ttl + OR (context(conf) != 'off' AND sw.total_count IS NULL) + THEN + updatestats := TRUE; + END IF; + END IF; + + sw._where := inwhere; + sw.lastused := now(); + sw.usecount := coalesce(sw.usecount,0) + 1; + + IF NOT updatestats THEN + UPDATE search_wheres SET + lastused = sw.lastused, + usecount = sw.usecount + WHERE md5(_where) = inwhere_hash + RETURNING * INTO sw + ; + RETURN sw; + END IF; + + -- Use explain to get estimated count/cost and a list of the partitions that would be hit by the query + t := clock_timestamp(); + EXECUTE format('EXPLAIN (format json) SELECT 1 FROM items WHERE %s', inwhere) + INTO explain_json; + RAISE NOTICE 'Time for just the explain: %', clock_timestamp() - t; + i := clock_timestamp() - t; + + sw.statslastupdated := now(); + sw.estimated_count := explain_json->0->'Plan'->'Plan Rows'; + sw.estimated_cost := explain_json->0->'Plan'->'Total Cost'; + sw.time_to_estimate := extract(epoch from i); + + RAISE NOTICE 'ESTIMATED_COUNT: % < %', sw.estimated_count, _estimated_count; + RAISE NOTICE 'ESTIMATED_COST: % < %', sw.estimated_cost, _estimated_cost; + + -- Do a full count of rows if context is set to on or if auto is set and estimates are low enough + IF + _context = 'on' + OR + ( _context = 'auto' AND + ( + sw.estimated_count < _estimated_count + AND + sw.estimated_cost < _estimated_cost + ) + ) + THEN + t := clock_timestamp(); + RAISE NOTICE 'Calculating actual count...'; + EXECUTE format( + 'SELECT count(*) FROM items WHERE %s', + inwhere + ) INTO sw.total_count; + i := clock_timestamp() - t; + RAISE NOTICE 'Actual Count: % -- %', sw.total_count, i; + sw.time_to_count := extract(epoch FROM i); + ELSE + sw.total_count := NULL; + sw.time_to_count := NULL; + END IF; + + + INSERT INTO search_wheres + (_where, lastused, usecount, statslastupdated, estimated_count, estimated_cost, time_to_estimate, partitions, total_count, time_to_count) + SELECT sw._where, sw.lastused, sw.usecount, sw.statslastupdated, sw.estimated_count, sw.estimated_cost, sw.time_to_estimate, sw.partitions, sw.total_count, sw.time_to_count + ON CONFLICT ((md5(_where))) + DO UPDATE + SET + lastused = sw.lastused, + usecount = sw.usecount, + statslastupdated = sw.statslastupdated, + estimated_count = sw.estimated_count, + estimated_cost = sw.estimated_cost, + time_to_estimate = sw.time_to_estimate, + total_count = sw.total_count, + time_to_count = sw.time_to_count + ; + RETURN sw; +END; +$$ LANGUAGE PLPGSQL ; + + + +DROP FUNCTION IF EXISTS search_query; +CREATE OR REPLACE FUNCTION search_query( + _search jsonb = '{}'::jsonb, + updatestats boolean = false, + _metadata jsonb = '{}'::jsonb +) RETURNS searches AS $$ +DECLARE + search searches%ROWTYPE; + pexplain jsonb; + t timestamptz; + i interval; +BEGIN + SELECT * INTO search FROM searches + WHERE hash=search_hash(_search, _metadata) FOR UPDATE; + + -- Calculate the where clause if not already calculated + IF search._where IS NULL THEN + search._where := stac_search_to_where(_search); + END IF; + + -- Calculate the order by clause if not already calculated + IF search.orderby IS NULL THEN + search.orderby := sort_sqlorderby(_search); + END IF; + + PERFORM where_stats(search._where, updatestats, _search->'conf'); + + search.lastused := now(); + search.usecount := coalesce(search.usecount, 0) + 1; + INSERT INTO searches (search, _where, orderby, lastused, usecount, metadata) + VALUES (_search, search._where, search.orderby, search.lastused, search.usecount, _metadata) + ON CONFLICT (hash) DO + UPDATE SET + _where = EXCLUDED._where, + orderby = EXCLUDED.orderby, + lastused = EXCLUDED.lastused, + usecount = EXCLUDED.usecount, + metadata = EXCLUDED.metadata + RETURNING * INTO search + ; + RETURN search; + +END; +$$ LANGUAGE PLPGSQL; + +CREATE OR REPLACE FUNCTION search(_search jsonb = '{}'::jsonb) RETURNS jsonb AS $$ +DECLARE + searches searches%ROWTYPE; + _where text; + token_where text; + full_where text; + orderby text; + query text; + token_type text := substr(_search->>'token',1,4); + _limit int := coalesce((_search->>'limit')::int, 10); + curs refcursor; + cntr int := 0; + iter_record items%ROWTYPE; + first_record jsonb; + first_item items%ROWTYPE; + last_item items%ROWTYPE; + last_record jsonb; + out_records jsonb := '[]'::jsonb; + prev_query text; + next text; + prev_id text; + has_next boolean := false; + has_prev boolean := false; + prev text; + total_count bigint; + context jsonb; + collection jsonb; + includes text[]; + excludes text[]; + exit_flag boolean := FALSE; + batches int := 0; + timer timestamptz := clock_timestamp(); + pstart timestamptz; + pend timestamptz; + pcurs refcursor; + search_where search_wheres%ROWTYPE; + id text; +BEGIN +CREATE TEMP TABLE results (content jsonb) ON COMMIT DROP; +-- if ids is set, short circuit and just use direct ids query for each id +-- skip any paging or caching +-- hard codes ordering in the same order as the array of ids +IF _search ? 'ids' THEN + INSERT INTO results + SELECT + CASE WHEN _search->'conf'->>'nohydrate' IS NOT NULL AND (_search->'conf'->>'nohydrate')::boolean = true THEN + content_nonhydrated(items, _search->'fields') + ELSE + content_hydrate(items, _search->'fields') + END + FROM items WHERE + items.id = ANY(to_text_array(_search->'ids')) + AND + CASE WHEN _search ? 'collections' THEN + items.collection = ANY(to_text_array(_search->'collections')) + ELSE TRUE + END + ORDER BY items.datetime desc, items.id desc + ; + SELECT INTO total_count count(*) FROM results; +ELSE + searches := search_query(_search); + _where := searches._where; + orderby := searches.orderby; + search_where := where_stats(_where); + total_count := coalesce(search_where.total_count, search_where.estimated_count); + + IF token_type='prev' THEN + token_where := get_token_filter(_search, null::jsonb); + orderby := sort_sqlorderby(_search, TRUE); + END IF; + IF token_type='next' THEN + token_where := get_token_filter(_search, null::jsonb); + END IF; + + full_where := concat_ws(' AND ', _where, token_where); + RAISE NOTICE 'FULL QUERY % %', full_where, clock_timestamp()-timer; + timer := clock_timestamp(); + + FOR query IN SELECT partition_queries(full_where, orderby, search_where.partitions) LOOP + timer := clock_timestamp(); + query := format('%s LIMIT %s', query, _limit + 1); + RAISE NOTICE 'Partition Query: %', query; + batches := batches + 1; + -- curs = create_cursor(query); + OPEN curs FOR EXECUTE query; + LOOP + FETCH curs into iter_record; + EXIT WHEN NOT FOUND; + cntr := cntr + 1; + + IF _search->'conf'->>'nohydrate' IS NOT NULL AND (_search->'conf'->>'nohydrate')::boolean = true THEN + last_record := content_nonhydrated(iter_record, _search->'fields'); + ELSE + last_record := content_hydrate(iter_record, _search->'fields'); + END IF; + last_item := iter_record; + IF cntr = 1 THEN + first_item := last_item; + first_record := last_record; + END IF; + IF cntr <= _limit THEN + INSERT INTO results (content) VALUES (last_record); + ELSIF cntr > _limit THEN + has_next := true; + exit_flag := true; + EXIT; + END IF; + END LOOP; + CLOSE curs; + RAISE NOTICE 'Query took %.', clock_timestamp()-timer; + timer := clock_timestamp(); + EXIT WHEN exit_flag; + END LOOP; + RAISE NOTICE 'Scanned through % partitions.', batches; +END IF; + +SELECT jsonb_agg(content) INTO out_records FROM results WHERE content is not NULL; + +DROP TABLE results; + + +-- Flip things around if this was the result of a prev token query +IF token_type='prev' THEN + out_records := flip_jsonb_array(out_records); + first_record := last_record; +END IF; + +-- If this query has a token, see if there is data before the first record +IF _search ? 'token' THEN + prev_query := format( + 'SELECT 1 FROM items WHERE %s LIMIT 1', + concat_ws( + ' AND ', + _where, + trim(get_token_filter(_search, to_jsonb(first_item))) + ) + ); + RAISE NOTICE 'Query to get previous record: % --- %', prev_query, first_record; + EXECUTE prev_query INTO has_prev; + IF FOUND and has_prev IS NOT NULL THEN + RAISE NOTICE 'Query results from prev query: %', has_prev; + has_prev := TRUE; + END IF; +END IF; +has_prev := COALESCE(has_prev, FALSE); + +IF has_prev THEN + prev := out_records->0->>'id'; +END IF; +IF has_next OR token_type='prev' THEN + next := out_records->-1->>'id'; +END IF; + +IF context(_search->'conf') != 'off' THEN + context := jsonb_strip_nulls(jsonb_build_object( + 'limit', _limit, + 'matched', total_count, + 'returned', coalesce(jsonb_array_length(out_records), 0) + )); +ELSE + context := jsonb_strip_nulls(jsonb_build_object( + 'limit', _limit, + 'returned', coalesce(jsonb_array_length(out_records), 0) + )); +END IF; + +collection := jsonb_build_object( + 'type', 'FeatureCollection', + 'features', coalesce(out_records, '[]'::jsonb), + 'next', next, + 'prev', prev, + 'context', context +); + +RETURN collection; +END; +$$ LANGUAGE PLPGSQL SECURITY DEFINER SET SEARCH_PATH TO pgstac, public; + + +CREATE OR REPLACE FUNCTION search_cursor(_search jsonb = '{}'::jsonb) RETURNS refcursor AS $$ +DECLARE + curs refcursor; + searches searches%ROWTYPE; + _where text; + _orderby text; + q text; + +BEGIN + searches := search_query(_search); + _where := searches._where; + _orderby := searches.orderby; + + OPEN curs FOR + WITH p AS ( + SELECT * FROM partition_queries(_where, _orderby) p + ) + SELECT + CASE WHEN EXISTS (SELECT 1 FROM p) THEN + (SELECT format($q$ + SELECT * FROM ( + %s + ) total + $q$, + string_agg( + format($q$ SELECT * FROM ( %s ) AS sub $q$, p), + ' + UNION ALL + ' + ) + )) + ELSE NULL + END FROM p; + RETURN curs; +END; +$$ LANGUAGE PLPGSQL; +SET SEARCH_PATH TO pgstac, public; + +CREATE OR REPLACE FUNCTION tileenvelope(zoom int, x int, y int) RETURNS geometry AS $$ +WITH t AS ( + SELECT + 20037508.3427892 as merc_max, + -20037508.3427892 as merc_min, + (2 * 20037508.3427892) / (2 ^ zoom) as tile_size +) +SELECT st_makeenvelope( + merc_min + (tile_size * x), + merc_max - (tile_size * (y + 1)), + merc_min + (tile_size * (x + 1)), + merc_max - (tile_size * y), + 3857 +) FROM t; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;DROP FUNCTION IF EXISTS mercgrid; + + +CREATE OR REPLACE FUNCTION ftime() RETURNS interval as $$ +SELECT age(clock_timestamp(), transaction_timestamp()); +$$ LANGUAGE SQL; +SET SEARCH_PATH to pgstac, public; + +DROP FUNCTION IF EXISTS geometrysearch; +CREATE OR REPLACE FUNCTION geometrysearch( + IN geom geometry, + IN queryhash text, + IN fields jsonb DEFAULT NULL, + IN _scanlimit int DEFAULT 10000, + IN _limit int DEFAULT 100, + IN _timelimit interval DEFAULT '5 seconds'::interval, + IN exitwhenfull boolean DEFAULT TRUE, -- Return as soon as the passed in geometry is full covered + IN skipcovered boolean DEFAULT TRUE -- Skip any items that would show up completely under the previous items +) RETURNS jsonb AS $$ +DECLARE + search searches%ROWTYPE; + curs refcursor; + _where text; + query text; + iter_record items%ROWTYPE; + out_records jsonb := '{}'::jsonb[]; + exit_flag boolean := FALSE; + counter int := 1; + scancounter int := 1; + remaining_limit int := _scanlimit; + tilearea float; + unionedgeom geometry; + clippedgeom geometry; + unionedgeom_area float := 0; + prev_area float := 0; + excludes text[]; + includes text[]; + +BEGIN + DROP TABLE IF EXISTS pgstac_results; + CREATE TEMP TABLE pgstac_results (content jsonb) ON COMMIT DROP; + + -- If skipcovered is true then you will always want to exit when the passed in geometry is full + IF skipcovered THEN + exitwhenfull := TRUE; + END IF; + + SELECT * INTO search FROM searches WHERE hash=queryhash; + + IF NOT FOUND THEN + RAISE EXCEPTION 'Search with Query Hash % Not Found', queryhash; + END IF; + + tilearea := st_area(geom); + _where := format('%s AND st_intersects(geometry, %L::geometry)', search._where, geom); + + + FOR query IN SELECT * FROM partition_queries(_where, search.orderby) LOOP + query := format('%s LIMIT %L', query, remaining_limit); + RAISE NOTICE '%', query; + OPEN curs FOR EXECUTE query; + LOOP + FETCH curs INTO iter_record; + EXIT WHEN NOT FOUND; + IF exitwhenfull OR skipcovered THEN -- If we are not using exitwhenfull or skipcovered, we do not need to do expensive geometry operations + clippedgeom := st_intersection(geom, iter_record.geometry); + + IF unionedgeom IS NULL THEN + unionedgeom := clippedgeom; + ELSE + unionedgeom := st_union(unionedgeom, clippedgeom); + END IF; + + unionedgeom_area := st_area(unionedgeom); + + IF skipcovered AND prev_area = unionedgeom_area THEN + scancounter := scancounter + 1; + CONTINUE; + END IF; + + prev_area := unionedgeom_area; + + RAISE NOTICE '% % % %', unionedgeom_area/tilearea, counter, scancounter, ftime(); + END IF; + RAISE NOTICE '% %', iter_record, content_hydrate(iter_record, fields); + INSERT INTO pgstac_results (content) VALUES (content_hydrate(iter_record, fields)); + + IF counter >= _limit + OR scancounter > _scanlimit + OR ftime() > _timelimit + OR (exitwhenfull AND unionedgeom_area >= tilearea) + THEN + exit_flag := TRUE; + EXIT; + END IF; + counter := counter + 1; + scancounter := scancounter + 1; + + END LOOP; + CLOSE curs; + EXIT WHEN exit_flag; + remaining_limit := _scanlimit - scancounter; + END LOOP; + + SELECT jsonb_agg(content) INTO out_records FROM pgstac_results WHERE content IS NOT NULL; + + RETURN jsonb_build_object( + 'type', 'FeatureCollection', + 'features', coalesce(out_records, '[]'::jsonb) + ); +END; +$$ LANGUAGE PLPGSQL; + +DROP FUNCTION IF EXISTS geojsonsearch; +CREATE OR REPLACE FUNCTION geojsonsearch( + IN geojson jsonb, + IN queryhash text, + IN fields jsonb DEFAULT NULL, + IN _scanlimit int DEFAULT 10000, + IN _limit int DEFAULT 100, + IN _timelimit interval DEFAULT '5 seconds'::interval, + IN exitwhenfull boolean DEFAULT TRUE, + IN skipcovered boolean DEFAULT TRUE +) RETURNS jsonb AS $$ + SELECT * FROM geometrysearch( + st_geomfromgeojson(geojson), + queryhash, + fields, + _scanlimit, + _limit, + _timelimit, + exitwhenfull, + skipcovered + ); +$$ LANGUAGE SQL; + +DROP FUNCTION IF EXISTS xyzsearch; +CREATE OR REPLACE FUNCTION xyzsearch( + IN _x int, + IN _y int, + IN _z int, + IN queryhash text, + IN fields jsonb DEFAULT NULL, + IN _scanlimit int DEFAULT 10000, + IN _limit int DEFAULT 100, + IN _timelimit interval DEFAULT '5 seconds'::interval, + IN exitwhenfull boolean DEFAULT TRUE, + IN skipcovered boolean DEFAULT TRUE +) RETURNS jsonb AS $$ + SELECT * FROM geometrysearch( + st_transform(tileenvelope(_z, _x, _y), 4326), + queryhash, + fields, + _scanlimit, + _limit, + _timelimit, + exitwhenfull, + skipcovered + ); +$$ LANGUAGE SQL; +GRANT USAGE ON SCHEMA pgstac to pgstac_read; +GRANT ALL ON SCHEMA pgstac to pgstac_ingest; +GRANT ALL ON SCHEMA pgstac to pgstac_admin; + +-- pgstac_read role limited to using function apis +GRANT EXECUTE ON FUNCTION search TO pgstac_read; +GRANT EXECUTE ON FUNCTION search_query TO pgstac_read; +GRANT EXECUTE ON FUNCTION item_by_id TO pgstac_read; +GRANT EXECUTE ON FUNCTION get_item TO pgstac_read; + +GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pgstac to pgstac_ingest; +GRANT ALL ON ALL TABLES IN SCHEMA pgstac to pgstac_ingest; +GRANT USAGE ON ALL SEQUENCES IN SCHEMA pgstac to pgstac_ingest; +SELECT set_version('0.6.0'); diff --git a/pypgstac/pypgstac/pypgstac.py b/pypgstac/pypgstac/pypgstac.py index e2838015..10c67ce1 100755 --- a/pypgstac/pypgstac/pypgstac.py +++ b/pypgstac/pypgstac/pypgstac.py @@ -58,14 +58,19 @@ def migrate(self, toversion: Optional[str] = None) -> str: return migrator.run_migration(toversion=toversion) def load( - self, table: Tables, file: str, method: Optional[Methods] = Methods.insert + self, + table: Tables, + file: str, + method: Optional[Methods] = Methods.insert, + dehydrated: Optional[bool] = False, + chunksize: Optional[int] = 10000, ) -> None: """Load collections or items into PGStac.""" loader = Loader(db=self._db) if table == "collections": loader.load_collections(file, method) if table == "items": - loader.load_items(file, method) + loader.load_items(file, method, dehydrated, chunksize) def cli() -> fire.Fire: diff --git a/pypgstac/pypgstac/version.py b/pypgstac/pypgstac/version.py index dd9b22cc..24822e2f 100644 --- a/pypgstac/pypgstac/version.py +++ b/pypgstac/pypgstac/version.py @@ -1 +1,2 @@ -__version__ = "0.5.1" +"""Version.""" +__version__ = "0.6.0" diff --git a/pypgstac/tests/conftest.py b/pypgstac/tests/conftest.py index 69b89404..f04455c0 100644 --- a/pypgstac/tests/conftest.py +++ b/pypgstac/tests/conftest.py @@ -17,8 +17,15 @@ def db() -> Generator: try: conn.execute("CREATE DATABASE pgstactestdb;") except psycopg.errors.DuplicateDatabase: - conn.execute("DROP DATABASE pgstactestdb WITH (FORCE);") - conn.execute("CREATE DATABASE pgstactestdb;") + try: + conn.execute("DROP DATABASE pgstactestdb WITH (FORCE);") + conn.execute("CREATE DATABASE pgstactestdb;") + except psycopg.errors.InsufficientPrivilege: + try: + conn.execute("DROP DATABASE pgstactestdb;") + conn.execute("CREATE DATABASE pgstactestdb;") + except: + pass os.environ["PGDATABASE"] = "pgstactestdb" @@ -31,7 +38,13 @@ def db() -> Generator: os.environ["PGDATABASE"] = origdb with psycopg.connect(autocommit=True) as conn: - conn.execute("DROP DATABASE pgstactestdb WITH (FORCE);") + try: + conn.execute("DROP DATABASE pgstactestdb WITH (FORCE);") + except psycopg.errors.InsufficientPrivilege: + try: + conn.execute("DROP DATABASE pgstactestdb;") + except: + pass @pytest.fixture(scope="function") diff --git a/pypgstac/tests/hydration/__init__.py b/pypgstac/tests/hydration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pypgstac/tests/hydration/test_dehydrate.py b/pypgstac/tests/hydration/test_dehydrate.py index 7a3768d2..44479ac9 100644 --- a/pypgstac/tests/hydration/test_dehydrate.py +++ b/pypgstac/tests/hydration/test_dehydrate.py @@ -22,186 +22,222 @@ ) -def test_landsat_c2_l1(loader: Loader) -> None: - """ - Test that a dehydrated item is created properly from a raw item against a - base item from a collection - """ - with open(LANDSAT_COLLECTION) as f: - collection = json.load(f) - loader.load_collections(str(LANDSAT_COLLECTION)) - - with open(LANDSAT_ITEM) as f: - item = json.load(f) - - base_item = cast( - Dict[str, Any], - loader.db.query_one( - "SELECT base_item FROM collections WHERE id=%s;", (collection["id"],) - ), - ) - - assert type(base_item) == dict - - dehydrated = hydration.dehydrate(base_item, item) - - # Expect certain keys on base and not on dehydrated - only_base_keys = ["type", "collection", "stac_version"] - assert all(k in base_item for k in only_base_keys) - assert not any(k in dehydrated for k in only_base_keys) - - # Expect certain keys on dehydrated and not on base - only_dehydrated_keys = ["id", "bbox", "geometry", "properties"] - assert not any(k in base_item for k in only_dehydrated_keys) - assert all(k in dehydrated for k in only_dehydrated_keys) - - # Properties, links should be exactly the same pre- and post-dehydration - assert item["properties"] == dehydrated["properties"] - assert item["links"] == dehydrated["links"] - - # Check specific assets are dehydrated correctly - thumbnail = dehydrated["assets"]["thumbnail"] - assert list(thumbnail.keys()) == ["href"] - assert thumbnail["href"] == item["assets"]["thumbnail"]["href"] - - # Red asset raster bands have additional `scale` and `offset` keys - red = dehydrated["assets"]["red"] - assert list(red.keys()) == ["href", "eo:bands", "raster:bands"] - assert len(red["raster:bands"]) == 1 - assert list(red["raster:bands"][0].keys()) == ["scale", "offset"] - item_red_rb = item["assets"]["red"]["raster:bands"][0] - assert red["raster:bands"] == [ - {"scale": item_red_rb["scale"], "offset": item_red_rb["offset"]} - ] - - # nir09 asset raster bands does not have a `unit` attribute, which is - # present on base - nir09 = dehydrated["assets"]["nir09"] - assert list(nir09.keys()) == ["href", "eo:bands", "raster:bands"] - assert len(nir09["raster:bands"]) == 1 - assert list(nir09["raster:bands"][0].keys()) == ["unit"] - assert nir09["raster:bands"] == [{"unit": DO_NOT_MERGE_MARKER}] - - -def test_single_depth_equals() -> None: - base_item = {"a": "first", "b": "second", "c": "third"} - item = {"a": "first", "b": "second", "c": "third"} - dehydrated = hydration.dehydrate(base_item, item) - assert dehydrated == {} - - -def test_nested_equals() -> None: - base_item = {"a": "first", "b": "second", "c": {"d": "third"}} - item = {"a": "first", "b": "second", "c": {"d": "third"}} - dehydrated = hydration.dehydrate(base_item, item) - assert dehydrated == {} - - -def test_nested_extra_keys() -> None: - """ - Test that items having nested dicts with keys not in base item preserve - the additional keys in the dehydrated item. - """ - base_item = {"a": "first", "b": "second", "c": {"d": "third"}} - item = { - "a": "first", - "b": "second", - "c": {"d": "third", "e": "fourth", "f": "fifth"}, - } - dehydrated = hydration.dehydrate(base_item, item) - assert dehydrated == {"c": {"e": "fourth", "f": "fifth"}} - - -def test_list_of_dicts_extra_keys() -> None: - """Test that an equal length list of dicts is dehydrated correctly""" - base_item = {"a": [{"b1": 1, "b2": 2}, {"c1": 1, "c2": 2}]} - item = {"a": [{"b1": 1, "b2": 2, "b3": 3}, {"c1": 1, "c2": 2, "c3": 3}]} - - dehydrated = hydration.dehydrate(base_item, item) - assert "a" in dehydrated - assert dehydrated["a"] == [{"b3": 3}, {"c3": 3}] - - -def test_equal_len_list_of_mixed_types() -> None: - """ - Test that a list of equal length containing matched types at each index dehydrates - dicts and preserves item-values of other types. - """ - base_item = {"a": [{"b1": 1, "b2": 2}, "foo", {"c1": 1, "c2": 2}, "bar"]} - item = { - "a": [{"b1": 1, "b2": 2, "b3": 3}, "far", {"c1": 1, "c2": 2, "c3": 3}, "boo"] - } - - dehydrated = hydration.dehydrate(base_item, item) - assert "a" in dehydrated - assert dehydrated["a"] == [{"b3": 3}, "far", {"c3": 3}, "boo"] - - -def test_unequal_len_list() -> None: - """Test that unequal length lists preserve the item value exactly""" - base_item = {"a": [{"b1": 1}, {"c1": 1}, {"d1": 1}]} - item = {"a": [{"b1": 1, "b2": 2}, {"c1": 1, "c2": 2}]} - - dehydrated = hydration.dehydrate(base_item, item) - assert "a" in dehydrated - assert dehydrated["a"] == item["a"] - - -def test_marked_non_merged_fields() -> None: - base_item = {"a": "first", "b": "second", "c": {"d": "third", "e": "fourth"}} - item = { - "a": "first", - "b": "second", - "c": {"d": "third", "f": "fifth"}, - } - dehydrated = hydration.dehydrate(base_item, item) - assert dehydrated == {"c": {"e": DO_NOT_MERGE_MARKER, "f": "fifth"}} - - -def test_marked_non_merged_fields_in_list() -> None: - base_item = {"a": [{"b": "first", "d": "third"}, {"c": "second", "e": "fourth"}]} - item = {"a": [{"b": "first"}, {"c": "second", "f": "fifth"}]} - - dehydrated = hydration.dehydrate(base_item, item) - assert dehydrated == { - "a": [ - {"d": DO_NOT_MERGE_MARKER}, - {"e": DO_NOT_MERGE_MARKER, "f": "fifth"}, +class TestDehydrate: + def dehydrate( + self, base_item: Dict[str, Any], item: Dict[str, Any] + ) -> Dict[str, Any]: + return hydration.dehydrate(base_item, item) + + def test_landsat_c2_l1(self, loader: Loader) -> None: + """ + Test that a dehydrated item is created properly from a raw item against a + base item from a collection + """ + with open(LANDSAT_COLLECTION) as f: + collection = json.load(f) + loader.load_collections(str(LANDSAT_COLLECTION)) + + with open(LANDSAT_ITEM) as f: + item = json.load(f) + + base_item = cast( + Dict[str, Any], + loader.db.query_one( + "SELECT base_item FROM collections WHERE id=%s;", (collection["id"],) + ), + ) + + assert type(base_item) == dict + + dehydrated = self.dehydrate(base_item, item) + + # Expect certain keys on base and not on dehydrated + only_base_keys = ["type", "collection", "stac_version"] + assert all(k in base_item for k in only_base_keys) + assert not any(k in dehydrated for k in only_base_keys) + + # Expect certain keys on dehydrated and not on base + only_dehydrated_keys = ["id", "bbox", "geometry", "properties"] + assert not any(k in base_item for k in only_dehydrated_keys) + assert all(k in dehydrated for k in only_dehydrated_keys) + + # Properties, links should be exactly the same pre- and post-dehydration + assert item["properties"] == dehydrated["properties"] + assert item["links"] == dehydrated["links"] + + # Check specific assets are dehydrated correctly + thumbnail = dehydrated["assets"]["thumbnail"] + assert list(thumbnail.keys()) == ["href"] + assert thumbnail["href"] == item["assets"]["thumbnail"]["href"] + + # Red asset raster bands have additional `scale` and `offset` keys + red = dehydrated["assets"]["red"] + assert list(red.keys()) == ["href", "eo:bands", "raster:bands"] + assert len(red["raster:bands"]) == 1 + assert list(red["raster:bands"][0].keys()) == ["scale", "offset"] + item_red_rb = item["assets"]["red"]["raster:bands"][0] + assert red["raster:bands"] == [ + {"scale": item_red_rb["scale"], "offset": item_red_rb["offset"]} ] - } - -def test_deeply_nested_dict() -> None: - base_item = {"a": {"b": {"c": {"d": "first", "d1": "second"}}}} - item = {"a": {"b": {"c": {"d": "first", "d1": "second", "d2": "third"}}}} - - dehydrated = hydration.dehydrate(base_item, item) - assert dehydrated == {"a": {"b": {"c": {"d2": "third"}}}} - - -def test_equal_list_of_non_dicts() -> None: - """Values of lists that match base_item should be dehydrated off""" - base_item = {"assets": {"thumbnail": {"roles": ["thumbnail"]}}} - item = {"assets": {"thumbnail": {"roles": ["thumbnail"], "href": "http://foo.com"}}} - - dehydrated = hydration.dehydrate(base_item, item) - assert dehydrated == {"assets": {"thumbnail": {"href": "http://foo.com"}}} - - -def test_invalid_assets_marked() -> None: - """ - Assets can be included on item-assets that are not uniformly included on - individual items. Ensure that base item asset keys without a matching item - key are marked do-no-merge after dehydration. - """ - base_item = { - "type": "Feature", - "assets": {"asset1": {"name": "Asset one"}, "asset2": {"name": "Asset two"}}, - } - hydrated = {"assets": {"asset1": {"name": "Asset one", "href": "http://foo.com"}}} - - dehydrated = hydration.dehydrate(base_item, hydrated) - - assert dehydrated == { - "assets": {"asset1": {"href": "http://foo.com"}, "asset2": DO_NOT_MERGE_MARKER}, - } + # nir09 asset raster bands does not have a `unit` attribute, which is + # present on base + nir09 = dehydrated["assets"]["nir09"] + assert list(nir09.keys()) == ["href", "eo:bands", "raster:bands"] + assert len(nir09["raster:bands"]) == 1 + assert list(nir09["raster:bands"][0].keys()) == ["unit"] + assert nir09["raster:bands"] == [{"unit": DO_NOT_MERGE_MARKER}] + + def test_single_depth_equals(self) -> None: + base_item = {"a": "first", "b": "second", "c": "third"} + item = {"a": "first", "b": "second", "c": "third"} + dehydrated = self.dehydrate(base_item, item) + assert dehydrated == {} + + def test_nested_equals(self) -> None: + base_item = {"a": "first", "b": "second", "c": {"d": "third"}} + item = {"a": "first", "b": "second", "c": {"d": "third"}} + dehydrated = self.dehydrate(base_item, item) + assert dehydrated == {} + + def test_nested_extra_keys(self) -> None: + """ + Test that items having nested dicts with keys not in base item preserve + the additional keys in the dehydrated item. + """ + base_item = {"a": "first", "b": "second", "c": {"d": "third"}} + item = { + "a": "first", + "b": "second", + "c": {"d": "third", "e": "fourth", "f": "fifth"}, + } + dehydrated = self.dehydrate(base_item, item) + assert dehydrated == {"c": {"e": "fourth", "f": "fifth"}} + + def test_list_of_dicts_extra_keys(self) -> None: + """Test that an equal length list of dicts is dehydrated correctly""" + base_item = {"a": [{"b1": 1, "b2": 2}, {"c1": 1, "c2": 2}]} + item = {"a": [{"b1": 1, "b2": 2, "b3": 3}, {"c1": 1, "c2": 2, "c3": 3}]} + + dehydrated = self.dehydrate(base_item, item) + assert "a" in dehydrated + assert dehydrated["a"] == [{"b3": 3}, {"c3": 3}] + + def test_equal_len_list_of_mixed_types(self) -> None: + """ + Test that a list of equal length containing matched + types at each index dehydrates + dicts and preserves item-values of other types. + """ + base_item = {"a": [{"b1": 1, "b2": 2}, "foo", {"c1": 1, "c2": 2}, "bar"]} + item = { + "a": [ + {"b1": 1, "b2": 2, "b3": 3}, + "far", + {"c1": 1, "c2": 2, "c3": 3}, + "boo", + ] + } + + dehydrated = self.dehydrate(base_item, item) + assert "a" in dehydrated + assert dehydrated["a"] == [{"b3": 3}, "far", {"c3": 3}, "boo"] + + def test_unequal_len_list(self) -> None: + """Test that unequal length lists preserve the item value exactly""" + base_item = {"a": [{"b1": 1}, {"c1": 1}, {"d1": 1}]} + item = {"a": [{"b1": 1, "b2": 2}, {"c1": 1, "c2": 2}]} + + dehydrated = self.dehydrate(base_item, item) + assert "a" in dehydrated + assert dehydrated["a"] == item["a"] + + def test_marked_non_merged_fields(self) -> None: + base_item = {"a": "first", "b": "second", "c": {"d": "third", "e": "fourth"}} + item = { + "a": "first", + "b": "second", + "c": {"d": "third", "f": "fifth"}, + } + dehydrated = self.dehydrate(base_item, item) + assert dehydrated == {"c": {"e": DO_NOT_MERGE_MARKER, "f": "fifth"}} + + def test_marked_non_merged_fields_in_list(self) -> None: + base_item = { + "a": [{"b": "first", "d": "third"}, {"c": "second", "e": "fourth"}] + } + item = {"a": [{"b": "first"}, {"c": "second", "f": "fifth"}]} + + dehydrated = self.dehydrate(base_item, item) + assert dehydrated == { + "a": [ + {"d": DO_NOT_MERGE_MARKER}, + {"e": DO_NOT_MERGE_MARKER, "f": "fifth"}, + ] + } + + def test_deeply_nested_dict(self) -> None: + base_item = {"a": {"b": {"c": {"d": "first", "d1": "second"}}}} + item = {"a": {"b": {"c": {"d": "first", "d1": "second", "d2": "third"}}}} + + dehydrated = self.dehydrate(base_item, item) + assert dehydrated == {"a": {"b": {"c": {"d2": "third"}}}} + + def test_equal_list_of_non_dicts(self) -> None: + """Values of lists that match base_item should be dehydrated off""" + base_item = {"assets": {"thumbnail": {"roles": ["thumbnail"]}}} + item = { + "assets": {"thumbnail": {"roles": ["thumbnail"], "href": "http://foo.com"}} + } + + dehydrated = self.dehydrate(base_item, item) + assert dehydrated == {"assets": {"thumbnail": {"href": "http://foo.com"}}} + + def test_invalid_assets_marked(self) -> None: + """ + Assets can be included on item-assets that are not uniformly included on + individual items. Ensure that base item asset keys without a matching item + key are marked do-no-merge after dehydration. + """ + base_item = { + "type": "Feature", + "assets": { + "asset1": {"name": "Asset one"}, + "asset2": {"name": "Asset two"}, + }, + } + hydrated = { + "assets": {"asset1": {"name": "Asset one", "href": "http://foo.com"}} + } + + dehydrated = self.dehydrate(base_item, hydrated) + + assert dehydrated == { + "type": DO_NOT_MERGE_MARKER, + "assets": { + "asset1": {"href": "http://foo.com"}, + "asset2": DO_NOT_MERGE_MARKER, + }, + } + + def test_top_level_base_keys_marked(self) -> None: + """ + Top level keys on the base item not present on the incoming item should + be marked as do not merge, no matter the nesting level. + """ + base_item = { + "single": "Feature", + "double": {"nested": "value"}, + "triple": {"nested": {"deep": "value"}}, + "included": "value", + } + hydrated = {"included": "value", "unique": "value"} + + dehydrated = self.dehydrate(base_item, hydrated) + + assert dehydrated == { + "single": DO_NOT_MERGE_MARKER, + "double": DO_NOT_MERGE_MARKER, + "triple": DO_NOT_MERGE_MARKER, + "unique": "value", + } diff --git a/pypgstac/tests/hydration/test_dehydrate_pg.py b/pypgstac/tests/hydration/test_dehydrate_pg.py new file mode 100644 index 00000000..a19ba516 --- /dev/null +++ b/pypgstac/tests/hydration/test_dehydrate_pg.py @@ -0,0 +1,43 @@ +from .test_dehydrate import TestDehydrate as TDehydrate +from typing import Dict, Any +import os +from typing import Generator +from pypgstac.db import PgstacDB +from pypgstac.migrate import Migrate +import psycopg +from contextlib import contextmanager + + +class TestDehydratePG(TDehydrate): + """Class to test Dehydration using pgstac.""" + + @contextmanager + def db(self) -> Generator: + """Set up database connection.""" + print("Setting up db.") + origdb: str = os.getenv("PGDATABASE", "") + with psycopg.connect(autocommit=True) as conn: + try: + conn.execute("CREATE DATABASE pgstactestdb;") + except psycopg.errors.DuplicateDatabase: + pass + + os.environ["PGDATABASE"] = "pgstactestdb" + + pgdb = PgstacDB() + pgdb.query("DROP SCHEMA IF EXISTS pgstac CASCADE;") + migrator = Migrate(pgdb) + print(migrator.run_migration()) + + yield pgdb + + print("Closing Connection to DB") + pgdb.close() + os.environ["PGDATABASE"] = origdb + + def dehydrate( + self, base_item: Dict[str, Any], item: Dict[str, Any] + ) -> Dict[str, Any]: + """Dehydrate item using pgstac.""" + with self.db() as db: + return next(db.func("strip_jsonb", item, base_item))[0] diff --git a/pypgstac/tests/hydration/test_hydrate.py b/pypgstac/tests/hydration/test_hydrate.py index be4c11c5..01fc2350 100644 --- a/pypgstac/tests/hydration/test_hydrate.py +++ b/pypgstac/tests/hydration/test_hydrate.py @@ -1,3 +1,4 @@ +"""Test Hydration.""" import json from pathlib import Path from typing import Any, Dict, cast @@ -32,160 +33,199 @@ ) -def test_landsat_c2_l1(loader: Loader) -> None: - """Test that a dehydrated item is is equal to the raw item it was dehydrated - from, against the base item of the collection""" - with open(LANDSAT_COLLECTION) as f: - collection = json.load(f) - loader.load_collections(str(LANDSAT_COLLECTION)) - - with open(LANDSAT_DEHYDRATED_ITEM) as f: - dehydrated = json.load(f) - - with open(LANDSAT_ITEM) as f: - raw_item = json.load(f) - - base_item = cast( - Dict[str, Any], - loader.db.query_one( - "SELECT base_item FROM collections WHERE id=%s;", (collection["id"],) - ), - ) - - assert type(base_item) == dict - - hydrated = hydration.hydrate(base_item, dehydrated) - assert hydrated == raw_item - - -def test_full_hydrate() -> None: - base_item = {"a": "first", "b": "second", "c": "third"} - dehydrated: Dict[str, Any] = {} - - rehydrated = hydration.hydrate(base_item, dehydrated) - assert rehydrated == base_item - - -def test_full_nested() -> None: - base_item = {"a": "first", "b": "second", "c": {"d": "third"}} - dehydrated: Dict[str, Any] = {} - - rehydrated = hydration.hydrate(base_item, dehydrated) - assert rehydrated == base_item - - -def test_nested_extra_keys() -> None: - """ - Test that items having nested dicts with keys not in base item preserve - the additional keys in the dehydrated item. - """ - base_item = {"a": "first", "b": "second", "c": {"d": "third"}} - dehydrated = {"c": {"e": "fourth", "f": "fifth"}} - hydrated = hydration.hydrate(base_item, dehydrated) - - assert hydrated == { - "a": "first", - "b": "second", - "c": {"d": "third", "e": "fourth", "f": "fifth"}, - } - - -def test_list_of_dicts_extra_keys() -> None: - """Test that an equal length list of dicts is hydrated correctly""" - base_item = {"a": [{"b1": 1, "b2": 2}, {"c1": 1, "c2": 2}]} - dehydrated = {"a": [{"b3": 3}, {"c3": 3}]} - - hydrated = hydration.hydrate(base_item, dehydrated) - assert hydrated == {"a": [{"b1": 1, "b2": 2, "b3": 3}, {"c1": 1, "c2": 2, "c3": 3}]} - - -def test_equal_len_list_of_mixed_types() -> None: - """ - Test that a list of equal length containing matched types at each index dehydrates - dicts and preserves item-values of other types. - """ - base_item = {"a": [{"b1": 1, "b2": 2}, "foo", {"c1": 1, "c2": 2}, "bar"]} - dehydrated = {"a": [{"b3": 3}, "far", {"c3": 3}, "boo"]} - - hydrated = hydration.hydrate(base_item, dehydrated) - assert hydrated == { - "a": [{"b1": 1, "b2": 2, "b3": 3}, "far", {"c1": 1, "c2": 2, "c3": 3}, "boo"] - } - - -def test_unequal_len_list() -> None: - """Test that unequal length lists preserve the item value exactly""" - base_item = {"a": [{"b1": 1}, {"c1": 1}, {"d1": 1}]} - dehydrated = {"a": [{"b1": 1, "b2": 2}, {"c1": 1, "c2": 2}]} - - hydrated = hydration.hydrate(base_item, dehydrated) - assert hydrated == dehydrated - - -def test_marked_non_merged_fields() -> None: - base_item = {"a": "first", "b": "second", "c": {"d": "third", "e": "fourth"}} - dehydrated = {"c": {"e": DO_NOT_MERGE_MARKER, "f": "fifth"}} - - hydrated = hydration.hydrate(base_item, dehydrated) - assert hydrated == { - "a": "first", - "b": "second", - "c": {"d": "third", "f": "fifth"}, - } - - -def test_marked_non_merged_fields_in_list() -> None: - base_item = {"a": [{"b": "first", "d": "third"}, {"c": "second", "e": "fourth"}]} - dehydrated = { - "a": [ - {"d": DO_NOT_MERGE_MARKER}, - {"e": DO_NOT_MERGE_MARKER, "f": "fifth"}, - ] - } - - hydrated = hydration.hydrate(base_item, dehydrated) - assert hydrated == {"a": [{"b": "first"}, {"c": "second", "f": "fifth"}]} - - -def test_deeply_nested_dict() -> None: - base_item = {"a": {"b": {"c": {"d": "first", "d1": "second"}}}} - dehydrated = {"a": {"b": {"c": {"d2": "third"}}}} - - hydrated = hydration.hydrate(base_item, dehydrated) - assert hydrated == { - "a": {"b": {"c": {"d": "first", "d1": "second", "d2": "third"}}} - } - - -def test_equal_list_of_non_dicts() -> None: - """Values of lists that match base_item should be hydrated back on""" - base_item = {"assets": {"thumbnail": {"roles": ["thumbnail"]}}} - dehydrated = {"assets": {"thumbnail": {"href": "http://foo.com"}}} - - hydrated = hydration.hydrate(base_item, dehydrated) - assert hydrated == { - "assets": {"thumbnail": {"roles": ["thumbnail"], "href": "http://foo.com"}} - } - - -def test_invalid_assets_removed() -> None: - """ - Assets can be included on item-assets that are not uniformly included on - individual items. Ensure that item asset keys from base_item aren't included - after hydration - """ - base_item = { - "type": "Feature", - "assets": {"asset1": {"name": "Asset one"}, "asset2": {"name": "Asset two"}}, - } - - dehydrated = { - "assets": {"asset1": {"href": "http://foo.com"}, "asset2": DO_NOT_MERGE_MARKER}, - } - - hydrated = hydration.hydrate(base_item, dehydrated) - - assert hydrated == { - "type": "Feature", - "assets": {"asset1": {"name": "Asset one", "href": "http://foo.com"}}, - } +class TestHydrate: + def hydrate( + self, base_item: Dict[str, Any], item: Dict[str, Any] + ) -> Dict[str, Any]: + return hydration.hydrate(base_item, item) + + def test_landsat_c2_l1(self, loader: Loader) -> None: + """Test that a dehydrated item is is equal to the raw item it was dehydrated + from, against the base item of the collection""" + with open(LANDSAT_COLLECTION) as f: + collection = json.load(f) + loader.load_collections(str(LANDSAT_COLLECTION)) + + with open(LANDSAT_DEHYDRATED_ITEM) as f: + dehydrated = json.load(f) + + with open(LANDSAT_ITEM) as f: + raw_item = json.load(f) + + base_item = cast( + Dict[str, Any], + loader.db.query_one( + "SELECT base_item FROM collections WHERE id=%s;", + (collection["id"],), + ), + ) + + assert type(base_item) == dict + + hydrated = self.hydrate(base_item, dehydrated) + assert hydrated == raw_item + + def test_full_hydrate(self) -> None: + base_item = {"a": "first", "b": "second", "c": "third"} + dehydrated: Dict[str, Any] = {} + + rehydrated = self.hydrate(base_item, dehydrated) + assert rehydrated == base_item + + def test_full_nested(self) -> None: + base_item = {"a": "first", "b": "second", "c": {"d": "third"}} + dehydrated: Dict[str, Any] = {} + + rehydrated = self.hydrate(base_item, dehydrated) + assert rehydrated == base_item + + def test_nested_extra_keys(self) -> None: + """ + Test that items having nested dicts with keys not in base item preserve + the additional keys in the dehydrated item. + """ + base_item = {"a": "first", "b": "second", "c": {"d": "third"}} + dehydrated = {"c": {"e": "fourth", "f": "fifth"}} + hydrated = self.hydrate(base_item, dehydrated) + + assert hydrated == { + "a": "first", + "b": "second", + "c": {"d": "third", "e": "fourth", "f": "fifth"}, + } + + def test_list_of_dicts_extra_keys(self) -> None: + """Test that an equal length list of dicts is hydrated correctly""" + base_item = {"a": [{"b1": 1, "b2": 2}, {"c1": 1, "c2": 2}]} + dehydrated = {"a": [{"b3": 3}, {"c3": 3}]} + + hydrated = self.hydrate(base_item, dehydrated) + assert hydrated == { + "a": [{"b1": 1, "b2": 2, "b3": 3}, {"c1": 1, "c2": 2, "c3": 3}] + } + + def test_equal_len_list_of_mixed_types(self) -> None: + """ + Test that a list of equal length containing matched types at + each index dehydrates + dicts and preserves item-values of other types. + """ + base_item = {"a": [{"b1": 1, "b2": 2}, "foo", {"c1": 1, "c2": 2}, "bar"]} + dehydrated = {"a": [{"b3": 3}, "far", {"c3": 3}, "boo"]} + + hydrated = self.hydrate(base_item, dehydrated) + assert hydrated == { + "a": [ + {"b1": 1, "b2": 2, "b3": 3}, + "far", + {"c1": 1, "c2": 2, "c3": 3}, + "boo", + ] + } + + def test_unequal_len_list(self) -> None: + """Test that unequal length lists preserve the item value exactly""" + base_item = {"a": [{"b1": 1}, {"c1": 1}, {"d1": 1}]} + dehydrated = {"a": [{"b1": 1, "b2": 2}, {"c1": 1, "c2": 2}]} + + hydrated = self.hydrate(base_item, dehydrated) + assert hydrated == dehydrated + + def test_marked_non_merged_fields(self) -> None: + base_item = { + "a": "first", + "b": "second", + "c": {"d": "third", "e": "fourth"}, + } + dehydrated = {"c": {"e": DO_NOT_MERGE_MARKER, "f": "fifth"}} + + hydrated = self.hydrate(base_item, dehydrated) + assert hydrated == { + "a": "first", + "b": "second", + "c": {"d": "third", "f": "fifth"}, + } + + def test_marked_non_merged_fields_in_list(self) -> None: + base_item = { + "a": [{"b": "first", "d": "third"}, {"c": "second", "e": "fourth"}] + } + dehydrated = { + "a": [ + {"d": DO_NOT_MERGE_MARKER}, + {"e": DO_NOT_MERGE_MARKER, "f": "fifth"}, + ] + } + + hydrated = self.hydrate(base_item, dehydrated) + assert hydrated == {"a": [{"b": "first"}, {"c": "second", "f": "fifth"}]} + + def test_deeply_nested_dict(self) -> None: + base_item = {"a": {"b": {"c": {"d": "first", "d1": "second"}}}} + dehydrated = {"a": {"b": {"c": {"d2": "third"}}}} + + hydrated = self.hydrate(base_item, dehydrated) + assert hydrated == { + "a": {"b": {"c": {"d": "first", "d1": "second", "d2": "third"}}} + } + + def test_equal_list_of_non_dicts(self) -> None: + """Values of lists that match base_item should be hydrated back on""" + base_item = {"assets": {"thumbnail": {"roles": ["thumbnail"]}}} + dehydrated = {"assets": {"thumbnail": {"href": "http://foo.com"}}} + + hydrated = self.hydrate(base_item, dehydrated) + assert hydrated == { + "assets": {"thumbnail": {"roles": ["thumbnail"], "href": "http://foo.com"}} + } + + def test_invalid_assets_removed(self) -> None: + """ + Assets can be included on item-assets that are not uniformly included on + individual items. Ensure that item asset keys from base_item aren't included + after hydration + """ + base_item = { + "type": "Feature", + "assets": { + "asset1": {"name": "Asset one"}, + "asset2": {"name": "Asset two"}, + }, + } + + dehydrated = { + "assets": { + "asset1": {"href": "http://foo.com"}, + "asset2": DO_NOT_MERGE_MARKER, + }, + } + + hydrated = self.hydrate(base_item, dehydrated) + + assert hydrated == { + "type": "Feature", + "assets": {"asset1": {"name": "Asset one", "href": "http://foo.com"}}, + } + + def test_top_level_base_keys_marked(self) -> None: + """ + Top level keys on the base item not present on the incoming item should + be marked as do not merge, no matter the nesting level. + """ + base_item = { + "single": "Feature", + "double": {"nested": "value"}, + "triple": {"nested": {"deep": "value"}}, + "included": "value", + } + + dehydrated = { + "single": DO_NOT_MERGE_MARKER, + "double": DO_NOT_MERGE_MARKER, + "triple": DO_NOT_MERGE_MARKER, + "unique": "value", + } + + hydrated = self.hydrate(base_item, dehydrated) + + assert hydrated == {"included": "value", "unique": "value"} diff --git a/pypgstac/tests/hydration/test_hydrate_pg.py b/pypgstac/tests/hydration/test_hydrate_pg.py new file mode 100644 index 00000000..a4cf3be8 --- /dev/null +++ b/pypgstac/tests/hydration/test_hydrate_pg.py @@ -0,0 +1,44 @@ +"""Test Hydration in PGStac.""" +from .test_hydrate import TestHydrate as THydrate +from typing import Dict, Any +import os +from typing import Generator +from pypgstac.db import PgstacDB +from pypgstac.migrate import Migrate +import psycopg +from contextlib import contextmanager + + +class TestHydratePG(THydrate): + """Test hydration using PGStac.""" + + @contextmanager + def db(self) -> Generator: + """Set up database.""" + print("Setting up db.") + origdb: str = os.getenv("PGDATABASE", "") + with psycopg.connect(autocommit=True) as conn: + try: + conn.execute("CREATE DATABASE pgstactestdb;") + except psycopg.errors.DuplicateDatabase: + pass + + os.environ["PGDATABASE"] = "pgstactestdb" + + pgdb = PgstacDB() + pgdb.query("DROP SCHEMA IF EXISTS pgstac CASCADE;") + migrator = Migrate(pgdb) + print(migrator.run_migration()) + + yield pgdb + + print("Closing Connection to DB") + pgdb.close() + os.environ["PGDATABASE"] = origdb + + def hydrate( + self, base_item: Dict[str, Any], item: Dict[str, Any] + ) -> Dict[str, Any]: + """Hydrate using pgstac.""" + with self.db() as db: + return next(db.func("merge_jsonb", item, base_item))[0] diff --git a/pypgstac/tests/test_load.py b/pypgstac/tests/test_load.py index f6683389..29692321 100644 --- a/pypgstac/tests/test_load.py +++ b/pypgstac/tests/test_load.py @@ -9,6 +9,7 @@ TEST_COLLECTIONS_JSON = TEST_DATA_DIR / "collections.json" TEST_COLLECTIONS = TEST_DATA_DIR / "collections.ndjson" TEST_ITEMS = TEST_DATA_DIR / "items.ndjson" +TEST_DEHYDRATED_ITEMS = TEST_DATA_DIR / "items.pgcopy" def test_load_collections_succeeds(loader: Loader) -> None: @@ -222,3 +223,19 @@ def test_partition_loads_year(loader: Loader) -> None: ) assert partitions == 1 + + +def test_load_items_dehydrated_ignore_succeeds(loader: Loader) -> None: + """Test pypgstac items ignore loader.""" + loader.load_collections( + str(TEST_COLLECTIONS), + insert_mode=Methods.ignore, + ) + + loader.load_items( + str(TEST_DEHYDRATED_ITEMS), insert_mode=Methods.insert, dehydrated=True + ) + + loader.load_items( + str(TEST_DEHYDRATED_ITEMS), insert_mode=Methods.ignore, dehydrated=True + ) diff --git a/scripts/bin/testdb b/scripts/bin/testdb index 135aef0d..cf459ecf 100755 --- a/scripts/bin/testdb +++ b/scripts/bin/testdb @@ -45,6 +45,14 @@ function bail() { source $(dirname $0)/migra_funcs if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + echo "Running Basic SQL tests." + pushd $(dirname $0)/../../test/basic + for f in *.sql; do + echo "Running tests $f" + ./sqltest.sh $f + done + popd + echo "Creating Temp DBs to check base and incremental migrations." create_migra_dbs trap drop_migra_dbs 0 2 3 15 @@ -54,12 +62,24 @@ if [ "${BASH_SOURCE[0]}" = "${0}" ]; then echo "Running PGTap Tests on Temp DB 2" pgtap $TODBURL + + + if [[ ! "${SKIPINCREMENTAL}" ]]; then echo "-----Testing incremental migrations.-----" echo "Setting Temp DB 1 to initial version 0.1.9" pypgstac migrate --dsn $FROMDBURL --toversion 0.1.9 echo "Using incremental migrations to bring Temp DB 1 up to date" pypgstac migrate --dsn $FROMDBURL + + echo "Running Basic SQL tests." + pushd $(dirname $0)/../../test/basic + for f in *.sql; do + echo "Running tests $f" + ./sqltest.sh $f + done + popd + echo "Running PGTap Tests on Temp DB 1" pgtap $FROMDBURL diff --git a/sql/001a_jsonutils.sql b/sql/001a_jsonutils.sql index a9214b44..403cf348 100644 --- a/sql/001a_jsonutils.sql +++ b/sql/001a_jsonutils.sql @@ -63,3 +63,163 @@ $$ LANGUAGE SQL IMMUTABLE STRICT; CREATE OR REPLACE FUNCTION flip_jsonb_array(j jsonb) RETURNS jsonb AS $$ SELECT jsonb_agg(value) FROM (SELECT value FROM jsonb_array_elements(j) WITH ORDINALITY ORDER BY ordinality DESC) as t; $$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; + + +CREATE OR REPLACE FUNCTION explode_dotpaths(j jsonb) RETURNS SETOF text[] AS $$ + SELECT string_to_array(p, '.') as e FROM jsonb_array_elements_text(j) p; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; + +CREATE OR REPLACE FUNCTION explode_dotpaths_recurse(IN j jsonb) RETURNS SETOF text[] AS $$ + WITH RECURSIVE t AS ( + SELECT e FROM explode_dotpaths(j) e + UNION ALL + SELECT e[1:cardinality(e)-1] + FROM t + WHERE cardinality(e)>1 + ) SELECT e FROM t; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; + + +CREATE OR REPLACE FUNCTION jsonb_set_nested(j jsonb, path text[], val jsonb) RETURNS jsonb AS $$ +DECLARE +BEGIN + IF cardinality(path) > 1 THEN + FOR i IN 1..(cardinality(path)-1) LOOP + IF j #> path[:i] IS NULL THEN + j := jsonb_set_lax(j, path[:i], '{}', TRUE); + END IF; + END LOOP; + END IF; + RETURN jsonb_set_lax(j, path, val, true); + +END; +$$ LANGUAGE PLPGSQL IMMUTABLE; + + + +CREATE OR REPLACE FUNCTION jsonb_include(j jsonb, f jsonb) RETURNS jsonb AS $$ +DECLARE + includes jsonb := f-> 'include'; + outj jsonb := '{}'::jsonb; + path text[]; +BEGIN + IF + includes IS NULL + OR jsonb_array_length(includes) = 0 + THEN + RETURN j; + ELSE + includes := includes || '["id","collection"]'::jsonb; + FOR path IN SELECT explode_dotpaths(includes) LOOP + outj := jsonb_set_nested(outj, path, j #> path); + END LOOP; + END IF; + RETURN outj; +END; +$$ LANGUAGE PLPGSQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION jsonb_exclude(j jsonb, f jsonb) RETURNS jsonb AS $$ +DECLARE + excludes jsonb := f-> 'exclude'; + outj jsonb := j; + path text[]; +BEGIN + IF + excludes IS NULL + OR jsonb_array_length(excludes) = 0 + THEN + RETURN j; + ELSE + FOR path IN SELECT explode_dotpaths(excludes) LOOP + outj := outj #- path; + END LOOP; + END IF; + RETURN outj; +END; +$$ LANGUAGE PLPGSQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION jsonb_fields(j jsonb, f jsonb DEFAULT '{"fields":[]}') RETURNS jsonb AS $$ + SELECT jsonb_exclude(jsonb_include(j, f), f); +$$ LANGUAGE SQL IMMUTABLE; + + +CREATE OR REPLACE FUNCTION merge_jsonb(_a jsonb, _b jsonb) RETURNS jsonb AS $$ + SELECT + CASE + WHEN _a = '"𒍟※"'::jsonb THEN NULL + WHEN _a IS NULL OR jsonb_typeof(_a) = 'null' THEN _b + WHEN jsonb_typeof(_a) = 'object' AND jsonb_typeof(_b) = 'object' THEN + ( + SELECT + jsonb_strip_nulls( + jsonb_object_agg( + key, + merge_jsonb(a.value, b.value) + ) + ) + FROM + jsonb_each(coalesce(_a,'{}'::jsonb)) as a + FULL JOIN + jsonb_each(coalesce(_b,'{}'::jsonb)) as b + USING (key) + ) + WHEN + jsonb_typeof(_a) = 'array' + AND jsonb_typeof(_b) = 'array' + AND jsonb_array_length(_a) = jsonb_array_length(_b) + THEN + ( + SELECT jsonb_agg(m) FROM + ( SELECT + merge_jsonb( + jsonb_array_elements(_a), + jsonb_array_elements(_b) + ) as m + ) as l + ) + ELSE _a + END + ; +$$ LANGUAGE SQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION strip_jsonb(_a jsonb, _b jsonb) RETURNS jsonb AS $$ + SELECT + CASE + + WHEN (_a IS NULL OR jsonb_typeof(_a) = 'null') AND _b IS NOT NULL AND jsonb_typeof(_b) != 'null' THEN '"𒍟※"'::jsonb + WHEN _b IS NULL OR jsonb_typeof(_a) = 'null' THEN _a + WHEN _a = _b AND jsonb_typeof(_a) = 'object' THEN '{}'::jsonb + WHEN _a = _b THEN NULL + WHEN jsonb_typeof(_a) = 'object' AND jsonb_typeof(_b) = 'object' THEN + ( + SELECT + jsonb_strip_nulls( + jsonb_object_agg( + key, + strip_jsonb(a.value, b.value) + ) + ) + FROM + jsonb_each(_a) as a + FULL JOIN + jsonb_each(_b) as b + USING (key) + ) + WHEN + jsonb_typeof(_a) = 'array' + AND jsonb_typeof(_b) = 'array' + AND jsonb_array_length(_a) = jsonb_array_length(_b) + THEN + ( + SELECT jsonb_agg(m) FROM + ( SELECT + strip_jsonb( + jsonb_array_elements(_a), + jsonb_array_elements(_b) + ) as m + ) as l + ) + ELSE _a + END + ; +$$ LANGUAGE SQL IMMUTABLE; diff --git a/sql/001s_stacutils.sql b/sql/001s_stacutils.sql index 9a4aab77..e39dcd37 100644 --- a/sql/001s_stacutils.sql +++ b/sql/001s_stacutils.sql @@ -26,18 +26,24 @@ BEGIN IF props ? 'properties' THEN props := props->'properties'; END IF; - IF props ? 'start_datetime' AND props ? 'end_datetime' THEN - dt := props->'start_datetime'; - edt := props->'end_datetime'; + IF + props ? 'start_datetime' + AND props->>'start_datetime' IS NOT NULL + AND props ? 'end_datetime' + AND props->>'end_datetime' IS NOT NULL + THEN + dt := props->>'start_datetime'; + edt := props->>'end_datetime'; IF dt > edt THEN RAISE EXCEPTION 'start_datetime must be < end_datetime'; END IF; ELSE - dt := props->'datetime'; - edt := props->'datetime'; + dt := props->>'datetime'; + edt := props->>'datetime'; END IF; IF dt is NULL OR edt IS NULL THEN - RAISE EXCEPTION 'Either datetime or both start_datetime and end_datetime must be set.'; + RAISE NOTICE 'DT: %, EDT: %', dt, edt; + RAISE EXCEPTION 'Either datetime (%) or both start_datetime (%) and end_datetime (%) must be set.', props->>'datetime',props->>'start_datetime',props->>'end_datetime'; END IF; RETURN tstzrange(dt, edt, '[]'); END; diff --git a/sql/002_collections.sql b/sql/002_collections.sql index 748a96b3..cebd42d9 100644 --- a/sql/002_collections.sql +++ b/sql/002_collections.sql @@ -6,8 +6,7 @@ CREATE OR REPLACE FUNCTION collection_base_item(content jsonb) RETURNS jsonb AS 'type', 'Feature', 'stac_version', content->'stac_version', 'assets', content->'item_assets', - 'collection', content->'id', - 'links', '[]'::jsonb + 'collection', content->'id' ); $$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; @@ -115,7 +114,11 @@ BEGIN RAISE INFO 'Error State:%', SQLSTATE; RAISE INFO 'Error Context:%', err_context; END; + + ALTER TABLE partitions DISABLE TRIGGER partitions_delete_trigger; DELETE FROM partitions WHERE collection=NEW.id AND name=partition_name; + ALTER TABLE partitions ENABLE TRIGGER partitions_delete_trigger; + INSERT INTO partitions (collection, name) VALUES (NEW.id, partition_name); ELSIF partition_empty THEN q := format($q$ @@ -137,7 +140,9 @@ BEGIN RAISE INFO 'Error State:%', SQLSTATE; RAISE INFO 'Error Context:%', err_context; END; + ALTER TABLE partitions DISABLE TRIGGER partitions_delete_trigger; DELETE FROM partitions WHERE collection=NEW.id AND name=partition_name; + ALTER TABLE partitions ENABLE TRIGGER partitions_delete_trigger; ELSE RAISE EXCEPTION 'Cannot modify partition % unless empty', partition_name; END IF; @@ -185,7 +190,7 @@ CREATE OR REPLACE FUNCTION partition_collection(collection text, strategy partit $$ LANGUAGE SQL; CREATE TABLE IF NOT EXISTS partitions ( - collection text REFERENCES collections(id), + collection text REFERENCES collections(id) ON DELETE CASCADE, name text PRIMARY KEY, partition_range tstzrange NOT NULL DEFAULT tstzrange('-infinity'::timestamptz,'infinity'::timestamptz, '[]'), datetime_range tstzrange, @@ -197,7 +202,23 @@ CREATE TABLE IF NOT EXISTS partitions ( ) WITH (FILLFACTOR=90); CREATE INDEX partitions_range_idx ON partitions USING GIST(partition_range); +CREATE OR REPLACE FUNCTION partitions_delete_trigger_func() RETURNS TRIGGER AS $$ +DECLARE + q text; +BEGIN + RAISE NOTICE 'Partition Delete Trigger. %', OLD.name; + EXECUTE format($q$ + DROP TABLE IF EXISTS %I CASCADE; + $q$, + OLD.name + ); + RAISE NOTICE 'Dropped partition.'; + RETURN OLD; +END; +$$ LANGUAGE PLPGSQL; +CREATE TRIGGER partitions_delete_trigger BEFORE DELETE ON partitions FOR EACH ROW +EXECUTE FUNCTION partitions_delete_trigger_func(); CREATE OR REPLACE FUNCTION partition_name( IN collection text, @@ -211,7 +232,7 @@ DECLARE BEGIN SELECT * INTO c FROM pgstac.collections WHERE id=collection; IF NOT FOUND THEN - RAISE EXCEPTION 'Collection % does not exist', collection; + RAISE EXCEPTION 'Collection % does not exist', collection USING ERRCODE = 'foreign_key_violation', HINT = 'Make sure collection exists before adding items'; END IF; parent_name := format('_items_%s', c.key); diff --git a/sql/003_items.sql b/sql/003_items.sql index 83bc6b23..c9c7dd1a 100644 --- a/sql/003_items.sql +++ b/sql/003_items.sql @@ -18,31 +18,8 @@ CREATE STATISTICS datetime_stats (dependencies) on datetime, end_datetime from i ALTER TABLE items ADD CONSTRAINT items_collections_fk FOREIGN KEY (collection) REFERENCES collections(id) ON DELETE CASCADE DEFERRABLE; -CREATE OR REPLACE FUNCTION content_slim(_item jsonb, _collection jsonb) RETURNS jsonb AS $$ - SELECT - jsonb_object_agg( - key, - CASE - WHEN - jsonb_typeof(c.value) = 'object' - AND - jsonb_typeof(i.value) = 'object' - THEN content_slim(i.value, c.value) - ELSE i.value - END - ) - FROM - jsonb_each(_item) as i - LEFT JOIN - jsonb_each(_collection) as c - USING (key) - WHERE - i.value IS DISTINCT FROM c.value - ; -$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; - CREATE OR REPLACE FUNCTION content_slim(_item jsonb) RETURNS jsonb AS $$ - SELECT content_slim(_item - '{id,type,collection,geometry,bbox}'::text[], collection_base_item(_item->>'collection')); + SELECT strip_jsonb(_item - '{id,geometry,collection,type}'::text[], collection_base_item(_item->>'collection')) - '{id,geometry,collection,type}'::text[]; $$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION content_dehydrate(content jsonb) RETURNS items AS $$ @@ -56,101 +33,53 @@ CREATE OR REPLACE FUNCTION content_dehydrate(content jsonb) RETURNS items AS $$ ; $$ LANGUAGE SQL STABLE; - CREATE OR REPLACE FUNCTION include_field(f text, fields jsonb DEFAULT '{}'::jsonb) RETURNS boolean AS $$ DECLARE - includes jsonb := coalesce(fields->'includes', fields->'include', '[]'::jsonb); - excludes jsonb := coalesce(fields->'excludes', fields->'exclude', '[]'::jsonb); + includes jsonb := fields->'include'; + excludes jsonb := fields->'exclude'; BEGIN IF f IS NULL THEN RETURN NULL; - ELSIF jsonb_array_length(includes)>0 AND includes ? f THEN - RETURN TRUE; - ELSIF jsonb_array_length(excludes)>0 AND excludes ? f THEN - RETURN FALSE; - ELSIF jsonb_array_length(includes)>0 AND NOT includes ? f THEN - RETURN FALSE; END IF; - RETURN TRUE; -END; -$$ LANGUAGE PLPGSQL IMMUTABLE; -CREATE OR REPLACE FUNCTION key_filter(IN k text, IN val jsonb, INOUT kf jsonb, OUT include boolean) AS $$ -DECLARE - includes jsonb := coalesce(kf->'includes', kf->'include', '[]'::jsonb); - excludes jsonb := coalesce(kf->'excludes', kf->'exclude', '[]'::jsonb); -BEGIN - RAISE NOTICE '% % %', k, val, kf; - - include := TRUE; - IF k = 'properties' AND NOT excludes ? 'properties' THEN - excludes := excludes || '["properties"]'; - include := TRUE; - RAISE NOTICE 'Prop include %', include; - ELSIF - jsonb_array_length(excludes)>0 AND excludes ? k THEN - include := FALSE; - ELSIF - jsonb_array_length(includes)>0 AND NOT includes ? k THEN - include := FALSE; - ELSIF - jsonb_array_length(includes)>0 AND includes ? k THEN - includes := '[]'::jsonb; - RAISE NOTICE 'KF: %', kf; + IF + jsonb_typeof(excludes) = 'array' + AND jsonb_array_length(excludes)>0 + AND excludes ? f + THEN + RETURN FALSE; END IF; - kf := jsonb_build_object('includes', includes, 'excludes', excludes); - RETURN; -END; -$$ LANGUAGE PLPGSQL IMMUTABLE PARALLEL SAFE; + IF + ( + jsonb_typeof(includes) = 'array' + AND jsonb_array_length(includes) > 0 + AND includes ? f + ) OR + ( + includes IS NULL + OR jsonb_typeof(includes) = 'null' + OR jsonb_array_length(includes) = 0 + ) + THEN + RETURN TRUE; + END IF; -CREATE OR REPLACE FUNCTION strip_assets(a jsonb) RETURNS jsonb AS $$ - WITH t AS (SELECT * FROM jsonb_each(a)) - SELECT jsonb_object_agg(key, value) FROM t - WHERE value ? 'href'; -$$ LANGUAGE SQL IMMUTABLE STRICT; + RETURN FALSE; +END; +$$ LANGUAGE PLPGSQL IMMUTABLE; +DROP FUNCTION IF EXISTS content_hydrate(jsonb, jsonb, jsonb); CREATE OR REPLACE FUNCTION content_hydrate( + _base_item jsonb, _item jsonb, - _collection jsonb, fields jsonb DEFAULT '{}'::jsonb ) RETURNS jsonb AS $$ - SELECT - jsonb_strip_nulls(jsonb_object_agg( - key, - CASE - WHEN key = 'properties' AND include_field('properties', fields) THEN - i.value - WHEN key = 'properties' THEN - content_hydrate(i.value, c.value, kf) - WHEN - c.value IS NULL AND key != 'properties' - THEN i.value - WHEN - key = 'assets' - AND - jsonb_typeof(c.value) = 'object' - AND - jsonb_typeof(i.value) = 'object' - THEN strip_assets(content_hydrate(i.value, c.value, kf)) - WHEN - jsonb_typeof(c.value) = 'object' - AND - jsonb_typeof(i.value) = 'object' - THEN content_hydrate(i.value, c.value, kf) - ELSE coalesce(i.value, c.value) - END - )) - FROM - jsonb_each(coalesce(_item,'{}'::jsonb)) as i - FULL JOIN - jsonb_each(coalesce(_collection,'{}'::jsonb)) as c - USING (key) - JOIN LATERAL ( - SELECT kf, include FROM key_filter(key, i.value, fields) - ) as k ON (include) - ; + SELECT merge_jsonb( + jsonb_fields(_item, fields), + jsonb_fields(_base_item, fields) + ); $$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; @@ -164,21 +93,18 @@ DECLARE base_item jsonb := _collection.base_item; BEGIN IF include_field('geometry', fields) THEN - geom := ST_ASGeoJson(_item.geometry)::jsonb; - END IF; - IF include_field('bbox', fields) THEN - bbox := geom_bbox(_item.geometry)::jsonb; + geom := ST_ASGeoJson(_item.geometry, 20)::jsonb; END IF; output := content_hydrate( - jsonb_build_object( - 'id', _item.id, - 'geometry', geom, - 'bbox',bbox, - 'collection', _item.collection - ) || _item.content, - _collection.base_item, - fields - ); + jsonb_build_object( + 'id', _item.id, + 'geometry', geom, + 'collection', _item.collection, + 'type', 'Feature' + ) || _item.content, + _collection.base_item, + fields + ); RETURN output; END; @@ -194,16 +120,13 @@ DECLARE output jsonb; BEGIN IF include_field('geometry', fields) THEN - geom := ST_ASGeoJson(_item.geometry)::jsonb; - END IF; - IF include_field('bbox', fields) THEN - bbox := geom_bbox(_item.geometry)::jsonb; + geom := ST_ASGeoJson(_item.geometry, 20)::jsonb; END IF; output := jsonb_build_object( 'id', _item.id, 'geometry', geom, - 'bbox',bbox, - 'collection', _item.collection + 'collection', _item.collection, + 'type', 'Feature' ) || _item.content; RETURN output; END; @@ -335,7 +258,7 @@ out items%ROWTYPE; BEGIN DELETE FROM items WHERE id = _id AND (_collection IS NULL OR collection=_collection) RETURNING * INTO STRICT out; END; -$$ LANGUAGE PLPGSQL STABLE; +$$ LANGUAGE PLPGSQL; --/* CREATE OR REPLACE FUNCTION create_item(data jsonb) RETURNS VOID AS $$ @@ -348,8 +271,8 @@ DECLARE old items %ROWTYPE; out items%ROWTYPE; BEGIN - SELECT delete_item(content->>'id', content->>'collection'); - SELECT create_item(content); + PERFORM delete_item(content->>'id', content->>'collection'); + PERFORM create_item(content); END; $$ LANGUAGE PLPGSQL SET SEARCH_PATH TO pgstac,public; diff --git a/sql/004_search.sql b/sql/004_search.sql index f2d0b418..5f9fb746 100644 --- a/sql/004_search.sql +++ b/sql/004_search.sql @@ -586,6 +586,8 @@ DECLARE cntr int := 0; iter_record items%ROWTYPE; first_record jsonb; + first_item items%ROWTYPE; + last_item items%ROWTYPE; last_record jsonb; out_records jsonb := '[]'::jsonb; prev_query text; @@ -666,7 +668,9 @@ ELSE ELSE last_record := content_hydrate(iter_record, _search->'fields'); END IF; + last_item := iter_record; IF cntr = 1 THEN + first_item := last_item; first_record := last_record; END IF; IF cntr <= _limit THEN @@ -703,7 +707,7 @@ IF _search ? 'token' THEN concat_ws( ' AND ', _where, - trim(get_token_filter(_search, to_jsonb(content_dehydrate(first_record)))) + trim(get_token_filter(_search, to_jsonb(first_item))) ) ); RAISE NOTICE 'Query to get previous record: % --- %', prev_query, first_record; diff --git a/sql/999_version.sql b/sql/999_version.sql index d8a4b1bd..db70c332 100644 --- a/sql/999_version.sql +++ b/sql/999_version.sql @@ -1 +1 @@ -SELECT set_version('0.5.1'); +SELECT set_version('0.6.0'); diff --git a/test/basic/cql2_searches.sql b/test/basic/cql2_searches.sql new file mode 100644 index 00000000..720294a6 --- /dev/null +++ b/test/basic/cql2_searches.sql @@ -0,0 +1,35 @@ +SET pgstac."default-filter-lang" TO 'cql2-json'; + +SELECT search('{"ids":["pgstac-test-item-0097"],"fields":{"include":["id"]}}'); + +SELECT search('{"ids":["pgstac-test-item-0097","pgstac-test-item-0003"],"fields":{"include":["id"]}}'); + + +SELECT search('{"collections":["pgstac-test-collection"],"fields":{"include":["id"]}, "limit": 1}'); + +SELECT search('{"collections":["something"]}'); + +SELECT search('{"collections":["something"],"fields":{"include":["id"]}}'); + +SELECT search('{"filter":{"op":"t_intersects", "args":[{"property":"datetime"},"2011-08-16T00:00:00Z/2011-08-17T00:00:00Z"]}, "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + +SELECT search('{"filter":{"op":"t_intersects", "args":[{"property":"datetime"},"2011-08-16T00:00:00Z/2011-08-17T00:00:00Z"]}, "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + +SELECT search('{"filter":{"op":"t_intersects", "args":[{"property":"datetime"},"2011-08-16T00:00:00Z/2011-08-17T00:00:00Z"]}, "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + +SELECT search('{"filter":{"op":"eq", "args":[{"property":"eo:cloud_cover"},36]}, "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + +SELECT search('{"filter":{"op":"lt", "args":[{"property":"eo:cloud_cover"},25]}, "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"eo:cloud_cover","direction":"asc"}]}'); + +SELECT search('{"filter":{"op":"in","args":[{"property":"id"},["pgstac-test-item-0097"]]},"fields":{"include":["id"]}}'); + + +SELECT search('{"filter":{"op":"in","args":[{"property":"id"},["pgstac-test-item-0097","pgstac-test-item-0003"]]},"fields":{"include":["id"]}}'); + +SELECT search('{"filter":{"op":"in","args":[{"property":"collection"},["pgstac-test-collection"]]},"fields":{"include":["id"]}, "limit": 1}'); + +SELECT search('{"filter":{"op":"in","args":[{"property":"collection"},["nonexistent"]]}}'); + +SELECT search('{"filter":{"op":"in","args":[{"property":"collection"},["nonexistent"]]}, "conf":{"context":"off"}}'); + +SELECT search('{"conf": {"nohydrate": true}, "limit": 2}'); diff --git a/test/basic/cql2_searches.sql.out b/test/basic/cql2_searches.sql.out new file mode 100644 index 00000000..f6be77d2 --- /dev/null +++ b/test/basic/cql2_searches.sql.out @@ -0,0 +1,52 @@ +SET pgstac."default-filter-lang" TO 'cql2-json'; +SET +SELECT search('{"ids":["pgstac-test-item-0097"],"fields":{"include":["id"]}}'); + {"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 1, "returned": 1}, "features": [{"id": "pgstac-test-item-0097", "collection": "pgstac-test-collection"}]} + +SELECT search('{"ids":["pgstac-test-item-0097","pgstac-test-item-0003"],"fields":{"include":["id"]}}'); + {"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 2, "returned": 2}, "features": [{"id": "pgstac-test-item-0003", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0097", "collection": "pgstac-test-collection"}]} + +SELECT search('{"collections":["pgstac-test-collection"],"fields":{"include":["id"]}, "limit": 1}'); + {"next": "pgstac-test-item-0003", "prev": null, "type": "FeatureCollection", "context": {"limit": 1, "matched": 100, "returned": 1}, "features": [{"id": "pgstac-test-item-0003", "collection": "pgstac-test-collection"}]} + +SELECT search('{"collections":["something"]}'); + {"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 0, "returned": 0}, "features": []} + +SELECT search('{"collections":["something"],"fields":{"include":["id"]}}'); + {"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 0, "returned": 0}, "features": []} + +SELECT search('{"filter":{"op":"t_intersects", "args":[{"property":"datetime"},"2011-08-16T00:00:00Z/2011-08-17T00:00:00Z"]}, "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + {"next": "pgstac-test-item-0016", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 57, "returned": 10}, "features": [{"id": "pgstac-test-item-0007", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 59}}, {"id": "pgstac-test-item-0008", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 64}}, {"id": "pgstac-test-item-0009", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 61}}, {"id": "pgstac-test-item-0010", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 31}}, {"id": "pgstac-test-item-0011", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 41}}, {"id": "pgstac-test-item-0012", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 4}}, {"id": "pgstac-test-item-0013", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0014", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 17}}, {"id": "pgstac-test-item-0015", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 54}}, {"id": "pgstac-test-item-0016", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 13}}]} + +SELECT search('{"filter":{"op":"t_intersects", "args":[{"property":"datetime"},"2011-08-16T00:00:00Z/2011-08-17T00:00:00Z"]}, "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + {"next": "pgstac-test-item-0016", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 57, "returned": 10}, "features": [{"id": "pgstac-test-item-0007", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 59}}, {"id": "pgstac-test-item-0008", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 64}}, {"id": "pgstac-test-item-0009", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 61}}, {"id": "pgstac-test-item-0010", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 31}}, {"id": "pgstac-test-item-0011", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 41}}, {"id": "pgstac-test-item-0012", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 4}}, {"id": "pgstac-test-item-0013", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0014", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 17}}, {"id": "pgstac-test-item-0015", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 54}}, {"id": "pgstac-test-item-0016", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 13}}]} + +SELECT search('{"filter":{"op":"t_intersects", "args":[{"property":"datetime"},"2011-08-16T00:00:00Z/2011-08-17T00:00:00Z"]}, "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + {"next": "pgstac-test-item-0016", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 57, "returned": 10}, "features": [{"id": "pgstac-test-item-0007", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 59}}, {"id": "pgstac-test-item-0008", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 64}}, {"id": "pgstac-test-item-0009", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 61}}, {"id": "pgstac-test-item-0010", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 31}}, {"id": "pgstac-test-item-0011", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 41}}, {"id": "pgstac-test-item-0012", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 4}}, {"id": "pgstac-test-item-0013", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0014", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 17}}, {"id": "pgstac-test-item-0015", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 54}}, {"id": "pgstac-test-item-0016", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 13}}]} + +SELECT search('{"filter":{"op":"eq", "args":[{"property":"eo:cloud_cover"},36]}, "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + {"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 2, "returned": 2}, "features": [{"id": "pgstac-test-item-0087", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-01T00:00:00Z", "eo:cloud_cover": 36}}, {"id": "pgstac-test-item-0089", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-07-31T00:00:00Z", "eo:cloud_cover": 36}}]} + +SELECT search('{"filter":{"op":"lt", "args":[{"property":"eo:cloud_cover"},25]}, "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"eo:cloud_cover","direction":"asc"}]}'); + {"next": "pgstac-test-item-0012", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 31, "returned": 10}, "features": [{"id": "pgstac-test-item-0097", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-07-31T00:00:00Z", "eo:cloud_cover": 1}}, {"id": "pgstac-test-item-0063", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0013", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0085", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-01T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0073", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-15T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0041", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0034", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0005", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-24T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0048", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 4}}, {"id": "pgstac-test-item-0012", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 4}}]} + +SELECT search('{"filter":{"op":"in","args":[{"property":"id"},["pgstac-test-item-0097"]]},"fields":{"include":["id"]}}'); + {"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 1, "returned": 1}, "features": [{"id": "pgstac-test-item-0097", "collection": "pgstac-test-collection"}]} + +SELECT search('{"filter":{"op":"in","args":[{"property":"id"},["pgstac-test-item-0097","pgstac-test-item-0003"]]},"fields":{"include":["id"]}}'); + {"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 2, "returned": 2}, "features": [{"id": "pgstac-test-item-0003", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0097", "collection": "pgstac-test-collection"}]} + +SELECT search('{"filter":{"op":"in","args":[{"property":"collection"},["pgstac-test-collection"]]},"fields":{"include":["id"]}, "limit": 1}'); + {"next": "pgstac-test-item-0003", "prev": null, "type": "FeatureCollection", "context": {"limit": 1, "matched": 100, "returned": 1}, "features": [{"id": "pgstac-test-item-0003", "collection": "pgstac-test-collection"}]} + +SELECT search('{"filter":{"op":"in","args":[{"property":"collection"},["nonexistent"]]}}'); + {"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 0, "returned": 0}, "features": []} + +SELECT search('{"filter":{"op":"in","args":[{"property":"collection"},["nonexistent"]]}, "conf":{"context":"off"}}'); + {"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "returned": 0}, "features": []} + +SELECT search('{"conf": {"nohydrate": true}, "limit": 2}'); + {"next": "pgstac-test-item-0002", "prev": null, "type": "FeatureCollection", "context": {"limit": 2, "matched": 100, "returned": 2}, "features": [{"id": "pgstac-test-item-0003", "bbox": [-85.379245, 30.933949, -85.308201, 31.003555], "type": "Feature", "links": [], "assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008506_nw_16_1_20110825.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008506_nw_16_1_20110825.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008506_nw_16_1_20110825.200.jpg"}}, "geometry": {"type": "Polygon", "coordinates": [[[-85.309412, 30.933949], [-85.308201, 31.002658], [-85.378084, 31.003555], [-85.379245, 30.934843], [-85.309412, 30.933949]]]}, "collection": "pgstac-test-collection", "properties": {"gsd": 1, "datetime": "2011-08-25T00:00:00Z", "naip:year": "2011", "proj:bbox": [654842, 3423507, 661516, 3431125], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7618, 6674], "eo:cloud_cover": 28, "proj:transform": [1, 0, 654842, 0, -1, 3431125, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]}, {"id": "pgstac-test-item-0002", "bbox": [-85.504167, 30.934008, -85.433293, 31.003486], "type": "Feature", "links": [], "assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008505_nw_16_1_20110825.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008505_nw_16_1_20110825.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008505_nw_16_1_20110825.200.jpg"}}, "geometry": {"type": "Polygon", "coordinates": [[[-85.434414, 30.934008], [-85.433293, 31.002658], [-85.503096, 31.003486], [-85.504167, 30.934834], [-85.434414, 30.934008]]]}, "collection": "pgstac-test-collection", "properties": {"gsd": 1, "datetime": "2011-08-25T00:00:00Z", "naip:year": "2011", "proj:bbox": [642906, 3423339, 649572, 3430950], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7611, 6666], "eo:cloud_cover": 33, "proj:transform": [1, 0, 642906, 0, -1, 3430950, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]}]} + +\set QUIET 1 +\set ECHO none diff --git a/test/basic/cql_searches.sql b/test/basic/cql_searches.sql new file mode 100644 index 00000000..3aa6728a --- /dev/null +++ b/test/basic/cql_searches.sql @@ -0,0 +1,41 @@ +SET pgstac."default-filter-lang" TO 'cql-json'; + +SELECT search('{"fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + +-- Test Paging +SELECT search('{"fields":{"include":["id"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + +SELECT search('{"token":"next:pgstac-test-item-0010", "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + +SELECT search('{"token":"next:pgstac-test-item-0020", "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + +SELECT search('{"token":"prev:pgstac-test-item-0021", "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + +SELECT search('{"token":"next:pgstac-test-item-0011", "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); +-- + +SELECT search('{"datetime":"2011-08-16T00:00:00Z/2011-08-17T00:00:00Z", "fields":{"include":["id","properties.datetime"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + +SELECT search('{"datetime":["2011-08-16T00:00:00Z","2011-08-17T00:00:00Z"], "fields":{"include":["id","properties.datetime"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + +SELECT search('{"filter":{"anyinteracts":[{"property":"datetime"},["2011-08-16T00:00:00Z","2011-08-17T00:00:00Z"]]}, "fields":{"include":["id","properties.datetime"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + +SELECT search('{"filter":{"eq":[{"property":"eo:cloud_cover"},36]}, "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + +SELECT search('{"filter":{"lt":[{"property":"eo:cloud_cover"},25]}, "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"eo:cloud_cover","direction":"asc"}]}'); + +SELECT search('{"ids":["pgstac-test-item-0097"],"fields":{"include":["id"]}}'); + +SELECT search('{"ids":["pgstac-test-item-0097","pgstac-test-item-0003"],"fields":{"include":["id"]}}'); + +SELECT search('{"ids":["bogusid"],"fields":{"include":["id"]}}'); + +SELECT search('{"collections":["pgstac-test-collection"],"fields":{"include":["id"]}, "limit": 1}'); + +SELECT search('{"collections":["something"]}'); + +SELECT search('{"collections":["something"],"fields":{"include":["id"]}}'); + +SELECT hash from search_query('{"collections":["pgstac-test-collection"]}'); + +SELECT search from search_query('{"collections":["pgstac-test-collection"]}'); diff --git a/test/basic/cql_searches.sql.out b/test/basic/cql_searches.sql.out new file mode 100644 index 00000000..64432c86 --- /dev/null +++ b/test/basic/cql_searches.sql.out @@ -0,0 +1,63 @@ +SET pgstac."default-filter-lang" TO 'cql-json'; +SET +SELECT search('{"fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + {"next": "pgstac-test-item-0010", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 100, "returned": 10}, "features": [{"id": "pgstac-test-item-0001", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-25T00:00:00Z", "eo:cloud_cover": 89}}, {"id": "pgstac-test-item-0002", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-25T00:00:00Z", "eo:cloud_cover": 33}}, {"id": "pgstac-test-item-0003", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-25T00:00:00Z", "eo:cloud_cover": 28}}, {"id": "pgstac-test-item-0004", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-24T00:00:00Z", "eo:cloud_cover": 23}}, {"id": "pgstac-test-item-0005", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-24T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0006", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-24T00:00:00Z", "eo:cloud_cover": 100}}, {"id": "pgstac-test-item-0007", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 59}}, {"id": "pgstac-test-item-0008", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 64}}, {"id": "pgstac-test-item-0009", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 61}}, {"id": "pgstac-test-item-0010", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 31}}]} + +-- Test Paging +SELECT search('{"fields":{"include":["id"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + {"next": "pgstac-test-item-0010", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 100, "returned": 10}, "features": [{"id": "pgstac-test-item-0001", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0002", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0003", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0004", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0005", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0006", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0007", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0008", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0009", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0010", "collection": "pgstac-test-collection"}]} + +SELECT search('{"token":"next:pgstac-test-item-0010", "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + {"next": "pgstac-test-item-0020", "prev": "pgstac-test-item-0011", "type": "FeatureCollection", "context": {"limit": 10, "matched": 100, "returned": 10}, "features": [{"id": "pgstac-test-item-0011", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 41}}, {"id": "pgstac-test-item-0012", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 4}}, {"id": "pgstac-test-item-0013", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0014", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 17}}, {"id": "pgstac-test-item-0015", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 54}}, {"id": "pgstac-test-item-0016", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 13}}, {"id": "pgstac-test-item-0017", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 59}}, {"id": "pgstac-test-item-0018", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 29}}, {"id": "pgstac-test-item-0019", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 52}}, {"id": "pgstac-test-item-0020", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 39}}]} + +SELECT search('{"token":"next:pgstac-test-item-0020", "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + {"next": "pgstac-test-item-0030", "prev": "pgstac-test-item-0021", "type": "FeatureCollection", "context": {"limit": 10, "matched": 100, "returned": 10}, "features": [{"id": "pgstac-test-item-0021", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 29}}, {"id": "pgstac-test-item-0022", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 84}}, {"id": "pgstac-test-item-0023", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 56}}, {"id": "pgstac-test-item-0024", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 58}}, {"id": "pgstac-test-item-0025", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 65}}, {"id": "pgstac-test-item-0026", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 52}}, {"id": "pgstac-test-item-0027", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 43}}, {"id": "pgstac-test-item-0028", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 46}}, {"id": "pgstac-test-item-0029", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 42}}, {"id": "pgstac-test-item-0030", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 16}}]} + +SELECT search('{"token":"prev:pgstac-test-item-0021", "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + {"next": "pgstac-test-item-0020", "prev": "pgstac-test-item-0011", "type": "FeatureCollection", "context": {"limit": 10, "matched": 100, "returned": 10}, "features": [{"id": "pgstac-test-item-0011", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 41}}, {"id": "pgstac-test-item-0012", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 4}}, {"id": "pgstac-test-item-0013", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0014", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 17}}, {"id": "pgstac-test-item-0015", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 54}}, {"id": "pgstac-test-item-0016", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 13}}, {"id": "pgstac-test-item-0017", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 59}}, {"id": "pgstac-test-item-0018", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 29}}, {"id": "pgstac-test-item-0019", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 52}}, {"id": "pgstac-test-item-0020", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 39}}]} + +SELECT search('{"token":"next:pgstac-test-item-0011", "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + {"next": "pgstac-test-item-0021", "prev": "pgstac-test-item-0012", "type": "FeatureCollection", "context": {"limit": 10, "matched": 100, "returned": 10}, "features": [{"id": "pgstac-test-item-0012", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 4}}, {"id": "pgstac-test-item-0013", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0014", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 17}}, {"id": "pgstac-test-item-0015", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 54}}, {"id": "pgstac-test-item-0016", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 13}}, {"id": "pgstac-test-item-0017", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 59}}, {"id": "pgstac-test-item-0018", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 29}}, {"id": "pgstac-test-item-0019", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 52}}, {"id": "pgstac-test-item-0020", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 39}}, {"id": "pgstac-test-item-0021", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 29}}]} + +-- +SELECT search('{"datetime":"2011-08-16T00:00:00Z/2011-08-17T00:00:00Z", "fields":{"include":["id","properties.datetime"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + {"next": "pgstac-test-item-0016", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 57, "returned": 10}, "features": [{"id": "pgstac-test-item-0007", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0008", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0009", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0010", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0011", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0012", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0013", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0014", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z"}}, {"id": "pgstac-test-item-0015", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z"}}, {"id": "pgstac-test-item-0016", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z"}}]} + +SELECT search('{"datetime":["2011-08-16T00:00:00Z","2011-08-17T00:00:00Z"], "fields":{"include":["id","properties.datetime"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + {"next": "pgstac-test-item-0016", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 57, "returned": 10}, "features": [{"id": "pgstac-test-item-0007", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0008", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0009", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0010", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0011", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0012", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0013", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0014", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z"}}, {"id": "pgstac-test-item-0015", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z"}}, {"id": "pgstac-test-item-0016", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z"}}]} + +SELECT search('{"filter":{"anyinteracts":[{"property":"datetime"},["2011-08-16T00:00:00Z","2011-08-17T00:00:00Z"]]}, "fields":{"include":["id","properties.datetime"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + {"next": "pgstac-test-item-0016", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 57, "returned": 10}, "features": [{"id": "pgstac-test-item-0007", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0008", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0009", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0010", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0011", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0012", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0013", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z"}}, {"id": "pgstac-test-item-0014", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z"}}, {"id": "pgstac-test-item-0015", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z"}}, {"id": "pgstac-test-item-0016", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z"}}]} + +SELECT search('{"filter":{"eq":[{"property":"eo:cloud_cover"},36]}, "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}'); + {"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 2, "returned": 2}, "features": [{"id": "pgstac-test-item-0087", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-01T00:00:00Z", "eo:cloud_cover": 36}}, {"id": "pgstac-test-item-0089", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-07-31T00:00:00Z", "eo:cloud_cover": 36}}]} + +SELECT search('{"filter":{"lt":[{"property":"eo:cloud_cover"},25]}, "fields":{"include":["id","properties.datetime","properties.eo:cloud_cover"]},"sortby":[{"field":"eo:cloud_cover","direction":"asc"}]}'); + {"next": "pgstac-test-item-0012", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 31, "returned": 10}, "features": [{"id": "pgstac-test-item-0097", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-07-31T00:00:00Z", "eo:cloud_cover": 1}}, {"id": "pgstac-test-item-0063", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0013", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0085", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-01T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0073", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-15T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0041", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0034", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0005", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-24T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0048", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 4}}, {"id": "pgstac-test-item-0012", "collection": "pgstac-test-collection", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 4}}]} + +SELECT search('{"ids":["pgstac-test-item-0097"],"fields":{"include":["id"]}}'); + {"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 1, "returned": 1}, "features": [{"id": "pgstac-test-item-0097", "collection": "pgstac-test-collection"}]} + +SELECT search('{"ids":["pgstac-test-item-0097","pgstac-test-item-0003"],"fields":{"include":["id"]}}'); + {"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 2, "returned": 2}, "features": [{"id": "pgstac-test-item-0003", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0097", "collection": "pgstac-test-collection"}]} + +SELECT search('{"ids":["bogusid"],"fields":{"include":["id"]}}'); + {"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 0, "returned": 0}, "features": []} + +SELECT search('{"collections":["pgstac-test-collection"],"fields":{"include":["id"]}, "limit": 1}'); + {"next": "pgstac-test-item-0003", "prev": null, "type": "FeatureCollection", "context": {"limit": 1, "matched": 100, "returned": 1}, "features": [{"id": "pgstac-test-item-0003", "collection": "pgstac-test-collection"}]} + +SELECT search('{"collections":["something"]}'); + {"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 0, "returned": 0}, "features": []} + +SELECT search('{"collections":["something"],"fields":{"include":["id"]}}'); + {"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 0, "returned": 0}, "features": []} + +SELECT hash from search_query('{"collections":["pgstac-test-collection"]}'); + 2bbae9a0ef0bbb5ffaca06603ce621d7 + +SELECT search from search_query('{"collections":["pgstac-test-collection"]}'); + {"collections": ["pgstac-test-collection"]} + +\set QUIET 1 +\set ECHO none diff --git a/test/basic/sqltest.sh b/test/basic/sqltest.sh new file mode 100755 index 00000000..48ff56d0 --- /dev/null +++ b/test/basic/sqltest.sh @@ -0,0 +1,63 @@ +#!/bin/bash +SCRIPTDIR=$(dirname "$0") +cd $SCRIPTDIR +SQLFILE=$(pwd)/$1 +SQLOUTFILE=${SQLFILE}.out +PGDATABASE_OLD=$PGDATABASE + +echo $SQLFILE +echo $SQLOUTFILE + +psql <"$TMPFILE" +\set QUIET 1 +\set ON_ERROR_STOP 1 +\set ON_ERROR_ROLLBACK 1 + +BEGIN; +SET SEARCH_PATH TO pgstac, public; +SET client_min_messages TO 'warning'; +SET pgstac.context TO 'on'; +SET pgstac."default-filter-lang" TO 'cql-json'; + +DELETE FROM collections WHERE id = 'pgstac-test-collection'; +\copy collections (content) FROM '../testdata/collections.ndjson'; +\copy items_staging (content) FROM '../testdata/items.ndjson' + +\t + +\set QUIET 0 +\set ECHO all +$(cat $SQLFILE) +\set QUIET 1 +\set ECHO none +ROLLBACK; +EOSQL + +if [ "$2" == "generateout" ]; then + echo "Creating $SQLOUTFILE" + cat $TMPFILE >$SQLOUTFILE +else + diff -Z -b -w -B --strip-trailing-cr "$TMPFILE" $SQLOUTFILE + error=$? +fi + +export PGDATABASE=$PGDATABASE_OLD +psql < true); + +SELECT xyzsearch(16792, 26892, 16, '2bbae9a0ef0bbb5ffaca06603ce621d7', '{"include":["id"]}'::jsonb, exitwhenfull => false, skipcovered => false); diff --git a/test/basic/xyz_searches.sql.out b/test/basic/xyz_searches.sql.out new file mode 100644 index 00000000..72da70d1 --- /dev/null +++ b/test/basic/xyz_searches.sql.out @@ -0,0 +1,22 @@ +SET pgstac."default-filter-lang" TO 'cql-json'; +SET +SELECT hash from search_query('{"collections":["pgstac-test-collection"]}'); + 2bbae9a0ef0bbb5ffaca06603ce621d7 + +SELECT xyzsearch(8615, 13418, 15, '2bbae9a0ef0bbb5ffaca06603ce621d7', '{"include":["id"]}'::jsonb); + {"type": "FeatureCollection", "features": [{"id": "pgstac-test-item-0003", "collection": "pgstac-test-collection"}]} + +SELECT xyzsearch(1048, 1682, 12, '2bbae9a0ef0bbb5ffaca06603ce621d7', '{"include":["id"]}'::jsonb); + {"type": "FeatureCollection", "features": [{"id": "pgstac-test-item-0050", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0049", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0048", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0047", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0100", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0089", "collection": "pgstac-test-collection"}]} + +SELECT xyzsearch(1048, 1682, 12, '2bbae9a0ef0bbb5ffaca06603ce621d7', '{"include":["id"]}'::jsonb, NULL, 1); + {"type": "FeatureCollection", "features": [{"id": "pgstac-test-item-0050", "collection": "pgstac-test-collection"}]} + +SELECT xyzsearch(16792, 26892, 16, '2bbae9a0ef0bbb5ffaca06603ce621d7', '{"include":["id"]}'::jsonb, exitwhenfull => true); + {"type": "FeatureCollection", "features": [{"id": "pgstac-test-item-0098", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0097", "collection": "pgstac-test-collection"}]} + +SELECT xyzsearch(16792, 26892, 16, '2bbae9a0ef0bbb5ffaca06603ce621d7', '{"include":["id"]}'::jsonb, exitwhenfull => false, skipcovered => false); + {"type": "FeatureCollection", "features": [{"id": "pgstac-test-item-0098", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0097", "collection": "pgstac-test-collection"}, {"id": "pgstac-test-item-0091", "collection": "pgstac-test-collection"}]} + +\set QUIET 1 +\set ECHO none diff --git a/test/pgtap.sql b/test/pgtap.sql index ab998c9c..cc52ce4e 100644 --- a/test/pgtap.sql +++ b/test/pgtap.sql @@ -19,7 +19,7 @@ SET SEARCH_PATH TO pgstac, pgtap, public; SET CLIENT_MIN_MESSAGES TO 'warning'; -- Plan the tests. -SELECT plan(98); +SELECT plan(101); --SELECT * FROM no_plan(); -- Run the tests. diff --git a/test/pgtap/003_items.sql b/test/pgtap/003_items.sql index 84c42f30..da618012 100644 --- a/test/pgtap/003_items.sql +++ b/test/pgtap/003_items.sql @@ -19,3 +19,36 @@ SELECT has_function('pgstac'::name, 'upsert_items', ARRAY['jsonb']); SELECT has_function('pgstac'::name, 'collection_bbox', ARRAY['text']); SELECT has_function('pgstac'::name, 'collection_temporal_extent', ARRAY['text']); SELECT has_function('pgstac'::name, 'update_collection_extents', NULL); + +DELETE FROM collections WHERE id = 'pgstac-test-collection'; +\copy collections (content) FROM 'test/testdata/collections.ndjson'; + +SELECT create_item('{"id": "pgstac-test-item-0003", "bbox": [-85.379245, 30.933949, -85.308201, 31.003555], "type": "Feature", "links": [], "assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008506_nw_16_1_20110825.tif", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "title": "RGBIR COG tile", "eo:bands": [{"name": "Red", "common_name": "red"}, {"name": "Green", "common_name": "green"}, {"name": "Blue", "common_name": "blue"}, {"name": "NIR", "common_name": "nir", "description": "near-infrared"}]}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008506_nw_16_1_20110825.txt", "type": "text/plain", "roles": ["metadata"], "title": "FGDC Metdata"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008506_nw_16_1_20110825.200.jpg", "type": "image/jpeg", "roles": ["thumbnail"], "title": "Thumbnail"}}, "geometry": {"type": "Polygon", "coordinates": [[[-85.309412, 30.933949], [-85.308201, 31.002658], [-85.378084, 31.003555], [-85.379245, 30.934843], [-85.309412, 30.933949]]]}, "collection": "pgstac-test-collection", "properties": {"gsd": 1, "datetime": "2011-08-25T00:00:00Z", "naip:year": "2011", "proj:bbox": [654842, 3423507, 661516, 3431125], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7618, 6674], "eo:cloud_cover": 28, "proj:transform": [1, 0, 654842, 0, -1, 3431125, 0, 0, 1]}, "stac_version": "1.0.0-beta.2", "stac_extensions": ["eo", "projection"]}'); + +SELECT results_eq($$ + SELECT content->'properties'->>'eo:cloud_cover' FROM items WHERE collection='pgstac-test-collection'; + $$,$$ + SELECT '28'; + $$, + 'Test create_item function' +); + +SELECT update_item('{"id": "pgstac-test-item-0003", "bbox": [-85.379245, 30.933949, -85.308201, 31.003555], "type": "Feature", "links": [], "assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008506_nw_16_1_20110825.tif", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "title": "RGBIR COG tile", "eo:bands": [{"name": "Red", "common_name": "red"}, {"name": "Green", "common_name": "green"}, {"name": "Blue", "common_name": "blue"}, {"name": "NIR", "common_name": "nir", "description": "near-infrared"}]}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008506_nw_16_1_20110825.txt", "type": "text/plain", "roles": ["metadata"], "title": "FGDC Metdata"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008506_nw_16_1_20110825.200.jpg", "type": "image/jpeg", "roles": ["thumbnail"], "title": "Thumbnail"}}, "geometry": {"type": "Polygon", "coordinates": [[[-85.309412, 30.933949], [-85.308201, 31.002658], [-85.378084, 31.003555], [-85.379245, 30.934843], [-85.309412, 30.933949]]]}, "collection": "pgstac-test-collection", "properties": {"gsd": 1, "datetime": "2011-08-25T00:00:00Z", "naip:year": "2011", "proj:bbox": [654842, 3423507, 661516, 3431125], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7618, 6674], "eo:cloud_cover": 29, "proj:transform": [1, 0, 654842, 0, -1, 3431125, 0, 0, 1]}, "stac_version": "1.0.0-beta.2", "stac_extensions": ["eo", "projection"]}'); + +SELECT results_eq($$ + SELECT content->'properties'->>'eo:cloud_cover' FROM items WHERE collection='pgstac-test-collection'; + $$,$$ + SELECT '29'; + $$, + 'Test update_item function' +); + +select delete_item('pgstac-test-item-0003'); + +SELECT results_eq($$ + SELECT count(*) FROM items WHERE collection='pgstac-test-collection'; + $$,$$ + SELECT 0::bigint; + $$, + 'Test delete_item function' +); diff --git a/test/pgtap/004_search.sql b/test/pgtap/004_search.sql index 8aee9c68..0ea9cc11 100644 --- a/test/pgtap/004_search.sql +++ b/test/pgtap/004_search.sql @@ -1,6 +1,5 @@ -- CREATE fixtures for testing search - as tests are run within a transaction, these will not persist -DELETE FROM collections WHERE id = 'pgstac-test-collection'; -\copy collections (content) FROM 'test/testdata/collections.ndjson' + \copy items_staging (content) FROM 'test/testdata/items.ndjson' SET pgstac.context TO 'on'; @@ -44,140 +43,9 @@ SELECT results_eq($$ 'Test creation of reverse sort sql' ); -SELECT results_eq($$ - select s from search('{"fields":{"include":["id","datetime","eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}') s; - $$,$$ - select '{"next": "pgstac-test-item-0010", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 100, "returned": 10}, "features": [{"id": "pgstac-test-item-0001", "properties": {"datetime": "2011-08-25T00:00:00Z", "eo:cloud_cover": 89}}, {"id": "pgstac-test-item-0002", "properties": {"datetime": "2011-08-25T00:00:00Z", "eo:cloud_cover": 33}}, {"id": "pgstac-test-item-0003", "properties": {"datetime": "2011-08-25T00:00:00Z", "eo:cloud_cover": 28}}, {"id": "pgstac-test-item-0004", "properties": {"datetime": "2011-08-24T00:00:00Z", "eo:cloud_cover": 23}}, {"id": "pgstac-test-item-0005", "properties": {"datetime": "2011-08-24T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0006", "properties": {"datetime": "2011-08-24T00:00:00Z", "eo:cloud_cover": 100}}, {"id": "pgstac-test-item-0007", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 59}}, {"id": "pgstac-test-item-0008", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 64}}, {"id": "pgstac-test-item-0009", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 61}}, {"id": "pgstac-test-item-0010", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 31}}]}'::jsonb - $$, - 'Test basic search with fields and sort extension' -); - -SELECT results_eq($$ - select s from search('{"token":"next:pgstac-test-item-0010", "fields":{"include":["id","datetime","eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}') s; - $$,$$ - select '{"next": "pgstac-test-item-0020", "prev": "pgstac-test-item-0011", "type": "FeatureCollection", "context": {"limit": 10, "matched": 100, "returned": 10}, "features": [{"id": "pgstac-test-item-0011", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 41}}, {"id": "pgstac-test-item-0012", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 4}}, {"id": "pgstac-test-item-0013", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0014", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 17}}, {"id": "pgstac-test-item-0015", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 54}}, {"id": "pgstac-test-item-0016", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 13}}, {"id": "pgstac-test-item-0017", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 59}}, {"id": "pgstac-test-item-0018", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 29}}, {"id": "pgstac-test-item-0019", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 52}}, {"id": "pgstac-test-item-0020", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 39}}]}'::jsonb - $$, - 'Test basic search with fields and sort extension and next token' -); - -SELECT results_eq($$ - select s from search('{"token":"prev:pgstac-test-item-0011", "fields":{"include":["id","datetime","eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}') s; - $$,$$ -- should be the same result as the first base query - select '{"next": "pgstac-test-item-0010", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 100, "returned": 10}, "features": [{"id": "pgstac-test-item-0001", "properties": {"datetime": "2011-08-25T00:00:00Z", "eo:cloud_cover": 89}}, {"id": "pgstac-test-item-0002", "properties": {"datetime": "2011-08-25T00:00:00Z", "eo:cloud_cover": 33}}, {"id": "pgstac-test-item-0003", "properties": {"datetime": "2011-08-25T00:00:00Z", "eo:cloud_cover": 28}}, {"id": "pgstac-test-item-0004", "properties": {"datetime": "2011-08-24T00:00:00Z", "eo:cloud_cover": 23}}, {"id": "pgstac-test-item-0005", "properties": {"datetime": "2011-08-24T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0006", "properties": {"datetime": "2011-08-24T00:00:00Z", "eo:cloud_cover": 100}}, {"id": "pgstac-test-item-0007", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 59}}, {"id": "pgstac-test-item-0008", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 64}}, {"id": "pgstac-test-item-0009", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 61}}, {"id": "pgstac-test-item-0010", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 31}}]}'::jsonb - $$, - 'Test basic search with fields and sort extension and prev token' -); - -SELECT results_eq($$ - select s from search('{"datetime":"2011-08-16T00:00:00Z/2011-08-17T00:00:00Z", "fields":{"include":["id","datetime","eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}') s; - $$,$$ - select '{"next": "pgstac-test-item-0016", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 57, "returned": 10}, "features": [{"id": "pgstac-test-item-0007", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 59}}, {"id": "pgstac-test-item-0008", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 64}}, {"id": "pgstac-test-item-0009", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 61}}, {"id": "pgstac-test-item-0010", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 31}}, {"id": "pgstac-test-item-0011", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 41}}, {"id": "pgstac-test-item-0012", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 4}}, {"id": "pgstac-test-item-0013", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0014", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 17}}, {"id": "pgstac-test-item-0015", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 54}}, {"id": "pgstac-test-item-0016", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 13}}]}'::jsonb - $$, - 'Test datetime search with datetime as / separated string' -); - - -SELECT results_eq($$ - select s from search('{"datetime":["2011-08-16T00:00:00Z","2011-08-17T00:00:00Z"], "fields":{"include":["id","datetime","eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}') s; - $$,$$ - select '{"next": "pgstac-test-item-0016", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 57, "returned": 10}, "features": [{"id": "pgstac-test-item-0007", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 59}}, {"id": "pgstac-test-item-0008", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 64}}, {"id": "pgstac-test-item-0009", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 61}}, {"id": "pgstac-test-item-0010", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 31}}, {"id": "pgstac-test-item-0011", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 41}}, {"id": "pgstac-test-item-0012", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 4}}, {"id": "pgstac-test-item-0013", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0014", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 17}}, {"id": "pgstac-test-item-0015", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 54}}, {"id": "pgstac-test-item-0016", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 13}}]}'::jsonb - $$, - 'Test datetime search with datetime as array' -); - -SELECT results_eq($$ - select s from search('{"filter":{"anyinteracts":[{"property":"datetime"},["2011-08-16T00:00:00Z","2011-08-17T00:00:00Z"]]}, "fields":{"include":["id","datetime","eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}') s; - $$,$$ - select '{"next": "pgstac-test-item-0016", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 57, "returned": 10}, "features": [{"id": "pgstac-test-item-0007", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 59}}, {"id": "pgstac-test-item-0008", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 64}}, {"id": "pgstac-test-item-0009", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 61}}, {"id": "pgstac-test-item-0010", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 31}}, {"id": "pgstac-test-item-0011", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 41}}, {"id": "pgstac-test-item-0012", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 4}}, {"id": "pgstac-test-item-0013", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0014", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 17}}, {"id": "pgstac-test-item-0015", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 54}}, {"id": "pgstac-test-item-0016", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 13}}]}'::jsonb - $$, - 'Test datetime as an anyinteracts filter' -); - -SELECT results_eq($$ - select s from search('{"filter":{"eq":[{"property":"eo:cloud_cover"},36]}, "fields":{"include":["id","datetime","eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}') s; - $$,$$ - select '{"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 2, "returned": 2}, "features": [{"id": "pgstac-test-item-0087", "properties": {"datetime": "2011-08-01T00:00:00Z", "eo:cloud_cover": 36}}, {"id": "pgstac-test-item-0089", "properties": {"datetime": "2011-07-31T00:00:00Z", "eo:cloud_cover": 36}}]}'::jsonb - $$, - 'Test equality as a filter on a numeric field' -); - -SELECT results_eq($$ - select s from search('{"filter":{"lt":[{"property":"eo:cloud_cover"},25]}, "fields":{"include":["id","datetime","eo:cloud_cover"]},"sortby":[{"field":"eo:cloud_cover","direction":"asc"}]}') s; - $$,$$ - select '{"next": "pgstac-test-item-0012", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 31, "returned": 10}, "features": [{"id": "pgstac-test-item-0097", "properties": {"datetime": "2011-07-31T00:00:00Z", "eo:cloud_cover": 1}}, {"id": "pgstac-test-item-0063", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0013", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0085", "properties": {"datetime": "2011-08-01T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0073", "properties": {"datetime": "2011-08-15T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0041", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0034", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0005", "properties": {"datetime": "2011-08-24T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0048", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 4}}, {"id": "pgstac-test-item-0012", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 4}}]}'::jsonb - $$, - 'Test lt as a filter on a numeric field with order by' -); - -SELECT results_eq($$ - select s from search('{"ids":["pgstac-test-item-0097"],"fields":{"include":["id"]}}') s; - $$,$$ - select '{"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 1, "returned": 1}, "features": [{"id": "pgstac-test-item-0097"}]}'::jsonb - $$, - 'Test ids search single' -); - - -SELECT results_eq($$ - select s from search('{"ids":["pgstac-test-item-0097","pgstac-test-item-0003"],"fields":{"include":["id"]}}') s; - $$,$$ - select '{"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 2, "returned": 2}, "features": [{"id": "pgstac-test-item-0003"},{"id": "pgstac-test-item-0097"}]}'::jsonb - $$, - 'Test ids search multi' -); - -SELECT results_eq($$ - select s from search('{"ids":["bogusid"],"fields":{"include":["id"]}}') s; - $$,$$ - select '{"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 0, "returned": 0}, "features": []}'::jsonb - $$, - 'non-existent id returns empty array' -); - - -SELECT results_eq($$ - select s from search('{"collections":["pgstac-test-collection"],"fields":{"include":["id"]}, "limit": 1}') s; - $$,$$ - select '{"next": "pgstac-test-item-0003", "prev": null, "type": "FeatureCollection", "context": {"limit": 1, "matched": 100, "returned": 1}, "features": [{"id": "pgstac-test-item-0003"}]}'::jsonb - $$, - 'Test collections search' -); - -SELECT results_eq($$ - select s from search('{"collections":["something"]}') s; - $$,$$ - select '{"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 0, "returned": 0}, "features": []}'::jsonb - $$, - 'Test collections search with unknow collection' -); - -SELECT results_eq($$ - select s from search('{"collections":["something"],"fields":{"include":["id"]}}') s; - $$,$$ - select '{"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 0, "returned": 0}, "features": []}'::jsonb - $$, - 'Test collections search return empty feature not null' -); SELECT has_function('pgstac'::name, 'search_query', ARRAY['jsonb','boolean','jsonb']); -SELECT results_eq($$ - select hash from search_query('{"collections":["pgstac-test-collection"]}') s; - $$,$$ - select '2bbae9a0ef0bbb5ffaca06603ce621d7' - $$, - 'Test search_query to return valid hash' -); - -SELECT results_eq($$ - select search from search_query('{"collections":["pgstac-test-collection"]}') s; - $$,$$ - select '{"collections":["pgstac-test-collection"]}'::jsonb - $$, - 'Test search_query to return valid search' -); - - SELECT results_eq($$ SELECT BTRIM(stac_search_to_where($q$ @@ -204,48 +72,6 @@ SELECT results_eq($$ SET pgstac."default-filter-lang" TO 'cql2-json'; - -SELECT results_eq($$ - select s from search('{"ids":["pgstac-test-item-0097"],"fields":{"include":["id"]}}') s; - $$,$$ - select '{"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 1, "returned": 1}, "features": [{"id": "pgstac-test-item-0097"}]}'::jsonb - $$, - 'Test ids search single' -); - -SELECT results_eq($$ - select s from search('{"ids":["pgstac-test-item-0097","pgstac-test-item-0003"],"fields":{"include":["id"]}}') s; - $$,$$ - select '{"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 2, "returned": 2}, "features": [{"id": "pgstac-test-item-0003"},{"id": "pgstac-test-item-0097"}]}'::jsonb - $$, - 'Test ids search multi' -); - - -SELECT results_eq($$ - select s from search('{"collections":["pgstac-test-collection"],"fields":{"include":["id"]}, "limit": 1}') s; - $$,$$ - select '{"next": "pgstac-test-item-0003", "prev": null, "type": "FeatureCollection", "context": {"limit": 1, "matched": 100, "returned": 1}, "features": [{"id": "pgstac-test-item-0003"}]}'::jsonb - $$, - 'Test collections search' -); - -SELECT results_eq($$ - select s from search('{"collections":["something"]}') s; - $$,$$ - select '{"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 0, "returned": 0}, "features": []}'::jsonb - $$, - 'Test collections search with unknow collection' -); - -SELECT results_eq($$ - select s from search('{"collections":["something"],"fields":{"include":["id"]}}') s; - $$,$$ - select '{"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 0, "returned": 0}, "features": []}'::jsonb - $$, - 'Test collections search return empty feature not null' -); - SELECT results_eq($$ SELECT BTRIM(stac_search_to_where($q$ { @@ -626,97 +452,6 @@ SELECT results_eq($$ - -SELECT results_eq($$ - select s from search('{"filter":{"op":"t_intersects", "args":[{"property":"datetime"},"2011-08-16T00:00:00Z/2011-08-17T00:00:00Z"]}, "fields":{"include":["id","datetime","eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}') s; - $$,$$ - select '{"next": "pgstac-test-item-0016", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 57, "returned": 10}, "features": [{"id": "pgstac-test-item-0007", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 59}}, {"id": "pgstac-test-item-0008", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 64}}, {"id": "pgstac-test-item-0009", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 61}}, {"id": "pgstac-test-item-0010", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 31}}, {"id": "pgstac-test-item-0011", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 41}}, {"id": "pgstac-test-item-0012", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 4}}, {"id": "pgstac-test-item-0013", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0014", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 17}}, {"id": "pgstac-test-item-0015", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 54}}, {"id": "pgstac-test-item-0016", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 13}}]}'::jsonb - $$, - 'Test datetime search with datetime as / separated string' -); - - -SELECT results_eq($$ - select s from search('{"filter":{"op":"t_intersects", "args":[{"property":"datetime"},"2011-08-16T00:00:00Z/2011-08-17T00:00:00Z"]}, "fields":{"include":["id","datetime","eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}') s; - $$,$$ - select '{"next": "pgstac-test-item-0016", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 57, "returned": 10}, "features": [{"id": "pgstac-test-item-0007", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 59}}, {"id": "pgstac-test-item-0008", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 64}}, {"id": "pgstac-test-item-0009", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 61}}, {"id": "pgstac-test-item-0010", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 31}}, {"id": "pgstac-test-item-0011", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 41}}, {"id": "pgstac-test-item-0012", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 4}}, {"id": "pgstac-test-item-0013", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0014", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 17}}, {"id": "pgstac-test-item-0015", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 54}}, {"id": "pgstac-test-item-0016", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 13}}]}'::jsonb - $$, - 'Test datetime search with datetime as array' -); - -SELECT results_eq($$ - select s from search('{"filter":{"op":"t_intersects", "args":[{"property":"datetime"},"2011-08-16T00:00:00Z/2011-08-17T00:00:00Z"]}, "fields":{"include":["id","datetime","eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}') s; - $$,$$ - select '{"next": "pgstac-test-item-0016", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 57, "returned": 10}, "features": [{"id": "pgstac-test-item-0007", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 59}}, {"id": "pgstac-test-item-0008", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 64}}, {"id": "pgstac-test-item-0009", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 61}}, {"id": "pgstac-test-item-0010", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 31}}, {"id": "pgstac-test-item-0011", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 41}}, {"id": "pgstac-test-item-0012", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 4}}, {"id": "pgstac-test-item-0013", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0014", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 17}}, {"id": "pgstac-test-item-0015", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 54}}, {"id": "pgstac-test-item-0016", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 13}}]}'::jsonb - $$, - 'Test datetime as an anyinteracts filter' -); - -SELECT results_eq($$ - select s from search('{"filter":{"op":"eq", "args":[{"property":"eo:cloud_cover"},36]}, "fields":{"include":["id","datetime","eo:cloud_cover"]},"sortby":[{"field":"datetime","direction":"desc"},{"field":"id","direction":"asc"}]}') s; - $$,$$ - select '{"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 2, "returned": 2}, "features": [{"id": "pgstac-test-item-0087", "properties": {"datetime": "2011-08-01T00:00:00Z", "eo:cloud_cover": 36}}, {"id": "pgstac-test-item-0089", "properties": {"datetime": "2011-07-31T00:00:00Z", "eo:cloud_cover": 36}}]}'::jsonb - $$, - 'Test equality as a filter on a numeric field' -); - -SELECT results_eq($$ - select s from search('{"filter":{"op":"lt", "args":[{"property":"eo:cloud_cover"},25]}, "fields":{"include":["id","datetime","eo:cloud_cover"]},"sortby":[{"field":"eo:cloud_cover","direction":"asc"}]}') s; - $$,$$ - select '{"next": "pgstac-test-item-0012", "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 31, "returned": 10}, "features": [{"id": "pgstac-test-item-0097", "properties": {"datetime": "2011-07-31T00:00:00Z", "eo:cloud_cover": 1}}, {"id": "pgstac-test-item-0063", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0013", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 2}}, {"id": "pgstac-test-item-0085", "properties": {"datetime": "2011-08-01T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0073", "properties": {"datetime": "2011-08-15T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0041", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0034", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0005", "properties": {"datetime": "2011-08-24T00:00:00Z", "eo:cloud_cover": 3}}, {"id": "pgstac-test-item-0048", "properties": {"datetime": "2011-08-16T00:00:00Z", "eo:cloud_cover": 4}}, {"id": "pgstac-test-item-0012", "properties": {"datetime": "2011-08-17T00:00:00Z", "eo:cloud_cover": 4}}]}'::jsonb - $$, - 'Test lt as a filter on a numeric field with order by' -); - -SELECT results_eq($$ - select s from search('{"filter":{"op":"in","args":[{"property":"id"},["pgstac-test-item-0097"]]},"fields":{"include":["id"]}}') s; - $$,$$ - select '{"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 1, "returned": 1}, "features": [{"id": "pgstac-test-item-0097"}]}'::jsonb - $$, - 'Test ids search single' -); - -SELECT results_eq($$ - select s from search('{"filter":{"op":"in","args":[{"property":"id"},["pgstac-test-item-0097","pgstac-test-item-0003"]]},"fields":{"include":["id"]}}') s; - $$,$$ - select '{"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 2, "returned": 2}, "features": [{"id": "pgstac-test-item-0003"},{"id": "pgstac-test-item-0097"}]}'::jsonb - $$, - 'Test ids search multi' -); - - -SELECT results_eq($$ - select s from search('{"filter":{"op":"in","args":[{"property":"collection"},["pgstac-test-collection"]]},"fields":{"include":["id"]}, "limit": 1}') s; - $$,$$ - select '{"next": "pgstac-test-item-0003", "prev": null, "type": "FeatureCollection", "context": {"limit": 1, "matched": 100, "returned": 1}, "features": [{"id": "pgstac-test-item-0003"}]}'::jsonb - $$, - 'Test collections search' -); - -SELECT results_eq($$ - select s from search('{"filter":{"op":"in","args":[{"property":"collection"},["nonexistent"]]}}') s; - $$,$$ - select '{"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "matched": 0, "returned": 0}, "features": []}'::jsonb - $$, - 'Test collections search with unknown collection' -); - -SELECT results_eq($$ - select s from search('{"filter":{"op":"in","args":[{"property":"collection"},["nonexistent"]]}, "conf":{"context":"off"}}') s; - $$,$$ - select '{"next": null, "prev": null, "type": "FeatureCollection", "context": {"limit": 10, "returned": 0}, "features": []}'::jsonb - $$, - 'Test using conf to turn off context' -); - - -SELECT results_eq($$ - select s from search('{"conf": {"nohydrate": true}, "limit": 2}') s; - $$,$$ - select '{"next": "pgstac-test-item-0002", "prev": null, "type": "FeatureCollection", "context": {"limit": 2, "matched": 100, "returned": 2}, "features": [{"id": "pgstac-test-item-0003", "bbox": [-85.379245, 30.933949, -85.308201, 31.003555], "assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008506_nw_16_1_20110825.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008506_nw_16_1_20110825.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008506_nw_16_1_20110825.200.jpg"}}, "geometry": {"type": "Polygon", "coordinates": [[[-85.309412, 30.933949], [-85.308201, 31.002658], [-85.378084, 31.003555], [-85.379245, 30.934843], [-85.309412, 30.933949]]]}, "collection": "pgstac-test-collection", "properties": {"gsd": 1, "datetime": "2011-08-25T00:00:00Z", "naip:year": "2011", "proj:bbox": [654842, 3423507, 661516, 3431125], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7618, 6674], "eo:cloud_cover": 28, "proj:transform": [1, 0, 654842, 0, -1, 3431125, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]}, {"id": "pgstac-test-item-0002", "bbox": [-85.504167,30.934008, -85.433293, 31.003486], "assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008505_nw_16_1_20110825.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008505_nw_16_1_20110825.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008505_nw_16_1_20110825.200.jpg"}}, "geometry": {"type": "Polygon", "coordinates": [[[-85.434414, 30.934008], [-85.433293, 31.002658], [-85.503096, 31.003486], [-85.504167, 30.934834], [-85.434414, 30.934008]]]}, "collection": "pgstac-test-collection", "properties": {"gsd": 1, "datetime": "2011-08-25T00:00:00Z", "naip:year": "2011", "proj:bbox": [642906, 3423339, 649572, 3430950], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7611, 6666], "eo:cloud_cover": 33, "proj:transform": [1, 0, 642906, 0, -1, 3430950, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]}]}'::jsonb - $$, - 'Test search using nohydrate conf.' -); /* template SELECT results_eq($$ diff --git a/test/pgtap/006_tilesearch.sql b/test/pgtap/006_tilesearch.sql index fc9e4602..da6614cb 100644 --- a/test/pgtap/006_tilesearch.sql +++ b/test/pgtap/006_tilesearch.sql @@ -1,44 +1,3 @@ SELECT has_function('pgstac'::name, 'geometrysearch', ARRAY['geometry','text','jsonb','int','int','interval','boolean','boolean']); SELECT has_function('pgstac'::name, 'geojsonsearch', ARRAY['jsonb','text','jsonb','int','int','interval','boolean','boolean']); SELECT has_function('pgstac'::name, 'xyzsearch', ARRAY['int','int','int','text','jsonb','int','int','interval','boolean','boolean']); - - -SELECT results_eq($$ - select s from xyzsearch(8615, 13418, 15, '2bbae9a0ef0bbb5ffaca06603ce621d7', '{"include":["id"]}'::jsonb) s; - $$,$$ - select '{"type": "FeatureCollection", "features": [{"id": "pgstac-test-item-0003"}]}'::jsonb - $$, - 'Test xyzsearch to return feature collection with the only intersecting item' -); - -SELECT results_eq($$ - select s from xyzsearch(1048, 1682, 12, '2bbae9a0ef0bbb5ffaca06603ce621d7', '{"include":["id"]}'::jsonb) s; - $$,$$ - select '{"type": "FeatureCollection", "features": [{"id": "pgstac-test-item-0050"}, {"id": "pgstac-test-item-0049"}, {"id": "pgstac-test-item-0048"}, {"id": "pgstac-test-item-0047"}, {"id": "pgstac-test-item-0100"}, {"id": "pgstac-test-item-0089"}]}'::jsonb - $$, - 'Test xyzsearch to return feature collection with all intersecting items' -); - -SELECT results_eq($$ - select s from xyzsearch(1048, 1682, 12, '2bbae9a0ef0bbb5ffaca06603ce621d7', '{"include":["id"]}'::jsonb, NULL, 1) s; - $$,$$ - select '{"type": "FeatureCollection", "features": [{"id": "pgstac-test-item-0050"}]}'::jsonb - $$, - 'Test xyzsearch to return feature collection with all intersecting items but limit to 1' -); - -SELECT results_eq($$ - select s from xyzsearch(16792, 26892, 16, '2bbae9a0ef0bbb5ffaca06603ce621d7', '{"include":["id"]}'::jsonb, exitwhenfull => true) s; - $$,$$ - select '{"type": "FeatureCollection", "features": [{"id": "pgstac-test-item-0098"}, {"id": "pgstac-test-item-0097"}]}'::jsonb - $$, - 'Test xyzsearch to return feature collection with all intersecting items but exits when tile is filled' -); - -SELECT results_eq($$ - select s from xyzsearch(16792, 26892, 16, '2bbae9a0ef0bbb5ffaca06603ce621d7', '{"include":["id"]}'::jsonb, exitwhenfull => false, skipcovered => false) s; - $$,$$ - select '{"type": "FeatureCollection", "features": [{"id": "pgstac-test-item-0098"}, {"id": "pgstac-test-item-0097"}, {"id": "pgstac-test-item-0091"}]}'::jsonb - $$, - 'Test xyzsearch to return feature collection with all intersecting items but exits continue even if tile is filled' -); diff --git a/test/testdata/items.pgcopy b/test/testdata/items.pgcopy new file mode 100644 index 00000000..3aae5028 --- /dev/null +++ b/test/testdata/items.pgcopy @@ -0,0 +1,100 @@ +pgstac-test-item-0003 0103000020E610000001000000050000005B3FFD67CD5355C0C4211B4817EF3E400CE6AF90B95355C0A112D731AE003F4004C93B87325855C0BEBC00FBE8003F40FA0AD28C455855C000E5EFDE51EF3E405B3FFD67CD5355C0C4211B4817EF3E40 pgstac-test-collection 2011-08-25 00:00:00+00 2011-08-25 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008506_nw_16_1_20110825.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008506_nw_16_1_20110825.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008506_nw_16_1_20110825.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-25T00:00:00Z", "naip:year": "2011", "proj:bbox": [654842, 3423507, 661516, 3431125], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7618, 6674], "eo:cloud_cover": 28, "proj:transform": [1, 0, 654842, 0, -1, 3431125, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0004 0103000020E610000001000000050000006364C91CCBC755C0DE76A1B94EEF3E4062307F85CCC755C002A08A1BB7003F403E7958A835CC55C0E75608ABB1003F40E695EB6D33CC55C0C32D1F4949EF3E406364C91CCBC755C0DE76A1B94EEF3E40 pgstac-test-collection 2011-08-24 00:00:00+00 2011-08-24 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008707_ne_16_1_20110824.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008707_ne_16_1_20110824.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008707_ne_16_1_20110824.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-24T00:00:00Z", "naip:year": "2011", "proj:bbox": [481788, 3422382, 488367, 3429918], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7536, 6579], "eo:cloud_cover": 23, "proj:transform": [1, 0, 481788, 0, -1, 3429918, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0005 0103000020E61000000100000005000000C1525DC0CBC355C03D7FDAA84EEF3E40D97A8670CCC355C0C7D5C8AEB4003F40AF06280D35C855C06478EC67B1003F402785798F33C855C0D921FE614BEF3E40C1525DC0CBC355C03D7FDAA84EEF3E40 pgstac-test-collection 2011-08-24 00:00:00+00 2011-08-24 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008708_nw_16_1_20110824.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008708_nw_16_1_20110824.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008708_nw_16_1_20110824.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-24T00:00:00Z", "naip:year": "2011", "proj:bbox": [487758, 3422377, 494334, 3429909], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7532, 6576], "eo:cloud_cover": 3, "proj:transform": [1, 0, 487758, 0, -1, 3429909, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0006 0103000020E61000000100000005000000F20A444FCACB55C03D7FDAA84EEF3E40C138B874CCCB55C054C6BFCFB8003F40DE567A6D36D055C0E199D024B1003F409ECF807A33D055C0CA52EBFD46EF3E40F20A444FCACB55C03D7FDAA84EEF3E40 pgstac-test-collection 2011-08-24 00:00:00+00 2011-08-24 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008707_nw_16_1_20110824.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008707_nw_16_1_20110824.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008707_nw_16_1_20110824.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-24T00:00:00Z", "naip:year": "2011", "proj:bbox": [475817, 3422390, 482401, 3429929], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7539, 6584], "eo:cloud_cover": 100, "proj:transform": [1, 0, 475817, 0, -1, 3429929, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0007 0103000020E61000000100000005000000CF68AB92C8D755C01F662FDB4E9F3E40850662D9CCD755C0F8AA9509BFB03E405A2A6F4738DC55C05EBBB4E1B0B03E401BF1643733DC55C086764EB3409F3E40CF68AB92C8D755C01F662FDB4E9F3E40 pgstac-test-collection 2011-08-17 00:00:00+00 2011-08-17 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008721_se_16_1_20110817.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008721_se_16_1_20110817.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008721_se_16_1_20110817.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-17T00:00:00Z", "naip:year": "2011", "proj:bbox": [457770, 3387803, 464384, 3395352], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7549, 6614], "eo:cloud_cover": 59, "proj:transform": [1, 0, 457770, 0, -1, 3395352, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0008 0103000020E61000000100000005000000CF8250DEC7DB55C0433C122F4F9F3E406EC493DDCCDB55C00E9F7422C1B03E405A10CAFB38E055C0BDC3EDD0B0B03E404B75012F33E055C0F2608BDD3E9F3E40CF8250DEC7DB55C0433C122F4F9F3E40 pgstac-test-collection 2011-08-17 00:00:00+00 2011-08-17 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008721_sw_16_1_20110817.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008721_sw_16_1_20110817.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008721_sw_16_1_20110817.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-17T00:00:00Z", "naip:year": "2011", "proj:bbox": [451780, 3387825, 458398, 3395377], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7552, 6618], "eo:cloud_cover": 64, "proj:transform": [1, 0, 451780, 0, -1, 3395377, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0009 0103000020E61000000100000005000000FF06EDD5C7DB55C06155BDFC4EAF3E409D4830D5CCDB55C02CB81FF0C0C03E405A10CAFB38E055C03BE5D18DB0C03E403333333333E055C0107A36AB3EAF3E40FF06EDD5C7DB55C06155BDFC4EAF3E40 pgstac-test-collection 2011-08-17 00:00:00+00 2011-08-17 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008721_nw_16_1_20110817.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008721_nw_16_1_20110817.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008721_nw_16_1_20110817.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-17T00:00:00Z", "naip:year": "2011", "proj:bbox": [451811, 3394751, 458425, 3402303], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7552, 6614], "eo:cloud_cover": 61, "proj:transform": [1, 0, 451811, 0, -1, 3402303, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0010 0103000020E61000000100000005000000D53F8864C8D755C03D7FDAA84EAF3E408BDD3EABCCD755C015C440D7BEC03E402BA6D24F38DC55C07CD45FAFB0C03E40EC6CC83F33DC55C04487C09140AF3E40D53F8864C8D755C03D7FDAA84EAF3E40 pgstac-test-collection 2011-08-17 00:00:00+00 2011-08-17 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008721_ne_16_1_20110817.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008721_ne_16_1_20110817.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008721_ne_16_1_20110817.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-17T00:00:00Z", "naip:year": "2011", "proj:bbox": [457797, 3394729, 464408, 3402278], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7549, 6611], "eo:cloud_cover": 31, "proj:transform": [1, 0, 457797, 0, -1, 3402278, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0011 0103000020E61000000100000005000000931CB0ABC9CF55C0C05DF6EB4EEF3E404AEEB089CCCF55C0CAC2D7D7BA003F406DC9AA0837D455C0FFB27BF2B0003F40459E245D33D455C09545611745EF3E40931CB0ABC9CF55C0C05DF6EB4EEF3E40 pgstac-test-collection 2011-08-17 00:00:00+00 2011-08-17 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008706_ne_16_1_20110817.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008706_ne_16_1_20110817.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008706_ne_16_1_20110817.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-17T00:00:00Z", "naip:year": "2011", "proj:bbox": [469847, 3422402, 476434, 3429944], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7542, 6587], "eo:cloud_cover": 41, "proj:transform": [1, 0, 469847, 0, -1, 3429944, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0012 0103000020E61000000100000005000000C3D4963AC8D755C01F662FDB4EEF3E4032ACE28DCCD755C0BC783F6EBF003F40B45BCB6438DC55C082919735B1003F40D42AFA4333DC55C0E57E87A240EF3E40C3D4963AC8D755C01F662FDB4EEF3E40 pgstac-test-collection 2011-08-17 00:00:00+00 2011-08-17 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008705_ne_16_1_20110817.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008705_ne_16_1_20110817.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008705_ne_16_1_20110817.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-17T00:00:00Z", "naip:year": "2011", "proj:bbox": [457906, 3422435, 464501, 3429985], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7550, 6595], "eo:cloud_cover": 4, "proj:transform": [1, 0, 457906, 0, -1, 3429985, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0013 0103000020E61000000100000005000000342E1C08C9D355C06155BDFC4EEF3E40BB61DBA2CCD355C06495D233BD003F400DA7CCCD37D855C082919735B1003F40151A886533D855C0DE59BBED42EF3E40342E1C08C9D355C06155BDFC4EEF3E40 pgstac-test-collection 2011-08-17 00:00:00+00 2011-08-17 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008706_nw_16_1_20110817.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008706_nw_16_1_20110817.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008706_nw_16_1_20110817.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-17T00:00:00Z", "naip:year": "2011", "proj:bbox": [463876, 3422417, 470467, 3429963], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7546, 6591], "eo:cloud_cover": 2, "proj:transform": [1, 0, 463876, 0, -1, 3429963, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0014 0103000020E6100000010000000500000089F02F82C6E355C06155BDFC4E7F3E4026FE28EACCE355C0B9A81611C5903E40EE3F321D3AE855C0F9F5436CB0903E40C896E5EB32E855C0A1A2EA573A7F3E4089F02F82C6E355C06155BDFC4E7F3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008728_sw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008728_sw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008728_sw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [439724, 3374025, 446357, 3381584], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7559, 6633], "eo:cloud_cover": 17, "proj:transform": [1, 0, 439724, 0, -1, 3381584, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0015 0103000020E61000000100000005000000D97A8670CCB755C0D921FE614BEF3E4051F9D7F2CAB755C06478EC67B1003F402785798F33BC55C0C7D5C8AEB4003F403FADA23F34BC55C03D7FDAA84EEF3E40D97A8670CCB755C0D921FE614BEF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008601_ne_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30086/m_3008601_ne_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008601_ne_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [505666, 3422377, 512242, 3429909], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7532, 6576], "eo:cloud_cover": 54, "proj:transform": [1, 0, 505666, 0, -1, 3429909, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0016 0103000020E6100000010000000500000091B41B7DCCBB55C00E2F88484DEF3E40D9942BBCCBBB55C0A5677A89B1003F40868DB27E33C055C051D9B0A6B2003F40CE531D7233C055C019A9F7544EEF3E4091B41B7DCCBB55C00E2F88484DEF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008601_nw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30086/m_3008601_nw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008601_nw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [499700, 3422375, 506271, 3429904], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7529, 6571], "eo:cloud_cover": 13, "proj:transform": [1, 0, 499700, 0, -1, 3429904, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0017 0103000020E6100000010000000500000062307F85CCAF55C0CA52EBFD46EF3E4022A98592C9AF55C0E199D024B1003F403FC7478B33B455C054C6BFCFB8003F400EF5BBB035B455C03D7FDAA84EEF3E4062307F85CCAF55C0CA52EBFD46EF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008602_ne_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30086/m_3008602_ne_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008602_ne_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [517599, 3422390, 524183, 3429929], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7539, 6584], "eo:cloud_cover": 59, "proj:transform": [1, 0, 517599, 0, -1, 3429929, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0018 0103000020E610000001000000050000001A6A1492CCB355C0C32D1F4949EF3E40C286A757CAB355C0E75608ABB1003F409ECF807A33B855C002A08A1BB7003F409D9B36E334B855C0DE76A1B94EEF3E401A6A1492CCB355C0C32D1F4949EF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008602_nw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30086/m_3008602_nw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008602_nw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [511633, 3422382, 518212, 3429918], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7536, 6579], "eo:cloud_cover": 29, "proj:transform": [1, 0, 511633, 0, -1, 3429918, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0019 0103000020E610000001000000050000001D3A3DEFC6DF55C0433C122F4FEF3E404417D4B7CCDF55C02593533BC3003F400C59DDEA39E455C03BE5D18DB0003F407522C15433E455C0F98557923CEF3E401D3A3DEFC6DF55C0433C122F4FEF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008704_ne_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008704_ne_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008704_ne_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [445964, 3422482, 452567, 3430038], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7556, 6603], "eo:cloud_cover": 52, "proj:transform": [1, 0, 445964, 0, -1, 3430038, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0020 0103000020E61000000100000005000000D68D7747C6E355C0070ABC934FEF3E40E50E9BC8CCE355C0DC7EF964C5003F40CA4FAA7D3AE855C03BE5D18DB0003F4063B7CF2A33E855C006685BCD3AEF3E40D68D7747C6E355C0070ABC934FEF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008704_nw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008704_nw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008704_nw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [439994, 3422511, 446600, 3430070], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7559, 6606], "eo:cloud_cover": 39, "proj:transform": [1, 0, 439994, 0, -1, 3430070, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0021 0103000020E610000001000000050000008FC70C54C6E355C0252367614FDF3E409D4830D5CCE355C0FA97A432C5F03E40E29178793AE855C058FE7C5BB0F03E4063B7CF2A33E855C02481069B3ADF3E408FC70C54C6E355C0252367614FDF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008704_sw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008704_sw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008704_sw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [439955, 3415584, 446565, 3423143], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7559, 6610], "eo:cloud_cover": 29, "proj:transform": [1, 0, 439955, 0, -1, 3423143, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0022 0103000020E610000001000000050000007C28D192C7DB55C0A2444B1E4FEF3E40BB61DBA2CCDB55C032755776C1003F406C7BBB2539E055C09FAA4203B1003F40A4A65D4C33E055C0107A36AB3EEF3E407C28D192C7DB55C0A2444B1E4FEF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008705_nw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008705_nw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008705_nw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [451935, 3422457, 458534, 3430010], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7553, 6599], "eo:cloud_cover": 84, "proj:transform": [1, 0, 451935, 0, -1, 3430010, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0023 0103000020E6100000010000000500000032ACE28DCCBF55C019A9F7544EEF3E407A724D81CCBF55C051D9B0A6B2003F40276BD44334C455C0A5677A89B1003F406F4BE48233C455C00E2F88484DEF3E4032ACE28DCCBF55C019A9F7544EEF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008708_ne_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008708_ne_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008708_ne_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [493729, 3422375, 500300, 3429904], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7529, 6571], "eo:cloud_cover": 56, "proj:transform": [1, 0, 493729, 0, -1, 3429904, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0024 0103000020E6100000010000000500000001BD70E7C2F755C048F949B54FCF3E407F2F8507CDF755C0EF3A1BF2CFE03E40E6E61BD13DFC55C0D61F6118B0E03E40105D50DF32FC55C0D0D556EC2FCF3E4001BD70E7C2F755C048F949B54FCF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008709_ne_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008709_ne_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008709_ne_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [410024, 3408849, 416657, 3416426], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7577, 6633], "eo:cloud_cover": 58, "proj:transform": [1, 0, 410024, 0, -1, 3416426, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0025 0103000020E61000000100000005000000A9A5B915C2FB55C0ADBEBA2A50CF3E40F6798CF2CCFB55C0A626C11BD2E03E40B648DA8D3E0056C035289A07B0E03E40F81A82E3320056C07DAF21382ECF3E40A9A5B915C2FB55C0ADBEBA2A50CF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008709_nw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008709_nw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008709_nw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [404045, 3408898, 410683, 3416478], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7580, 6638], "eo:cloud_cover": 65, "proj:transform": [1, 0, 404045, 0, -1, 3416478, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0026 0103000020E61000000100000005000000EA7AA2EBC2F755C0ADBEBA2A50BF3E407F2F8507CDF755C0AD4B8DD0CFD03E4046EF54C03DFC55C09430D3F6AFD03E403FE1ECD632FC55C0359BC76130BF3E40EA7AA2EBC2F755C0ADBEBA2A50BF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008709_se_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008709_se_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008709_se_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [409966, 3401923, 416603, 3409499], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7576, 6637], "eo:cloud_cover": 52, "proj:transform": [1, 0, 409966, 0, -1, 3409499, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0027 0103000020E6100000010000000500000061DF4E22C2FB55C06CCF2C0950BF3E40DF37BEF6CCFB55C0653733FAD1D03E40E6CC76853E0056C09430D3F6AFD03E40F81A82E3320056C03CC093162EBF3E4061DF4E22C2FB55C06CCF2C0950BF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008709_sw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008709_sw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008709_sw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [403983, 3401971, 410625, 3409551], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7580, 6642], "eo:cloud_cover": 43, "proj:transform": [1, 0, 403983, 0, -1, 3409551, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0028 0103000020E6100000010000000500000060AB048BC3F355C0A80183A44FCF3E40F6798CF2CCF355C055682096CDE03E4040321D3A3DF855C0F3380CE6AFE03E40390A100533F855C087C1FC1532CF3E4060AB048BC3F355C0A80183A44FCF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008710_nw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008710_nw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008710_nw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [416002, 3408804, 422632, 3416377], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7573, 6630], "eo:cloud_cover": 46, "proj:transform": [1, 0, 416002, 0, -1, 3416377, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0029 0103000020E6100000010000000500000060AB048BC3F355C06CCF2C0950BF3E4026FE28EACCF355C07381CB63CDD03E40B77C24253DF855C0B2497EC4AFD03E4081D07AF832F855C04B8FA67A32BF3E4060AB048BC3F355C06CCF2C0950BF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008710_sw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008710_sw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008710_sw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [415948, 3401878, 422582, 3409450], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7572, 6634], "eo:cloud_cover": 42, "proj:transform": [1, 0, 415948, 0, -1, 3409450, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0030 0103000020E61000000100000005000000A723809BC5E755C0252367614FCF3E40B58AFED0CCE755C0946A9F8EC7E03E407104A9143BEC55C03BE5D18DB0E03E40F243A51133EC55C06C95607138CF3E40A723809BC5E755C0252367614FCF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008711_ne_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008711_ne_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008711_ne_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [433938, 3408689, 440556, 3416252], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7563, 6618], "eo:cloud_cover": 16, "proj:transform": [1, 0, 433938, 0, -1, 3416252, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0031 0103000020E610000001000000050000001897AAB4C5E755C048F949B54FBF3E405682C5E1CCE755C0B2834A5CC7D03E4041800C1D3BEC55C058FE7C5BB0D03E40AA7D3A1E33EC55C0906B43C538BF3E401897AAB4C5E755C048F949B54FBF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008711_se_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008711_se_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008711_se_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [433895, 3401763, 440517, 3409325], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7562, 6622], "eo:cloud_cover": 16, "proj:transform": [1, 0, 433895, 0, -1, 3409325, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0032 0103000020E610000001000000050000003596B036C6E355C0A2444B1E4FCF3E405C59A2B3CCE355C0BF654E97C5E03E40E29178793AE855C01DCC26C0B0E03E404B75012F33E855C0A1A2EA573ACF3E403596B036C6E355C0A2444B1E4FCF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008712_nw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008712_nw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008712_nw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [439916, 3408657, 446531, 3416217], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7560, 6615], "eo:cloud_cover": 7, "proj:transform": [1, 0, 439916, 0, -1, 3416217, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0033 0103000020E61000000100000005000000766B990CC7DF55C0C05DF6EB4EBF3E40E50E9BC8CCDF55C0E4A3C519C3D03E40897AC1A739E455C09AED0A7DB0D03E40AA7D3A1E33E455C0179F02603CBF3E40766B990CC7DF55C0C05DF6EB4EBF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008712_se_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008712_se_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008712_se_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [445860, 3401702, 452474, 3409258], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7556, 6614], "eo:cloud_cover": 49, "proj:transform": [1, 0, 445860, 0, -1, 3409258, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0034 0103000020E61000000100000005000000BE4BA94BC6E355C0252367614FBF3E40149337C0CCE355C03C873254C5D03E40D026874F3AE855C03BE5D18DB0D03E4021C8410933E855C0C478CDAB3ABF3E40BE4BA94BC6E355C0252367614FBF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008712_sw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008712_sw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008712_sw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [439878, 3401731, 446496, 3409290], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7559, 6618], "eo:cloud_cover": 3, "proj:transform": [1, 0, 439878, 0, -1, 3409290, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0035 0103000020E61000000100000005000000BAF605F4C2F755C02AE09EE74FAF3E407F2F8507CDF755C0CB64389ECFC03E408DB5BFB33DFC55C0F3380CE6AFC03E405723BBD232FC55C0F3AB394030AF3E40BAF605F4C2F755C02AE09EE74FAF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008717_ne_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008717_ne_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008717_ne_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [409908, 3394996, 416549, 3402572], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7576, 6641], "eo:cloud_cover": 33, "proj:transform": [1, 0, 409908, 0, -1, 3402572, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0036 0103000020E610000001000000050000001A19E42EC2FB55C02AE09EE74FAF3E40C7F5EFFACCFB55C02448A5D8D1C03E401651137D3E0056C0F3380CE6AFC03E40F81A82E3320056C0FAD005F52DAF3E401A19E42EC2FB55C02AE09EE74FAF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008717_nw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008717_nw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008717_nw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [403921, 3395044, 410567, 3402624], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7580, 6646], "eo:cloud_cover": 12, "proj:transform": [1, 0, 403921, 0, -1, 3402624, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0037 0103000020E6100000010000000500000073309B00C3F755C048F949B54F9F3E4050ABE80FCDF755C0E97DE36BCFB03E40A5F78DAF3DFC55C01152B7B3AFB03E403FE1ECD632FC55C011C5E40D309F3E4073309B00C3F755C048F949B54F9F3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008717_se_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008717_se_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008717_se_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [409850, 3388069, 416495, 3395645], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7576, 6645], "eo:cloud_cover": 54, "proj:transform": [1, 0, 409850, 0, -1, 3395645, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0038 0103000020E61000000100000005000000A3CEDC43C2FB55C04EB6813B509F3E407F2F8507CDFB55C0E25817B7D1B03E4004E621533E0056C0B2497EC4AFB03E40B62BF4C1320056C05F96766A2E9F3E40A3CEDC43C2FB55C04EB6813B509F3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008717_sw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008717_sw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008717_sw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [403860, 3388118, 410509, 3395697], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7579, 6649], "eo:cloud_cover": 77, "proj:transform": [1, 0, 403860, 0, -1, 3395697, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0039 0103000020E610000001000000050000005AD427B9C3F355C0E9F010C64FAF3E4038691A14CDF355C0374F75C8CDC03E4016855D143DF855C0170FEF39B0C03E40B05417F032F855C069A8514832AF3E405AD427B9C3F355C0E9F010C64FAF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008718_nw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008718_nw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008718_nw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [415894, 3394951, 422531, 3402524], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7573, 6637], "eo:cloud_cover": 12, "proj:transform": [1, 0, 415894, 0, -1, 3402524, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0040 0103000020E610000001000000050000002B508BC1C3F355C06612F5824F9F3E4038691A14CDF355C055682096CDB03E405E4BC8073DF855C035289A07B0B03E40E0D8B3E732F855C087C1FC15329F3E402B508BC1C3F355C06612F5824F9F3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008718_sw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008718_sw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008718_sw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [415840, 3388024, 422481, 3395597], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7573, 6641], "eo:cloud_cover": 10, "proj:transform": [1, 0, 415840, 0, -1, 3395597, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0041 0103000020E61000000100000005000000779FE3A3C5E755C0252367614FAF3E40CDCCCCCCCCE755C0D5592DB0C7C03E4000917EFB3AEC55C07CD45FAFB0C03E40514CDE0033EC55C00D8D278238AF3E40779FE3A3C5E755C0252367614FAF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008719_ne_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008719_ne_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008719_ne_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [433853, 3394836, 440479, 3402399], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7563, 6626], "eo:cloud_cover": 3, "proj:transform": [1, 0, 433853, 0, -1, 3402399, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0042 0103000020E61000000100000005000000D0D03FC1C5E755C0A80183A44F9F3E4026FE28EACCE755C0B2834A5CC7B03E40B8CA13083BEC55C0F9F5436CB0B03E40DA01D71533EC55C0906B43C5389F3E40D0D03FC1C5E755C0A80183A44F9F3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008719_se_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008719_se_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008719_se_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [433810, 3387910, 440440, 3395472], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7562, 6630], "eo:cloud_cover": 99, "proj:transform": [1, 0, 433810, 0, -1, 3395472, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0043 0103000020E610000001000000050000002FA52E19C7DF55C0A2444B1E4FAF3E409D4830D5CCDF55C06682E15CC3C03E40897AC1A739E455C0BDC3EDD0B0C03E40923B6C2233E455C09A7D1EA33CAF3E402FA52E19C7DF55C0A2444B1E4FAF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008720_ne_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008720_ne_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008720_ne_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [445825, 3394776, 452443, 3402332], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7556, 6618], "eo:cloud_cover": 80, "proj:transform": [1, 0, 445825, 0, -1, 3402332, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0044 0103000020E610000001000000050000004701A260C6E355C0014D840D4FAF3E40B58AFED0CCE355C0BF654E97C5C03E40B8E4B8533AE855C05EBBB4E1B0C03E40F243A51133E855C0A1A2EA573AAF3E404701A260C6E355C0014D840D4FAF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008720_nw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008720_nw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008720_nw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [439839, 3394804, 446461, 3402364], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7560, 6622], "eo:cloud_cover": 30, "proj:transform": [1, 0, 439839, 0, -1, 3402364, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0045 0103000020E61000000100000005000000B85A272EC7DF55C0842BA0504F9F3E405682C5E1CCDF55C043ACFE08C3B03E407138F3AB39E455C09AED0A7DB0B03E4063B7CF2A33E455C07C6473D53C9F3E40B85A272EC7DF55C0842BA0504F9F3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008720_se_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008720_se_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008720_se_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [445790, 3387850, 452412, 3395405], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7555, 6622], "eo:cloud_cover": 82, "proj:transform": [1, 0, 445790, 0, -1, 3395405, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0046 0103000020E61000000100000005000000D0B69A75C6E355C0E333D93F4F9F3E403E40F7E5CCE355C09B8F6B43C5B03E4089601C5C3AE855C03BE5D18DB0B03E40AA7D3A1E33E855C02481069B3A9F3E40D0B69A75C6E355C0E333D93F4F9F3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008720_sw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008720_sw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008720_sw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [439800, 3387878, 446426, 3395437], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7559, 6626], "eo:cloud_cover": 16, "proj:transform": [1, 0, 439800, 0, -1, 3395437, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0047 0103000020E61000000100000005000000FBE59315C3F755C0CBD765F84F8F3E4020274C18CDF755C0AD4B8DD0CFA03E40A5F78DAF3DFC55C0D61F6118B0A03E40279F1EDB32FC55C0359BC761308F3E40FBE59315C3F755C0CBD765F84F8F3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008725_ne_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008725_ne_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008725_ne_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [409792, 3381143, 416441, 3388719], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7576, 6649], "eo:cloud_cover": 43, "proj:transform": [1, 0, 409792, 0, -1, 3388719, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0048 0103000020E61000000100000005000000849B8C2AC3F755C0A80183A44F7F3E40C11E1329CDF755C08A75AA7CCF903E40938C9C853DFC55C0534145D5AF903E40E6AF90B932FC55C0B2BCAB1E307F3E40849B8C2AC3F755C0A80183A44F7F3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008725_se_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008725_se_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008725_se_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [409735, 3374216, 416387, 3381792], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7576, 6652], "eo:cloud_cover": 4, "proj:transform": [1, 0, 409735, 0, -1, 3381792, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0049 0103000020E61000000100000005000000FBCBEEC9C3F355C0E9F010C64F8F3E4020274C18CDF355C032923D42CDA03E408DCF64FF3CF855C0B2497EC4AFA03E40E0D8B3E732F855C00AA01859328F3E40FBCBEEC9C3F355C0E9F010C64F8F3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008726_nw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008726_nw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008726_nw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [415786, 3381098, 422431, 3388670], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7572, 6645], "eo:cloud_cover": 9, "proj:transform": [1, 0, 415786, 0, -1, 3388670, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0050 0103000020E61000000100000005000000B40584D6C3F355C06CCF2C09507F3E40F0A2AF20CDF355C055682096CD903E408DCF64FF3CF855C0D61F6118B0903E40C896E5EB32F855C02E76FBAC327F3E40B40584D6C3F355C06CCF2C09507F3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008726_sw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008726_sw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008726_sw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [415732, 3374172, 422381, 3381744], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7572, 6649], "eo:cloud_cover": 84, "proj:transform": [1, 0, 415732, 0, -1, 3381744, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0051 0103000020E6100000010000000500000012C0CDE2C5E755C0E333D93F4F8F3E407F2F8507CDE755C0946A9F8EC7A03E404757E9EE3AEC55C07CD45FAFB0A03E40514CDE0033EC55C0CB9D9960388F3E4012C0CDE2C5E755C0E333D93F4F8F3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008727_ne_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008727_ne_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008727_ne_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [433768, 3380983, 440401, 3388546], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7563, 6633], "eo:cloud_cover": 22, "proj:transform": [1, 0, 433768, 0, -1, 3388546, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0052 0103000020E6100000010000000500000041446ADAC5E755C0252367614F7F3E40C7F5EFFACCE755C0D09CF529C7903E40D6E3BED53AEC55C058FE7C5BB0903E40C896E5EB32EC55C0AD84EE92387F3E4041446ADAC5E755C0252367614F7F3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008727_se_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008727_se_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008727_se_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [433726, 3374057, 440363, 3381619], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7562, 6637], "eo:cloud_cover": 12, "proj:transform": [1, 0, 433726, 0, -1, 3381619, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0053 0103000020E610000001000000050000005952EE3EC7DF55C01F662FDB4E8F3E40F6798CF2CCDF55C02593533BC3A03E4060CD018239E455C07CD45FAFB0A03E40390A100533E455C0B796C9703C8F3E405952EE3EC7DF55C01F662FDB4E8F3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008728_ne_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008728_ne_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008728_ne_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [445756, 3380923, 452381, 3388479], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7556, 6625], "eo:cloud_cover": 24, "proj:transform": [1, 0, 445756, 0, -1, 3388479, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0054 0103000020E6100000010000000500000029E8F692C6E355C0C51A2E724F8F3E40C7F5EFFACCE355C01E6E8786C5A03E4047718E3A3AE855C0BDC3EDD0B0A03E40390A100533E855C006685BCD3A8F3E4029E8F692C6E355C0C51A2E724F8F3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008728_nw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008728_nw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008728_nw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [439762, 3380952, 446391, 3388511], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7559, 6629], "eo:cloud_cover": 5, "proj:transform": [1, 0, 439762, 0, -1, 3388511, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0055 0103000020E61000000100000005000000D09CF529C7DF55C06155BDFC4E7F3E40850662D9CCDF55C06682E15CC3903E403049658A39E455C05EBBB4E1B0903E40F243A51133E455C0F98557923C7F3E40D09CF529C7DF55C06155BDFC4E7F3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008728_se_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008728_se_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008728_se_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [445721, 3373997, 452351, 3381553], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7556, 6630], "eo:cloud_cover": 79, "proj:transform": [1, 0, 445721, 0, -1, 3381553, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0056 0103000020E61000000100000005000000D80FB1C1C2F755C02AE09EE74FEF3E400EBC5AEECCF755C0302AA913D0003F40F8510DFB3DFC55C076172829B0003F40514CDE0033FC55C011C5E40D30EF3E40D80FB1C1C2F755C02AE09EE74FEF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008701_ne_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008701_ne_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008701_ne_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [410140, 3422703, 416766, 3430280], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7577, 6626], "eo:cloud_cover": 62, "proj:transform": [1, 0, 410140, 0, -1, 3430280, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0057 0103000020E61000000100000005000000C0E78711C2FB55C08FA50F5D50EF3E40C7F5EFFACCFB55C0471E882CD2003F40E0F599B33E0056C035289A07B0003F4081D07AF8320056C01EA7E8482EEF3E40C0E78711C2FB55C08FA50F5D50EF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008701_nw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008701_nw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008701_nw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [404169, 3422752, 410799, 3430332], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7580, 6630], "eo:cloud_cover": 7, "proj:transform": [1, 0, 404169, 0, -1, 3430332, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0058 0103000020E61000000100000005000000F0517FBDC2F755C08AE8D7D64FDF3E405682C5E1CCF755C09032E202D0F03E4087DEE2E13DFC55C0D61F6118B0F03E40B05417F032FC55C011C5E40D30DF3E40F0517FBDC2F755C08AE8D7D64FDF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008701_se_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008701_se_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008701_se_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [410082, 3415776, 416712, 3423353], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7577, 6630], "eo:cloud_cover": 53, "proj:transform": [1, 0, 410082, 0, -1, 3423353, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0059 0103000020E61000000100000005000000C0E78711C2FB55C0EFAD484C50DF3E40F6798CF2CCFB55C0471E882CD2F03E405740A19E3E0056C035289A07B0F03E40C896E5EB320056C01EA7E8482EDF3E40C0E78711C2FB55C0EFAD484C50DF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008701_sw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008701_sw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008701_sw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [404107, 3415825, 410741, 3423405], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7580, 6634], "eo:cloud_cover": 57, "proj:transform": [1, 0, 404107, 0, -1, 3423405, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0066 0103000020E610000001000000050000009D4830D5CC9755C006685BCD3AEF3E4036B05582C59755C03BE5D18DB0003F401BF16437339C55C0DC7EF964C5003F402A7288B8399C55C0070ABC934FEF3E409D4830D5CC9755C006685BCD3AEF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008605_ne_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30086/m_3008605_ne_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008605_ne_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [553400, 3422511, 560006, 3430070], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7559, 6606], "eo:cloud_cover": 86, "proj:transform": [1, 0, 553400, 0, -1, 3430070, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0060 0103000020E61000000100000005000000EF37DA71C3F355C02AE09EE74FEF3E405682C5E1CCF355C0374F75C8CD003F4010AE80423DF855C035289A07B0003F40390A100533F855C069A8514832EF3E40EF37DA71C3F355C02AE09EE74FEF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008702_nw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008702_nw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008702_nw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [416111, 3422658, 422733, 3430231], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7573, 6622], "eo:cloud_cover": 70, "proj:transform": [1, 0, 416111, 0, -1, 3430231, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0061 0103000020E6100000010000000500000037FE4465C3F355C0E9F010C64FDF3E40B58AFED0CCF355C0F65FE7A6CDF03E40B1A547533DF855C035289A07B0F03E40C2BF081A33F855C0C9B08A3732DF3E4037FE4465C3F355C0E9F010C64FDF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008702_sw_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008702_sw_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008702_sw_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [416056, 3415731, 422683, 3423304], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7573, 6627], "eo:cloud_cover": 94, "proj:transform": [1, 0, 416056, 0, -1, 3423304, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0062 0103000020E6100000010000000500000095B88E71C5E755C0E333D93F4FEF3E40749B70AFCCE755C0B2834A5CC7003F40826F9A3E3BEC55C0B806B64AB0003F404B75012F33EC55C08AAE0B3F38EF3E4095B88E71C5E755C0E333D93F4FEF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008703_ne_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008703_ne_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008703_ne_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [434023, 3422542, 440634, 3430105], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7563, 6611], "eo:cloud_cover": 85, "proj:transform": [1, 0, 434023, 0, -1, 3430105, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0063 0103000020E610000001000000050000001E6E8786C5E755C0A80183A44FDF3E40149337C0CCE755C0D09CF529C7F03E40826F9A3E3BEC55C076172829B0F03E401BF1643733EC55C04E7CB5A338DF3E401E6E8786C5E755C0A80183A44FDF3E40 pgstac-test-collection 2011-08-16 00:00:00+00 2011-08-16 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008703_se_16_1_20110816.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008703_se_16_1_20110816.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008703_se_16_1_20110816.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-16T00:00:00Z", "naip:year": "2011", "proj:bbox": [433980, 3415616, 440595, 3423178], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7562, 6615], "eo:cloud_cover": 2, "proj:transform": [1, 0, 433980, 0, -1, 3423178, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0064 0103000020E610000001000000050000005C59A2B3CC9F55C0107A36AB3EEF3E40948444DAC69F55C09FAA4203B1003F40459E245D33A455C032755776C1003F4084D72E6D38A455C0A2444B1E4FEF3E405C59A2B3CC9F55C0107A36AB3EEF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008604_ne_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30086/m_3008604_ne_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008604_ne_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [541466, 3422457, 548065, 3430010], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7553, 6599], "eo:cloud_cover": 40, "proj:transform": [1, 0, 541466, 0, -1, 3430010, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0065 0103000020E610000001000000050000002CD505BCCCA355C0E57E87A240EF3E404CA4349BC7A355C082919735B1003F40CE531D7233A855C0BC783F6EBF003F403D2B69C537A855C01F662FDB4EEF3E402CD505BCCCA355C0E57E87A240EF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008604_nw_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30086/m_3008604_nw_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008604_nw_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [535499, 3422435, 542094, 3429985], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7550, 6595], "eo:cloud_cover": 27, "proj:transform": [1, 0, 535499, 0, -1, 3429985, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0067 0103000020E610000001000000050000008BDD3EABCC9B55C0F98557923CEF3E40F4A62215C69B55C03BE5D18DB0003F40BCE82B4833A055C02593533BC3003F40E3C5C21039A055C0433C122F4FEF3E408BDD3EABCC9B55C0F98557923CEF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008605_nw_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30086/m_3008605_nw_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008605_nw_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [547433, 3422482, 554036, 3430038], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7556, 6603], "eo:cloud_cover": 35, "proj:transform": [1, 0, 547433, 0, -1, 3430038, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0068 0103000020E61000000100000005000000CDCCCCCCCC8F55C0D2C2651536EF3E40AE2EA704C48F55C0F9F5436CB0003F4004AF963B339455C04B5645B8C9003F40B2F336363B9455C0E333D93F4FEF3E40CDCCCCCCCC8F55C0D2C2651536EF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008606_ne_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30086/m_3008606_ne_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008606_ne_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [565333, 3422577, 571948, 3430144], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7567, 6615], "eo:cloud_cover": 22, "proj:transform": [1, 0, 565333, 0, -1, 3430144, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0069 0103000020E61000000100000005000000B58AFED0CC9355C08AAE0B3F38EF3E407E9065C1C49355C0B806B64AB0003F408C648F50339855C0B2834A5CC7003F406B47718E3A9855C0E333D93F4FEF3E40B58AFED0CC9355C08AAE0B3F38EF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008606_nw_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30086/m_3008606_nw_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008606_nw_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [559366, 3422542, 565977, 3430105], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7563, 6611], "eo:cloud_cover": 97, "proj:transform": [1, 0, 559366, 0, -1, 3430105, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0070 0103000020E61000000100000005000000C7F5EFFACC8755C069A8514832EF3E40F0517FBDC28755C035289A07B0003F40AA7D3A1E338C55C0374F75C8CD003F4011C8258E3C8C55C02AE09EE74FEF3E40C7F5EFFACC8755C069A8514832EF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008607_ne_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30086/m_3008607_ne_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008607_ne_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [577267, 3422658, 583889, 3430231], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7573, 6622], "eo:cloud_cover": 32, "proj:transform": [1, 0, 577267, 0, -1, 3430231, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0071 0103000020E61000000100000005000000C7F5EFFACC8B55C03FADA23F34EF3E40D8F50B76C38B55C058FE7C5BB0003F4063B7CF2A339055C0624A24D1CB003F40E15D2EE23B9055C048F949B54FEF3E40C7F5EFFACC8B55C03FADA23F34EF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008607_nw_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30086/m_3008607_nw_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008607_nw_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [571300, 3422616, 577918, 3430186], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7570, 6618], "eo:cloud_cover": 68, "proj:transform": [1, 0, 571300, 0, -1, 3430186, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0072 0103000020E610000001000000050000007F2F8507CD7F55C01EA7E8482EEF3E40200A664CC17F55C035289A07B0003F40390A1005338455C0471E882CD2003F40401878EE3D8455C08FA50F5D50EF3E407F2F8507CD7F55C01EA7E8482EEF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008608_ne_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30086/m_3008608_ne_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008608_ne_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [589201, 3422752, 595831, 3430332], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7580, 6630], "eo:cloud_cover": 30, "proj:transform": [1, 0, 589201, 0, -1, 3430332, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0073 0103000020E61000000100000005000000AFB321FFCC8355C011C5E40D30EF3E4008AEF204C28355C076172829B0003F40F243A511338855C0302AA913D0003F4028F04E3E3D8855C02AE09EE74FEF3E40AFB321FFCC8355C011C5E40D30EF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008608_nw_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30086/m_3008608_nw_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008608_nw_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [583234, 3422703, 589860, 3430280], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7577, 6626], "eo:cloud_cover": 3, "proj:transform": [1, 0, 583234, 0, -1, 3430280, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0074 0103000020E610000001000000050000004AD40B3ECD6F55C09F008A9125EF3E4093E52494BE6F55C00C957F2DAF003F40B05417F0327455C07E18213CDA003F40F7E978CC407455C0D1949D7E50EF3E404AD40B3ECD6F55C09F008A9125EF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008502_ne_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008502_ne_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008502_ne_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [613069, 3422979, 619715, 3430573], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7594, 6646], "eo:cloud_cover": 52, "proj:transform": [1, 0, 613069, 0, -1, 3430573, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0075 0103000020E6100000010000000500000050ABE80FCD7B55C0A8AAD0402CEF3E4009E23C9CC07B55C01152B7B3AF003F40698EACFC328055C07C2B1213D4003F403FFED2A23E8055C0728C648F50EF3E4050ABE80FCD7B55C0A8AAD0402CEF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008501_nw_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008501_nw_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008501_nw_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [595168, 3422804, 601802, 3430387], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7583, 6634], "eo:cloud_cover": 8, "proj:transform": [1, 0, 595168, 0, -1, 3430387, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0076 0103000020E6100000010000000500000008E57D1CCD7755C050C763062AEF3E40F1B913ECBF7755C01152B7B3AF003F4081D07AF8327C55C0D40E7F4DD6003F403FE42D573F7C55C0728C648F50EF3E4008E57D1CCD7755C050C763062AEF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008501_ne_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008501_ne_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008501_ne_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [601135, 3422859, 607773, 3430446], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7587, 6638], "eo:cloud_cover": 59, "proj:transform": [1, 0, 601135, 0, -1, 3430446, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0077 0103000020E61000000100000005000000BB61DBA2CCAB55C09545611745EF3E40933655F7C8AB55C0FFB27BF2B0003F40B6114F7633B055C0CAC2D7D7BA003F406DE34F5436B055C0C05DF6EB4EEF3E40BB61DBA2CCAB55C09545611745EF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008603_nw_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30086/m_3008603_nw_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008603_nw_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [523566, 3422402, 530153, 3429944], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7542, 6587], "eo:cloud_cover": 64, "proj:transform": [1, 0, 523566, 0, -1, 3429944, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0078 0103000020E61000000100000005000000EBE5779ACCA755C0DE59BBED42EF3E40F3583332C8A755C082919735B1003F40459E245D33AC55C06495D233BD003F40CCD1E3F736AC55C06155BDFC4EEF3E40EBE5779ACCA755C0DE59BBED42EF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008603_ne_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30086/m_3008603_ne_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30086/m_3008603_ne_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [529533, 3422417, 536124, 3429963], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7546, 6591], "eo:cloud_cover": 21, "proj:transform": [1, 0, 529533, 0, -1, 3429963, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0079 0103000020E61000000100000005000000A305685BCD6355C0FC1BB4571FEF3E4094331477BC6355C0C4E8B985AE003F406F6589CE326855C0E10D6954E0003F40252026E1426855C0365A0EF450EF3E40A305685BCD6355C0FC1BB4571FEF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008504_nw_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008504_nw_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008504_nw_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [630971, 3423185, 637629, 3430789], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7604, 6658], "eo:cloud_cover": 65, "proj:transform": [1, 0, 630971, 0, -1, 3430789, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0080 0103000020E61000000100000005000000EACBD24ECD5F55C0A9F57EA31DEF3E400B98C0ADBB5F55C024F1F274AE003F402E76FBAC326455C098F90E7EE2003F40B492567C436455C0DC0E0D8B51EF3E40EACBD24ECD5F55C0A9F57EA31DEF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008504_ne_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008504_ne_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008504_ne_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [636939, 3423261, 643601, 3430868], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7607, 6662], "eo:cloud_cover": 95, "proj:transform": [1, 0, 636939, 0, -1, 3430868, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0081 0103000020E61000000100000005000000D960E124CD6B55C04DDA54DD23EF3E4052103CBEBD6B55C00C957F2DAF003F409FE925C6327055C0D7FB8D76DC003F40CD22145B417055C077499C1551EF3E40D960E124CD6B55C04DDA54DD23EF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008503_nw_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008503_nw_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008503_nw_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [619037, 3423045, 625687, 3430642], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7597, 6650], "eo:cloud_cover": 39, "proj:transform": [1, 0, 619037, 0, -1, 3430642, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0082 0103000020E610000001000000050000004AD40B3ECD6755C07218CC5F21EF3E40F321A81ABD6755C089B663EAAE003F4087A757CA326C55C04DF8A57EDE003F408542041C426C55C0F46A80D250EF3E404AD40B3ECD6755C07218CC5F21EF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008503_ne_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008503_ne_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008503_ne_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [625004, 3423113, 631658, 3430714], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7601, 6654], "eo:cloud_cover": 26, "proj:transform": [1, 0, 625004, 0, -1, 3430714, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0001 0103000020E6100000010000000500000044FD2E6CCD5755C0174850FC18EF3E40C405A051BA5755C0E2016553AE003F408D7E349C325C55C007D15AD1E6003F409B1C3EE9445C55C09B1F7F6951EF3E4044FD2E6CCD5755C0174850FC18EF3E40 pgstac-test-collection 2011-08-25 00:00:00+00 2011-08-25 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008505_ne_16_1_20110825.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008505_ne_16_1_20110825.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008505_ne_16_1_20110825.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-25T00:00:00Z", "naip:year": "2011", "proj:bbox": [648874, 3423421, 655544, 3431036], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7615, 6670], "eo:cloud_cover": 89, "proj:transform": [1, 0, 648874, 0, -1, 3431036, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0002 0103000020E610000001000000050000002CBB6070CD5B55C0CE33F6251BEF3E407D259012BB5B55C0A112D731AE003F40E6AF90B9326055C06DFE5F75E4003F403C2EAA45446055C05930F14751EF3E402CBB6070CD5B55C0CE33F6251BEF3E40 pgstac-test-collection 2011-08-25 00:00:00+00 2011-08-25 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008505_nw_16_1_20110825.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008505_nw_16_1_20110825.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008505_nw_16_1_20110825.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-25T00:00:00Z", "naip:year": "2011", "proj:bbox": [642906, 3423339, 649572, 3430950], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7611, 6666], "eo:cloud_cover": 33, "proj:transform": [1, 0, 642906, 0, -1, 3430950, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0083 0103000020E61000000100000005000000C11E1329CD7355C015FDA19927EF3E40DA91EA3BBF7355C0D0622992AF003F40991249F4327855C0EB025E66D8003F402788BA0F407855C0EFAD484C50EF3E40C11E1329CD7355C015FDA19927EF3E40 pgstac-test-collection 2011-08-15 00:00:00+00 2011-08-15 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008502_nw_16_1_20110815.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008502_nw_16_1_20110815.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008502_nw_16_1_20110815.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-15T00:00:00Z", "naip:year": "2011", "proj:bbox": [607102, 3422917, 613744, 3430508], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7591, 6642], "eo:cloud_cover": 26, "proj:transform": [1, 0, 607102, 0, -1, 3430508, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0086 0103000020E6100000010000000500000078D32D3BC4EF55C0070ABC934FDF3E4026FE28EACCEF55C0C1525DC0CBF03E40F98557923CF455C0B806B64AB0F03E40DA01D71533F455C09EB5DB2E34DF3E4078D32D3BC4EF55C0070ABC934FDF3E40 pgstac-test-collection 2011-08-01 00:00:00+00 2011-08-01 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008702_se_16_1_20110801.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008702_se_16_1_20110801.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008702_se_16_1_20110801.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-01T00:00:00Z", "naip:year": "2011", "proj:bbox": [422031, 3415689, 428653, 3423259], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7570, 6622], "eo:cloud_cover": 40, "proj:transform": [1, 0, 422031, 0, -1, 3423259, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0087 0103000020E610000001000000050000004E0CC9C9C4EB55C0E333D93F4FEF3E40FC5069C4CCEB55C04B5645B8C9003F4052D158FB3BF055C0F9F5436CB0003F403333333333F055C0D2C2651536EF3E404E0CC9C9C4EB55C0E333D93F4FEF3E40 pgstac-test-collection 2011-08-01 00:00:00+00 2011-08-01 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008703_nw_16_1_20110801.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008703_nw_16_1_20110801.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008703_nw_16_1_20110801.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-01T00:00:00Z", "naip:year": "2011", "proj:bbox": [428052, 3422577, 434667, 3430144], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7567, 6615], "eo:cloud_cover": 36, "proj:transform": [1, 0, 428052, 0, -1, 3430144, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0088 0103000020E61000000100000005000000D7C1C1DEC4EB55C048F949B54FDF3E409D4830D5CCEB55C00A67B796C9F03E40292499D53BF055C0B806B64AB0F03E40F243A51133F055C03788D68A36DF3E40D7C1C1DEC4EB55C048F949B54FDF3E40 pgstac-test-collection 2011-08-01 00:00:00+00 2011-08-01 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008703_sw_16_1_20110801.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008703_sw_16_1_20110801.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008703_sw_16_1_20110801.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-01T00:00:00Z", "naip:year": "2011", "proj:bbox": [428006, 3415651, 434624, 3423217], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7566, 6618], "eo:cloud_cover": 23, "proj:transform": [1, 0, 428006, 0, -1, 3423217, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0089 0103000020E61000000100000005000000FBB1497EC4EF55C0252367614F8F3E4038691A14CDEF55C01B9E5E29CBA03E405E656D533CF455C0F3380CE6AFA03E40B05417F032F455C09EB5DB2E348F3E40FBB1497EC4EF55C0252367614F8F3E40 pgstac-test-collection 2011-07-31 00:00:00+00 2011-07-31 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008726_ne_16_1_20110731.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008726_ne_16_1_20110731.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008726_ne_16_1_20110731.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-07-31T00:00:00Z", "naip:year": "2011", "proj:bbox": [421780, 3381056, 428421, 3388625], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7569, 6641], "eo:cloud_cover": 36, "proj:transform": [1, 0, 421780, 0, -1, 3388625, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0090 0103000020E61000000100000005000000BF99982EC4EF55C0CBD765F84FCF3E40850662D9CCEF55C0DF6B088ECBE03E40A054FB743CF455C076172829B0E03E40698EACFC32F455C06283859334CF3E40BF99982EC4EF55C0CBD765F84FCF3E40 pgstac-test-collection 2011-07-31 00:00:00+00 2011-07-31 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008710_ne_16_1_20110731.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008710_ne_16_1_20110731.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008710_ne_16_1_20110731.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-07-31T00:00:00Z", "naip:year": "2011", "proj:bbox": [421981, 3408763, 428607, 3416332], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7569, 6626], "eo:cloud_cover": 84, "proj:transform": [1, 0, 421981, 0, -1, 3416332, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0091 0103000020E6100000010000000500000001892650C4EF55C048F949B54F9F3E4026FE28EACCEF55C03F74417DCBB03E40179F02603CF455C076172829B0B03E4081D07AF832F455C0C18BBE82349F3E4001892650C4EF55C048F949B54F9F3E40 pgstac-test-collection 2011-07-31 00:00:00+00 2011-07-31 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008718_se_16_1_20110731.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008718_se_16_1_20110731.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008718_se_16_1_20110731.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-07-31T00:00:00Z", "naip:year": "2011", "proj:bbox": [421830, 3387983, 428468, 3395552], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7569, 6638], "eo:cloud_cover": 31, "proj:transform": [1, 0, 421830, 0, -1, 3395552, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0092 0103000020E61000000100000005000000E9465854C4EF55C048F949B54FBF3E40DF37BEF6CCEF55C0FD84B35BCBD03E40404CC2853CF455C09430D3F6AFD03E40DA01D71533F455C0809C306134BF3E40E9465854C4EF55C048F949B54FBF3E40 pgstac-test-collection 2011-07-31 00:00:00+00 2011-07-31 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008710_se_16_1_20110731.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008710_se_16_1_20110731.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008710_se_16_1_20110731.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-07-31T00:00:00Z", "naip:year": "2011", "proj:bbox": [421930, 3401836, 428560, 3409405], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7569, 6630], "eo:cloud_cover": 24, "proj:transform": [1, 0, 421930, 0, -1, 3409405, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0093 0103000020E61000000100000005000000FA97A432C5EB55C0252367614F8F3E4067EDB60BCDEB55C0C9772975C9A03E405F7F129F3BF055C0F9F5436CB0A03E4081D07AF832F055C055A18158368F3E40FA97A432C5EB55C0252367614F8F3E40 pgstac-test-collection 2011-07-31 00:00:00+00 2011-07-31 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008727_nw_16_1_20110731.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008727_nw_16_1_20110731.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008727_nw_16_1_20110731.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-07-31T00:00:00Z", "naip:year": "2011", "proj:bbox": [427774, 3381018, 434411, 3388584], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7566, 6637], "eo:cloud_cover": 61, "proj:transform": [1, 0, 427774, 0, -1, 3388584, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0094 0103000020E6100000010000000500000030F31DFCC4EB55C0C51A2E724FCF3E4026FE28EACCEB55C028806264C9E03E40F99FFCDD3BF055C076172829B0E03E40AA7D3A1E33F055C055A1815836CF3E4030F31DFCC4EB55C0C51A2E724FCF3E40 pgstac-test-collection 2011-07-31 00:00:00+00 2011-07-31 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008711_nw_16_1_20110731.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008711_nw_16_1_20110731.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008711_nw_16_1_20110731.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-07-31T00:00:00Z", "naip:year": "2011", "proj:bbox": [427959, 3408724, 434581, 3416290], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7566, 6622], "eo:cloud_cover": 29, "proj:transform": [1, 0, 427959, 0, -1, 3416290, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0095 0103000020E6100000010000000500000078B988EFC4EB55C08AE8D7D64FBF3E40850662D9CCEB55C0EC4D0CC9C9D03E40B8B06EBC3BF055C03BE5D18DB0D03E40390A100533F055C0797764AC36BF3E4078B988EFC4EB55C08AE8D7D64FBF3E40 pgstac-test-collection 2011-07-31 00:00:00+00 2011-07-31 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008711_sw_16_1_20110731.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008711_sw_16_1_20110731.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008711_sw_16_1_20110731.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-07-31T00:00:00Z", "naip:year": "2011", "proj:bbox": [427913, 3401798, 434539, 3409364], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7566, 6626], "eo:cloud_cover": 63, "proj:transform": [1, 0, 427913, 0, -1, 3409364, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0096 0103000020E61000000100000005000000B9A81611C5EB55C06612F5824FAF3E40DF37BEF6CCEB55C0696FF085C9C03E405F7F129F3BF055C058FE7C5BB0C03E40B05417F032F055C0F698486936AF3E40B9A81611C5EB55C06612F5824FAF3E40 pgstac-test-collection 2011-07-31 00:00:00+00 2011-07-31 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008719_nw_16_1_20110731.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008719_nw_16_1_20110731.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008719_nw_16_1_20110731.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-07-31T00:00:00Z", "naip:year": "2011", "proj:bbox": [427867, 3394871, 434496, 3402437], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7566, 6629], "eo:cloud_cover": 26, "proj:transform": [1, 0, 427867, 0, -1, 3402437, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0097 0103000020E6100000010000000500000001892650C4EF55C0C51A2E724FAF3E400EBC5AEECCEF55C0C1525DC0CBC03E40B796C9703CF455C0F9F5436CB0C03E40390A100533F455C09EB5DB2E34AF3E4001892650C4EF55C0C51A2E724FAF3E40 pgstac-test-collection 2011-07-31 00:00:00+00 2011-07-31 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008718_ne_16_1_20110731.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008718_ne_16_1_20110731.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008718_ne_16_1_20110731.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-07-31T00:00:00Z", "naip:year": "2011", "proj:bbox": [421880, 3394909, 428514, 3402479], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7570, 6634], "eo:cloud_cover": 1, "proj:transform": [1, 0, 421880, 0, -1, 3402479, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0098 0103000020E61000000100000005000000E92CB308C5EB55C0E9F010C64F9F3E4026FE28EACCEB55C046990D32C9B03E40E7340BB43BF055C0D61F6118B0B03E4021C8410933F055C01A6F2BBD369F3E40E92CB308C5EB55C0E9F010C64F9F3E40 pgstac-test-collection 2011-07-31 00:00:00+00 2011-07-31 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008719_sw_16_1_20110731.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008719_sw_16_1_20110731.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008719_sw_16_1_20110731.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-07-31T00:00:00Z", "naip:year": "2011", "proj:bbox": [427820, 3387945, 434454, 3395510], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7565, 6634], "eo:cloud_cover": 73, "proj:transform": [1, 0, 427820, 0, -1, 3395510, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0099 0103000020E61000000100000005000000FA97A432C5EB55C0070ABC934F7F3E407F2F8507CDEB55C0A5A14621C9903E40BE874B8E3BF055C035289A07B0903E40B05417F032F055C0D87F9D9B367F3E40FA97A432C5EB55C0070ABC934F7F3E40 pgstac-test-collection 2011-07-31 00:00:00+00 2011-07-31 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008727_sw_16_1_20110731.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008727_sw_16_1_20110731.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008727_sw_16_1_20110731.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-07-31T00:00:00Z", "naip:year": "2011", "proj:bbox": [427728, 3374092, 434369, 3381657], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7565, 6641], "eo:cloud_cover": 90, "proj:transform": [1, 0, 427728, 0, -1, 3381657, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0084 0103000020E610000001000000050000003E26529ACD4F55C09014916115EF3E4036AD1402B94F55C01E34BBEEAD003F408D7E349C325455C094C151F2EA003F403BE0BA62465455C023BBD23252EF3E403E26529ACD4F55C09014916115EF3E40 pgstac-test-collection 2011-08-02 00:00:00+00 2011-08-02 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008506_ne_16_1_20110802.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30085/m_3008506_ne_16_1_20110802.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30085/m_3008506_ne_16_1_20110802.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-02T00:00:00Z", "naip:year": "2011", "proj:bbox": [660809, 3423596, 667487, 3431217], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7621, 6678], "eo:cloud_cover": 52, "proj:transform": [1, 0, 660809, 0, -1, 3431217, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0085 0103000020E610000001000000050000001FA2D11DC4EF55C048F949B54FEF3E409D4830D5CCEF55C0624A24D1CB003F40280AF4893CF455C058FE7C5BB0003F40390A100533F455C03FADA23F34EF3E401FA2D11DC4EF55C048F949B54FEF3E40 pgstac-test-collection 2011-08-01 00:00:00+00 2011-08-01 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008702_ne_16_1_20110801.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008702_ne_16_1_20110801.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008702_ne_16_1_20110801.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-08-01T00:00:00Z", "naip:year": "2011", "proj:bbox": [422082, 3422616, 428700, 3430186], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7570, 6618], "eo:cloud_cover": 3, "proj:transform": [1, 0, 422082, 0, -1, 3430186, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]} +pgstac-test-item-0100 0103000020E61000000100000005000000CB2DAD86C4EF55C0070ABC934F7F3E4038691A14CDEF55C09E7C7A6CCB903E40A62BD8463CF455C076172829B0903E40C896E5EB32F455C02194F771347F3E40CB2DAD86C4EF55C0070ABC934F7F3E40 pgstac-test-collection 2011-07-31 00:00:00+00 2011-07-31 00:00:00+00 {"assets": {"image": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008726_se_16_1_20110731.tif"}, "metadata": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_fgdc_2011/30087/m_3008726_se_16_1_20110731.txt"}, "thumbnail": {"href": "https://naipeuwest.blob.core.windows.net/naip/v002/al/2011/al_100cm_2011/30087/m_3008726_se_16_1_20110731.200.jpg"}}, "properties": {"gsd": 1, "datetime": "2011-07-31T00:00:00Z", "naip:year": "2011", "proj:bbox": [421730, 3374130, 428375, 3381699], "proj:epsg": 26916, "providers": [{"url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/", "name": "USDA Farm Service Agency", "roles": ["producer", "licensor"]}], "naip:state": "al", "proj:shape": [7569, 6645], "eo:cloud_cover": 50, "proj:transform": [1, 0, 421730, 0, -1, 3381699, 0, 0, 1]}, "stac_extensions": ["eo", "projection"]}