Skip to content

Commit 9b9fb46

Browse files
Redefine the Catalog Hierarchy using Closure Tables (#954)
* WIP: made changes to orm and adapter * MNT: fix typo * ENH: upgrade path in Alembic migration * ENH: add triggers to alembig migration * MNT: add alembic downgrade path * FIX: add include_data_sources to parent * DOC: update the description for the Nodes tables * MNT: clean-up ORM definitions * ENH: add indices * TST: update fts5 script * FIX: prevent the root node from being deleted * TST: fix the join operation in aliased ORMs when applying conditions * ENH: fix recursive joins * TST: fix tests * REV: revert changes * FIX: key attribute in CatalogNodeAdapter * MNT: dependency and linting * MNT: add PG dependency * TST: use sqlite or pg uri * TST: use sqlite or pg uri * Update CHANGELOG.md * ENH: add index on parent column * FIX: create_index signature * FIX: cartesian product warning * Cleanup rebase mistake * Drop synchronous PG dependency (we use asyncpg) * MNT: changelog * FIX: move mounting a node to async engine --------- Co-authored-by: Dan Allan <[email protected]>
1 parent 3e7212c commit 9b9fb46

File tree

13 files changed

+639
-182
lines changed

13 files changed

+639
-182
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ continuous deployment processes.
1717

1818
## v0.1.0-b31 (2025-08-01)
1919

20+
### Changed
21+
22+
- The logic of hierarchical organization of the Nodes table in Catalog: use the concept
23+
of Closure Table to track ancestors and descendands of the nodes.
24+
2025
### Added
2126

2227
- Pooling of ADBC connections to storage databases.
@@ -39,13 +44,16 @@ continuous deployment processes.
3944
dependencies injection
4045
- Updated front-end dependencies, and updated node version used for building
4146
front-end.
47+
- The logic of hierarchical organization of the Nodes table in Catalog: use the
48+
concept of Closure Table to track ancestors and descendants of the nodes.
4249

4350
### Fixed
4451

4552
- Restored authentication check for API key
4653
- Updated usage for change in Zarr 3.x API.
4754
- Improved error message if config location is non-file
4855

56+
4957
## v0.1.0-b29 (2025-06-06)
5058

5159
### Added

docs/source/explanations/catalog.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ presents the data to clients. Each row represents one node in the logical
3131
"tree" of data represented by Tiled.
3232

3333
- `metadata` --- user-controlled JSON object, with arbitrary metadata
34-
- `ancestors` and `key` --- together specify the unique path of the data
34+
- `key` --- the name of the Node; together with ancestors specify the unique path of the data
3535
- `structre_family` --- enum of structure types (`"container"`, `"array"`, `"table"`, ...)
3636
- `specs` --- user-controlled JSON list of specs, such as `[{"name": "XDI", "version": "1"}]`
37-
- `id` an internal integer primary key, not exposed by the API
37+
- `id` --- an internal integer primary key, not exposed by the API
38+
- `parent` --- the `id` of the node's parent
3839
- `time_created` and `time_updated` --- for forensics, not exposed by the API
3940

4041
The `time_created` and `time_updated` columns, which appear in this table and

tiled/_tests/test_access_control.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ def test_public_access(
468468
public_client_g["g", "A3"]
469469

470470

471-
def test_service_principal_access(tmpdir):
471+
def test_service_principal_access(tmpdir, sqlite_or_postgres_uri):
472472
"Test that a service principal can work with SimpleAccessPolicy."
473473
config = {
474474
"authentication": {
@@ -494,7 +494,7 @@ def test_service_principal_access(tmpdir):
494494
{
495495
"tree": "catalog",
496496
"args": {
497-
"uri": f"sqlite:///{tmpdir}/catalog.db",
497+
"uri": sqlite_or_postgres_uri,
498498
"writable_storage": f"file://localhost{tmpdir}/data",
499499
"init_if_not_exists": True,
500500
},

tiled/_tests/test_catalog.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ async def test_nested_node_creation(a):
6060
specs=[],
6161
)
6262
c = await b.lookup_adapter(["c"])
63-
assert b.segments == ["b"]
64-
assert c.segments == ["b", "c"]
63+
assert await b.path_segments() == ["b"]
64+
assert await c.path_segments() == ["b", "c"]
6565
assert (await a.keys_range(0, 1)) == ["b"]
6666
assert (await b.keys_range(0, 1)) == ["c"]
6767
# smoke test
@@ -344,7 +344,7 @@ async def test_delete_tree(tmpdir):
344344
d.write_array([7, 8, 9])
345345

346346
nodes_before_delete = (await tree.context.execute("SELECT * from nodes")).all()
347-
assert len(nodes_before_delete) == 7
347+
assert len(nodes_before_delete) == 7 + 1 # +1 for the root node
348348
data_sources_before_delete = (
349349
await tree.context.execute("SELECT * from data_sources")
350350
).all()
@@ -361,7 +361,7 @@ async def test_delete_tree(tmpdir):
361361
await tree.delete_tree(external_only=False)
362362

363363
nodes_after_delete = (await tree.context.execute("SELECT * from nodes")).all()
364-
assert len(nodes_after_delete) == 0
364+
assert len(nodes_after_delete) == 0 + 1 # the root node that should remain
365365
data_sources_after_delete = (
366366
await tree.context.execute("SELECT * from data_sources")
367367
).all()
@@ -371,7 +371,7 @@ async def test_delete_tree(tmpdir):
371371

372372

373373
@pytest.mark.asyncio
374-
async def test_access_control(tmpdir):
374+
async def test_access_control(tmpdir, sqlite_or_postgres_uri):
375375
config = {
376376
"authentication": {
377377
"allow_anonymous_access": True,
@@ -410,7 +410,7 @@ async def test_access_control(tmpdir):
410410
"tree": "catalog",
411411
"path": "/",
412412
"args": {
413-
"uri": f"sqlite:///{tmpdir}/catalog.db",
413+
"uri": sqlite_or_postgres_uri,
414414
"writable_storage": str(tmpdir / "data"),
415415
"init_if_not_exists": True,
416416
},
@@ -536,7 +536,7 @@ async def test_constraints_on_parameter_and_num(a, assets):
536536

537537

538538
@pytest.mark.asyncio
539-
async def test_init_db_logging(tmpdir, caplog):
539+
async def test_init_db_logging(sqlite_or_postgres_uri, tmpdir, caplog):
540540
config = {
541541
"database": {
542542
"uri": "sqlite://", # in-memory
@@ -546,7 +546,7 @@ async def test_init_db_logging(tmpdir, caplog):
546546
"tree": "catalog",
547547
"path": "/",
548548
"args": {
549-
"uri": f"sqlite:///{tmpdir}/catalog.db",
549+
"uri": sqlite_or_postgres_uri,
550550
"writable_storage": str(tmpdir / "data"),
551551
"init_if_not_exists": True,
552552
},

tiled/_tests/test_cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def test_serve_catalog_temp(args, tmp_path):
9393
"",
9494
],
9595
)
96-
def test_serve_config(args, tmp_path):
96+
def test_serve_config(args, tmp_path, sqlite_or_postgres_uri):
9797
"Test 'tiled serve config' with a tmp config file."
9898
(tmp_path / "data").mkdir()
9999
(tmp_path / "config").mkdir()
@@ -107,7 +107,7 @@ def test_serve_config(args, tmp_path):
107107
- path: /
108108
tree: catalog
109109
args:
110-
uri: sqlite:///{tmp_path / 'catalog.db'}
110+
uri: {sqlite_or_postgres_uri}
111111
writable_storage: {tmp_path / 'data'}
112112
init_if_not_exists: true
113113
"""

tiled/_tests/test_writing.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ async def test_delete(tree):
510510
key="delete_me",
511511
)
512512
nodes_before_delete = (await tree.context.execute("SELECT * from nodes")).all()
513-
assert len(nodes_before_delete) == 1
513+
assert len(nodes_before_delete) == 1 + 1 # +1 for the root node
514514
data_sources_before_delete = (
515515
await tree.context.execute("SELECT * from data_sources")
516516
).all()
@@ -531,7 +531,7 @@ async def test_delete(tree):
531531
client.delete("delete_me")
532532

533533
nodes_after_delete = (await tree.context.execute("SELECT * from nodes")).all()
534-
assert len(nodes_after_delete) == 0
534+
assert len(nodes_after_delete) == 0 + 1 # the root node should still exist
535535
data_sources_after_delete = (
536536
await tree.context.execute("SELECT * from data_sources")
537537
).all()

0 commit comments

Comments
 (0)