Skip to content

Commit 0331eb7

Browse files
fix(#167): restructure sortables response
1 parent 9502eda commit 0331eb7

File tree

12 files changed

+82
-47
lines changed

12 files changed

+82
-47
lines changed

docs/index-config.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Entries in `queryables` and `sortables` must have a corresponding entry in `inde
1818

1919
Each queryable and sortable property must include a list of collections for which the property is queryable or sortable. The `*` wildcard value can be used to indicate all collections. It is **not** currently possible to wildcard partial collection IDs, such as `collection-*`.
2020

21+
`storage_type` **must** reference a valid [DuckDB data type](https://duckdb.org/docs/stable/sql/data_types/overview.html).
22+
2123
### Queryables
2224

2325
Queryables require a `json_schema` property containing a schema that could be used to validate values of this property. This JSON schema is not used directly by the API but is provided to API clients via the `/queryables` endpoints such that a client can validate any value it intends to send as query value for this property.

packages/stac-index/src/stac_index/indexer/creator/configurer.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def add_items_columns(config: IndexConfig, connection: DuckDBPyConnection) -> No
1010
connection.execute(
1111
f"""
1212
ALTER TABLE items
13-
ADD COLUMN {indexable.table_column_name} {indexable.storage_type.value}
13+
ADD COLUMN {indexable.table_column_name} {indexable.storage_type}
1414
"""
1515
)
1616

@@ -57,14 +57,15 @@ def _configure_sortables(config: IndexConfig, connection: DuckDBPyConnection) ->
5757
sortables_collections.append((collection_id, name))
5858
connection.execute(
5959
"""
60-
INSERT INTO sortables (name, description, json_path, items_column)
61-
VALUES (?, ?, ?, ?)
60+
INSERT INTO sortables (name, description, json_path, items_column, json_type)
61+
VALUES (?, ?, ?, ?, ?)
6262
""",
6363
[
6464
name,
6565
indexable.description,
6666
indexable.json_path,
6767
indexable.table_column_name,
68+
indexable.json_type,
6869
],
6970
)
7071
for collection_id, name in sortables_collections:

packages/stac-index/src/stac_index/indexer/creator/sql/01-tables/111-sortables.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ CREATE TABLE sortables (
33
description VARCHAR NOT NULL,
44
json_path VARCHAR NOT NULL,
55
items_column VARCHAR,
6+
json_type VARCHAR NOT NULL,
67
UNIQUE(json_path),
78
UNIQUE(items_column),
89
);

packages/stac-index/src/stac_index/indexer/creator/sql/11-views/002-sortables_by_collection.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CREATE VIEW sortables_by_collection AS
44
, s.description
55
, s.json_path
66
, COALESCE(s.items_column, s.name) AS items_column
7+
, s.json_type
78
FROM sortables s
89
JOIN sortables_collections sc ON s.name == sc.name
910
UNION
@@ -12,6 +13,7 @@ CREATE VIEW sortables_by_collection AS
1213
, s.description
1314
, s.json_path
1415
, COALESCE(s.items_column, s.name) AS items_column
16+
, s.json_type
1517
FROM sortables s
1618
JOIN (
1719
SELECT sac.name
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
INSERT INTO sortables (name, description, json_path, items_column) VALUES
2-
('id', 'Item ID', 'id', NULL),
3-
('collection', 'Collection ID', 'collection', 'collection_id'),
4-
('datetime', 'Datetime, NULL if datetime is a range', 'datetime', NULL),
5-
('start_datetime', 'Start datetime if datetime is a range, NULL if not', 'start_datetime', NULL),
6-
('end_datetime', 'End datetime if datetime is a range, NULL if not', 'end_datetime', NULL),
1+
INSERT INTO sortables (name, description, json_path, items_column, json_type) VALUES
2+
('id', 'Item ID', 'id', NULL, 'string'),
3+
('collection', 'Collection ID', 'collection', 'collection_id', 'string'),
4+
('datetime', 'Datetime, NULL if datetime is a range', 'datetime', NULL, 'string'),
5+
('start_datetime', 'Start datetime if datetime is a range, NULL if not', 'start_datetime', NULL, 'string'),
6+
('end_datetime', 'End datetime if datetime is a range, NULL if not', 'end_datetime', NULL, 'string'),
77
;

packages/stac-index/src/stac_index/indexer/types/index_config.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
from enum import Enum
2-
from re import sub
3-
from typing import Any, Dict, Final, List, Optional
1+
from re import IGNORECASE, match, sub
2+
from typing import Any, Dict, Final, List, Optional, Self
43

54
from pydantic import BaseModel
65

@@ -11,20 +10,36 @@ class StorageTypeProperties(BaseModel):
1110
pass
1211

1312

14-
class StorageType(str, Enum):
15-
DOUBLE = "DOUBLE"
16-
17-
1813
class Indexable(BaseModel):
1914
json_path: str
2015
description: str
21-
storage_type: StorageType
16+
# storage_type is required to match the name of a valid DuckDB type as per https://duckdb.org/docs/stable/sql/data_types/overview.html.
17+
# This could be an enum for more strict control, but this enum would require ongoing maintenance to match DuckDB's types.
18+
# If a caller provided an invalid enum value it would result in a runtime failure from this class, which is
19+
# functionally no different to a runtime failure when DuckDB attempts to add a table column of this type.
20+
# Don't attempt to validate that storage_type is a valid DuckDB type and provide documentation around this.
21+
storage_type: str
2222
storage_type_properties: Optional[StorageTypeProperties] = None
2323

2424
@property
25-
def table_column_name(self) -> str:
25+
def table_column_name(self: Self) -> str:
2626
return "i_{}".format(sub("[^A-Za-z0-9]", "_", self.json_path))
2727

28+
@property
29+
def json_type(self: Self) -> str:
30+
if self.storage_type in (
31+
"JSON",
32+
"UUID",
33+
"VARCHAR",
34+
):
35+
return "string"
36+
elif match("^(DATE|TIME)", self.storage_type, flags=IGNORECASE):
37+
return "string"
38+
elif self.storage_type == "BOOLEAN":
39+
return "boolean"
40+
else:
41+
return "number"
42+
2843

2944
class Queryable(BaseModel):
3045
json_schema: Dict[str, Any]
Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
1-
from typing import Final, List
1+
from typing import Any, Callable, Final, Self, cast
22

3-
from pydantic import BaseModel
3+
from pydantic import BaseModel, model_serializer
44

55

66
class SortableField(BaseModel):
7-
title: str
8-
description: str
7+
type: str
98

109

1110
class SortablesResponse(BaseModel):
12-
title: Final[str] = "STAC Sortables"
13-
fields: List[SortableField]
11+
title: Final[str] = "Sortables"
12+
properties: dict[str, SortableField]
13+
14+
@model_serializer(mode="wrap")
15+
def serialize_model(self: Self, serializer: Callable) -> dict[str, Any]:
16+
return {
17+
"$schema": "https://json-schema.org/draft/2020-12/schema",
18+
"$id": "https://example.com/sortables",
19+
"title": "Sortables",
20+
"type": "object",
21+
**cast(dict[str, Any], serializer(self)),
22+
}

src/stac_fastapi/indexed/sortables/routes.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,11 @@ def add_routes(app: FastAPI) -> None:
1818
)
1919
async def get_all_sortables() -> SortablesResponse:
2020
return SortablesResponse(
21-
fields=[
22-
SortableField(
23-
title=config.name,
24-
description=config.description,
25-
)
21+
properties={
22+
config.name: SortableField(type=config.type)
2623
for config in await get_sortable_configs()
2724
if config.collection_id == collection_wildcard
28-
]
25+
}
2926
)
3027

3128
@app.get(
@@ -36,12 +33,9 @@ async def get_all_sortables() -> SortablesResponse:
3633
)
3734
async def get_collection_sortables(collection_id: str) -> SortablesResponse:
3835
return SortablesResponse(
39-
fields=[
40-
SortableField(
41-
title=config.name,
42-
description=config.description,
43-
)
36+
properties={
37+
config.name: SortableField(type=config.type)
4438
for config in await get_sortable_configs()
4539
if config.collection_id in [collection_id, collection_wildcard]
46-
]
40+
}
4741
)

src/stac_fastapi/indexed/sortables/sortable_config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
_logger: Final[Logger] = getLogger(__name__)
1010

1111

12-
@dataclass
12+
@dataclass(kw_only=True)
1313
class SortableConfig:
1414
name: str
15+
type: str
1516
collection_id: str
1617
description: str
1718
items_column: str
@@ -31,13 +32,15 @@ async def _get_sortable_configs(_: str) -> List[SortableConfig]:
3132
collection_id=row[1],
3233
description=row[2],
3334
items_column=row[3],
35+
type=row[4],
3436
)
3537
for row in await fetchall(
3638
f"""
3739
SELECT name
3840
, collection_id
3941
, description
4042
, items_column
43+
, json_type
4144
FROM {format_query_object_name('sortables_by_collection')}
4245
"""
4346
)

tests/with_environment/integration_tests/test_get_search.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,10 @@ def test_get_search_token_immutable() -> None:
222222

223223
def test_get_search_alternate_order() -> None:
224224
sortable_field_names: List[str] = [
225-
entry["title"]
226-
for entry in requests.get(f"{api_base_url}/sortables").json()["fields"]
225+
entry
226+
for entry in requests.get(f"{api_base_url}/sortables")
227+
.json()["properties"]
228+
.keys()
227229
]
228230
assert "datetime" in sortable_field_names
229231
assert "id" in sortable_field_names

0 commit comments

Comments
 (0)