dirsql 0.3.12__tar.gz → 0.3.13__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dirsql-0.3.12 → dirsql-0.3.13}/Cargo.lock +1 -1
- {dirsql-0.3.12 → dirsql-0.3.13}/PKG-INFO +1 -1
- {dirsql-0.3.12 → dirsql-0.3.13}/dirsql/_async.py +17 -0
- dirsql-0.3.13/dirsql/resolve_config.py +43 -0
- dirsql-0.3.13/dirsql/resolve_config_test.py +231 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/api/index.md +20 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/Cargo.toml +1 -1
- {dirsql-0.3.12/packages/rust → dirsql-0.3.13/packages/python}/docs/api/index.md +20 -0
- dirsql-0.3.13/packages/python/tests/integration/test_serialization.py +237 -0
- {dirsql-0.3.12/packages/python → dirsql-0.3.13/packages/rust}/docs/api/index.md +20 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/lib.rs +90 -0
- dirsql-0.3.13/packages/rust/tests/serialization.rs +161 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/Cargo.toml +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/README.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/dirsql/__init__.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/dirsql/_cli/__init__.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/dirsql/_cli/binary_path.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/dirsql/_cli/is_windows.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/dirsql/_cli/main.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/dirsql/test_async.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/AGENTS.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/cli/config.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/cli/http-api.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/cli/index.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/cli/init.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/cli/server.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/getting-started.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/guide/async.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/guide/crdt.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/guide/persistence.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/guide/querying.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/guide/tables.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/guide/watching.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/index.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/migrations.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/package.json +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/playwright.config.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/docs/vitest.config.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/README.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/conftest.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/cli/config.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/cli/http-api.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/cli/index.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/cli/init.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/cli/server.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/guide/async.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/guide/crdt.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/guide/persistence.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/guide/querying.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/guide/tables.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/guide/watching.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/playwright.config.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/vitest.config.ts +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/src/lib.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/integration/test_async_dirsql.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/integration/test_binding.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/integration/test_dirsql.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/integration/test_docs_examples.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/integration/test_docs_gaps.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/integration/test_from_config.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/integration/test_persist.py +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/Cargo.toml +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/README.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/cli/config.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/cli/http-api.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/cli/index.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/cli/init.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/cli/server.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/guide/async.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/guide/crdt.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/guide/persistence.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/guide/querying.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/guide/tables.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/guide/watching.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/bin/dirsql.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/cli/mod.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/cli/router.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/cli/server.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/config.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/db.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/persist.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/scanner.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/watcher.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/async_sdk.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/cli_e2e.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/cli_integration.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/docs_examples.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/docs_gaps.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/from_config.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/init_e2e.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/init_integration.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/persist.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/readonly_query.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/sdk.rs +0 -0
- {dirsql-0.3.12 → dirsql-0.3.13}/pyproject.toml +0 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
|
|
5
5
|
from dirsql._dirsql import DirSQL as _RustDirSQL
|
|
6
|
+
from dirsql.resolve_config import resolve_config
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class _WatchStream:
|
|
@@ -108,3 +109,19 @@ class DirSQL:
|
|
|
108
109
|
def watch(self):
|
|
109
110
|
"""Start watching for file changes. Returns an async iterable of RowEvent."""
|
|
110
111
|
return _WatchStream(self._db)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def __dict__(self):
|
|
115
|
+
"""Resolved construction state as a JSON-serializable dict.
|
|
116
|
+
|
|
117
|
+
Recomputed on each access; reads the ``.dirsql.toml`` if ``config=``
|
|
118
|
+
was supplied. Works before ``await db.ready()``.
|
|
119
|
+
"""
|
|
120
|
+
return resolve_config(
|
|
121
|
+
self._root,
|
|
122
|
+
self._tables,
|
|
123
|
+
self._ignore,
|
|
124
|
+
self._config,
|
|
125
|
+
self._persist,
|
|
126
|
+
self._persist_path,
|
|
127
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Synchronous config resolver for `DirSQL.__dict__`.
|
|
2
|
+
|
|
3
|
+
Mirrors `DirSQLBuilder::resolve` in the Rust core: explicit kwargs win for
|
|
4
|
+
scalars; tables and ignore lists are concatenated; persist is OR-ed;
|
|
5
|
+
path-valued config fields resolve relative to the config file's parent.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import tomllib
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def resolve_config(root, tables, ignore, config, persist, persist_path):
|
|
13
|
+
"""Merge kwargs with a `.dirsql.toml` into the serialized state shape."""
|
|
14
|
+
cfg, cfg_tables, cfg_dir = {}, [], None
|
|
15
|
+
if config is not None:
|
|
16
|
+
with open(config, "rb") as f:
|
|
17
|
+
doc = tomllib.load(f)
|
|
18
|
+
cfg = doc.get("dirsql") or {}
|
|
19
|
+
cfg_tables = doc.get("table") or []
|
|
20
|
+
cfg_dir = os.path.dirname(os.path.abspath(config))
|
|
21
|
+
|
|
22
|
+
def _abs(p):
|
|
23
|
+
return p if os.path.isabs(p) else os.path.join(cfg_dir, p)
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
"root": root or (_abs(cfg["root"]) if "root" in cfg else cfg_dir),
|
|
27
|
+
"tables": [
|
|
28
|
+
{"ddl": t.ddl, "glob": t.glob, "strict": bool(t.strict)}
|
|
29
|
+
for t in (tables or [])
|
|
30
|
+
]
|
|
31
|
+
+ [
|
|
32
|
+
{
|
|
33
|
+
"ddl": e.get("ddl"),
|
|
34
|
+
"glob": e.get("glob"),
|
|
35
|
+
"strict": bool(e.get("strict")),
|
|
36
|
+
}
|
|
37
|
+
for e in cfg_tables
|
|
38
|
+
],
|
|
39
|
+
"ignore": list(ignore or []) + list(cfg.get("ignore") or []),
|
|
40
|
+
"persist": bool(persist or cfg.get("persist")),
|
|
41
|
+
"persist_path": persist_path
|
|
42
|
+
or (_abs(cfg["persist_path"]) if "persist_path" in cfg else None),
|
|
43
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Unit tests for `resolve_config`."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from dirsql.resolve_config import resolve_config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _FakeTable:
|
|
12
|
+
"""Stand-in for the PyO3 `Table` class. `resolve_config` only reads
|
|
13
|
+
`ddl`, `glob`, `strict`, so we don't need the real binding here."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, ddl, glob, strict=False):
|
|
16
|
+
self.ddl = ddl
|
|
17
|
+
self.glob = glob
|
|
18
|
+
self.strict = strict
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def cfg_dir():
|
|
23
|
+
with tempfile.TemporaryDirectory() as d:
|
|
24
|
+
yield d
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _write(path, body):
|
|
28
|
+
with open(path, "w") as f:
|
|
29
|
+
f.write(body)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def describe_resolve_config():
|
|
33
|
+
def describe_without_a_config_file():
|
|
34
|
+
def it_forwards_the_root_kwarg(cfg_dir):
|
|
35
|
+
out = resolve_config("/abs/data", None, None, None, False, None)
|
|
36
|
+
assert out == {
|
|
37
|
+
"root": "/abs/data",
|
|
38
|
+
"tables": [],
|
|
39
|
+
"ignore": [],
|
|
40
|
+
"persist": False,
|
|
41
|
+
"persist_path": None,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def it_serializes_a_programmatic_table_with_default_strict():
|
|
45
|
+
out = resolve_config(
|
|
46
|
+
"/r",
|
|
47
|
+
[_FakeTable("x", "*")],
|
|
48
|
+
None,
|
|
49
|
+
None,
|
|
50
|
+
False,
|
|
51
|
+
None,
|
|
52
|
+
)
|
|
53
|
+
assert out["tables"] == [{"ddl": "x", "glob": "*", "strict": False}]
|
|
54
|
+
|
|
55
|
+
def it_serializes_a_programmatic_table_with_strict_true():
|
|
56
|
+
out = resolve_config(
|
|
57
|
+
"/r",
|
|
58
|
+
[_FakeTable("x", "*", strict=True)],
|
|
59
|
+
None,
|
|
60
|
+
None,
|
|
61
|
+
False,
|
|
62
|
+
None,
|
|
63
|
+
)
|
|
64
|
+
assert out["tables"][0]["strict"] is True
|
|
65
|
+
|
|
66
|
+
def it_forwards_the_ignore_kwarg():
|
|
67
|
+
out = resolve_config("/r", None, ["**/skip/**"], None, False, None)
|
|
68
|
+
assert out["ignore"] == ["**/skip/**"]
|
|
69
|
+
|
|
70
|
+
def it_forwards_persist_and_persist_path_kwargs():
|
|
71
|
+
out = resolve_config(
|
|
72
|
+
"/r",
|
|
73
|
+
None,
|
|
74
|
+
None,
|
|
75
|
+
None,
|
|
76
|
+
True,
|
|
77
|
+
"/abs/cache.db",
|
|
78
|
+
)
|
|
79
|
+
assert out["persist"] is True
|
|
80
|
+
assert out["persist_path"] == "/abs/cache.db"
|
|
81
|
+
|
|
82
|
+
def describe_with_a_config_file():
|
|
83
|
+
def it_resolves_a_relative_dirsql_root_against_the_config_parent(cfg_dir):
|
|
84
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
85
|
+
_write(path, '[dirsql]\nroot = "data"\n')
|
|
86
|
+
out = resolve_config(None, None, None, path, False, None)
|
|
87
|
+
assert out["root"] == os.path.join(cfg_dir, "data")
|
|
88
|
+
assert os.path.isabs(out["root"])
|
|
89
|
+
|
|
90
|
+
def it_preserves_an_absolute_dirsql_root_verbatim(cfg_dir):
|
|
91
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
92
|
+
_write(path, '[dirsql]\nroot = "/other/abs/path"\n')
|
|
93
|
+
out = resolve_config(None, None, None, path, False, None)
|
|
94
|
+
assert out["root"] == "/other/abs/path"
|
|
95
|
+
|
|
96
|
+
def it_defaults_root_to_config_parent_when_dirsql_root_is_absent(cfg_dir):
|
|
97
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
98
|
+
_write(path, '[dirsql]\nignore = ["x"]\n')
|
|
99
|
+
out = resolve_config(None, None, None, path, False, None)
|
|
100
|
+
assert out["root"] == cfg_dir
|
|
101
|
+
|
|
102
|
+
def it_defaults_root_to_config_parent_when_dirsql_section_is_absent(cfg_dir):
|
|
103
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
104
|
+
_write(
|
|
105
|
+
path, '[[table]]\nddl = "CREATE TABLE t (x TEXT)"\nglob = "*.json"\n'
|
|
106
|
+
)
|
|
107
|
+
out = resolve_config(None, None, None, path, False, None)
|
|
108
|
+
assert out["root"] == cfg_dir
|
|
109
|
+
|
|
110
|
+
def it_reads_table_entries_with_strict_defaulting_to_false(cfg_dir):
|
|
111
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
112
|
+
_write(
|
|
113
|
+
path,
|
|
114
|
+
'[[table]]\nddl = "CREATE TABLE t (x TEXT)"\nglob = "*.json"\n',
|
|
115
|
+
)
|
|
116
|
+
out = resolve_config(None, None, None, path, False, None)
|
|
117
|
+
assert out["tables"] == [
|
|
118
|
+
{"ddl": "CREATE TABLE t (x TEXT)", "glob": "*.json", "strict": False}
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
def it_respects_strict_true_on_a_table_entry(cfg_dir):
|
|
122
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
123
|
+
_write(
|
|
124
|
+
path,
|
|
125
|
+
'[[table]]\nddl = "CREATE TABLE t (x TEXT)"\nglob = "*.json"\nstrict = true\n',
|
|
126
|
+
)
|
|
127
|
+
out = resolve_config(None, None, None, path, False, None)
|
|
128
|
+
assert out["tables"][0]["strict"] is True
|
|
129
|
+
|
|
130
|
+
def it_returns_empty_tables_when_no_table_entries_declared(cfg_dir):
|
|
131
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
132
|
+
_write(path, "[dirsql]\nignore = []\n")
|
|
133
|
+
out = resolve_config(None, None, None, path, False, None)
|
|
134
|
+
assert out["tables"] == []
|
|
135
|
+
|
|
136
|
+
def it_forwards_dirsql_ignore(cfg_dir):
|
|
137
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
138
|
+
_write(path, '[dirsql]\nignore = ["node_modules/**"]\n')
|
|
139
|
+
out = resolve_config(None, None, None, path, False, None)
|
|
140
|
+
assert out["ignore"] == ["node_modules/**"]
|
|
141
|
+
|
|
142
|
+
def it_flips_persist_on_when_dirsql_persist_is_true(cfg_dir):
|
|
143
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
144
|
+
_write(path, "[dirsql]\npersist = true\n")
|
|
145
|
+
out = resolve_config(None, None, None, path, False, None)
|
|
146
|
+
assert out["persist"] is True
|
|
147
|
+
|
|
148
|
+
def it_resolves_a_relative_persist_path_against_the_config_dir(cfg_dir):
|
|
149
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
150
|
+
_write(
|
|
151
|
+
path,
|
|
152
|
+
'[dirsql]\npersist = true\npersist_path = "cache/dirsql.db"\n',
|
|
153
|
+
)
|
|
154
|
+
out = resolve_config(None, None, None, path, False, None)
|
|
155
|
+
assert out["persist_path"] == os.path.join(cfg_dir, "cache/dirsql.db")
|
|
156
|
+
|
|
157
|
+
def it_preserves_an_absolute_persist_path_verbatim(cfg_dir):
|
|
158
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
159
|
+
_write(path, '[dirsql]\npersist_path = "/var/cache/dirsql.db"\n')
|
|
160
|
+
out = resolve_config(None, None, None, path, False, None)
|
|
161
|
+
assert out["persist_path"] == "/var/cache/dirsql.db"
|
|
162
|
+
|
|
163
|
+
def it_leaves_persist_path_none_when_absent(cfg_dir):
|
|
164
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
165
|
+
_write(path, '[dirsql]\nignore = ["x"]\n')
|
|
166
|
+
out = resolve_config(None, None, None, path, False, None)
|
|
167
|
+
assert out["persist_path"] is None
|
|
168
|
+
|
|
169
|
+
def describe_merging_kwargs_with_a_config_file():
|
|
170
|
+
def it_explicit_root_wins_over_dirsql_root(cfg_dir):
|
|
171
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
172
|
+
_write(path, '[dirsql]\nroot = "from-config"\n')
|
|
173
|
+
out = resolve_config("/from-kwarg", None, None, path, False, None)
|
|
174
|
+
assert out["root"] == "/from-kwarg"
|
|
175
|
+
|
|
176
|
+
def it_concatenates_programmatic_then_config_tables(cfg_dir):
|
|
177
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
178
|
+
_write(
|
|
179
|
+
path,
|
|
180
|
+
'[[table]]\nddl = "CREATE TABLE c (x TEXT)"\nglob = "c/*"\n',
|
|
181
|
+
)
|
|
182
|
+
out = resolve_config(
|
|
183
|
+
"/r",
|
|
184
|
+
[_FakeTable("p-ddl", "p/*")],
|
|
185
|
+
None,
|
|
186
|
+
path,
|
|
187
|
+
False,
|
|
188
|
+
None,
|
|
189
|
+
)
|
|
190
|
+
assert [t["ddl"] for t in out["tables"]] == [
|
|
191
|
+
"p-ddl",
|
|
192
|
+
"CREATE TABLE c (x TEXT)",
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
def it_concatenates_ignore_kwargs_first_then_config_ignore(cfg_dir):
|
|
196
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
197
|
+
_write(path, '[dirsql]\nignore = ["from-config/**"]\n')
|
|
198
|
+
out = resolve_config(
|
|
199
|
+
"/r",
|
|
200
|
+
None,
|
|
201
|
+
["from-kwarg/**"],
|
|
202
|
+
path,
|
|
203
|
+
False,
|
|
204
|
+
None,
|
|
205
|
+
)
|
|
206
|
+
assert out["ignore"] == ["from-kwarg/**", "from-config/**"]
|
|
207
|
+
|
|
208
|
+
def it_ors_persist_across_kwarg_true_and_config_absent(cfg_dir):
|
|
209
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
210
|
+
_write(path, '[dirsql]\nignore = ["x"]\n')
|
|
211
|
+
out = resolve_config("/r", None, None, path, True, None)
|
|
212
|
+
assert out["persist"] is True
|
|
213
|
+
|
|
214
|
+
def it_ors_persist_across_kwarg_absent_and_config_true(cfg_dir):
|
|
215
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
216
|
+
_write(path, "[dirsql]\npersist = true\n")
|
|
217
|
+
out = resolve_config("/r", None, None, path, False, None)
|
|
218
|
+
assert out["persist"] is True
|
|
219
|
+
|
|
220
|
+
def it_explicit_persist_path_wins_over_config_persist_path(cfg_dir):
|
|
221
|
+
path = os.path.join(cfg_dir, ".dirsql.toml")
|
|
222
|
+
_write(path, '[dirsql]\npersist_path = "from-config.db"\n')
|
|
223
|
+
out = resolve_config(
|
|
224
|
+
"/r",
|
|
225
|
+
None,
|
|
226
|
+
None,
|
|
227
|
+
path,
|
|
228
|
+
False,
|
|
229
|
+
"/from-kwarg.db",
|
|
230
|
+
)
|
|
231
|
+
assert out["persist_path"] == "/from-kwarg.db"
|
|
@@ -141,6 +141,26 @@ for await (const event of db.watch()) { // AsyncIterable<RowEvent>
|
|
|
141
141
|
|
|
142
142
|
Returns an async iterable of `RowEvent` objects. The file watcher starts automatically on first iteration. The iterator never terminates on its own.
|
|
143
143
|
|
|
144
|
+
#### `serialization`
|
|
145
|
+
|
|
146
|
+
::: code-group
|
|
147
|
+
|
|
148
|
+
```python [Python]
|
|
149
|
+
vars(db) -> dict
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```rust [Rust]
|
|
153
|
+
db.config() -> DirSQLConfig
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```typescript [TypeScript]
|
|
157
|
+
JSON.stringify(db) // via db.toJSON()
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
:::
|
|
161
|
+
|
|
162
|
+
Returns the resolved construction state as a JSON-compatible value with fields `root`, `tables`, `ignore`, `persist`, `persist_path` (camelCase `persistPath` in TypeScript). Each table is `{ ddl, glob, strict }`. Excludes the original `config` path (already merged into `root` / `tables` / `ignore`), per-table `extract`, and per-table `name`. Available immediately after construction in Python and TypeScript; Rust's sync `build()` returns a ready instance.
|
|
163
|
+
|
|
144
164
|
---
|
|
145
165
|
|
|
146
166
|
## Table
|
|
@@ -4,7 +4,7 @@ name = "dirsql-py-ext"
|
|
|
4
4
|
# pypi/maturin handler can rewrite it via `write-version` before
|
|
5
5
|
# `maturin build`. `pyproject.toml` declares `dynamic = ["version"]`
|
|
6
6
|
# and maturin reads this field. Mirrors `packages/rust/Cargo.toml`.
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.13"
|
|
8
8
|
edition.workspace = true
|
|
9
9
|
publish = false
|
|
10
10
|
readme = "README.md"
|
|
@@ -141,6 +141,26 @@ for await (const event of db.watch()) { // AsyncIterable<RowEvent>
|
|
|
141
141
|
|
|
142
142
|
Returns an async iterable of `RowEvent` objects. The file watcher starts automatically on first iteration. The iterator never terminates on its own.
|
|
143
143
|
|
|
144
|
+
#### `serialization`
|
|
145
|
+
|
|
146
|
+
::: code-group
|
|
147
|
+
|
|
148
|
+
```python [Python]
|
|
149
|
+
vars(db) -> dict
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```rust [Rust]
|
|
153
|
+
db.config() -> DirSQLConfig
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```typescript [TypeScript]
|
|
157
|
+
JSON.stringify(db) // via db.toJSON()
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
:::
|
|
161
|
+
|
|
162
|
+
Returns the resolved construction state as a JSON-compatible value with fields `root`, `tables`, `ignore`, `persist`, `persist_path` (camelCase `persistPath` in TypeScript). Each table is `{ ddl, glob, strict }`. Excludes the original `config` path (already merged into `root` / `tables` / `ignore`), per-table `extract`, and per-table `name`. Available immediately after construction in Python and TypeScript; Rust's sync `build()` returns a ready instance.
|
|
163
|
+
|
|
144
164
|
---
|
|
145
165
|
|
|
146
166
|
## Table
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Integration tests for DirSQL config serialization (#194)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from dirsql import DirSQL, Table
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def empty_dir():
|
|
14
|
+
with tempfile.TemporaryDirectory() as d:
|
|
15
|
+
yield d
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _noop_extract(_path):
|
|
19
|
+
return []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def describe_DirSQL_serialization():
|
|
23
|
+
def describe_top_level_shape():
|
|
24
|
+
@pytest.mark.asyncio
|
|
25
|
+
async def it_exposes_resolved_state_via_vars(empty_dir):
|
|
26
|
+
"""`vars(db)` returns the resolved runtime state as a dict."""
|
|
27
|
+
db = DirSQL(
|
|
28
|
+
empty_dir,
|
|
29
|
+
tables=[
|
|
30
|
+
Table(
|
|
31
|
+
ddl="CREATE TABLE items (name TEXT)",
|
|
32
|
+
glob="items/*.json",
|
|
33
|
+
extract=_noop_extract,
|
|
34
|
+
)
|
|
35
|
+
],
|
|
36
|
+
ignore=["**/skip/**"],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
state = vars(db)
|
|
40
|
+
assert isinstance(state, dict)
|
|
41
|
+
assert set(state.keys()) == {
|
|
42
|
+
"root",
|
|
43
|
+
"tables",
|
|
44
|
+
"ignore",
|
|
45
|
+
"persist",
|
|
46
|
+
"persist_path",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@pytest.mark.asyncio
|
|
50
|
+
async def it_returns_json_serializable_dict(empty_dir):
|
|
51
|
+
"""`json.dumps(vars(db))` succeeds with the resolved state."""
|
|
52
|
+
db = DirSQL(
|
|
53
|
+
empty_dir,
|
|
54
|
+
tables=[
|
|
55
|
+
Table(
|
|
56
|
+
ddl="CREATE TABLE items (name TEXT)",
|
|
57
|
+
glob="items/*.json",
|
|
58
|
+
extract=_noop_extract,
|
|
59
|
+
)
|
|
60
|
+
],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
payload = json.dumps(vars(db))
|
|
64
|
+
parsed = json.loads(payload)
|
|
65
|
+
assert parsed["root"] == empty_dir
|
|
66
|
+
assert isinstance(parsed["tables"], list)
|
|
67
|
+
assert len(parsed["tables"]) == 1
|
|
68
|
+
assert parsed["tables"][0]["ddl"] == "CREATE TABLE items (name TEXT)"
|
|
69
|
+
assert parsed["tables"][0]["glob"] == "items/*.json"
|
|
70
|
+
assert parsed["tables"][0]["strict"] is False
|
|
71
|
+
assert parsed["ignore"] == []
|
|
72
|
+
assert parsed["persist"] is False
|
|
73
|
+
assert parsed["persist_path"] is None
|
|
74
|
+
|
|
75
|
+
@pytest.mark.asyncio
|
|
76
|
+
async def it_works_before_ready(empty_dir):
|
|
77
|
+
"""`vars(db)` is available immediately, before the scan finishes."""
|
|
78
|
+
db = DirSQL(
|
|
79
|
+
empty_dir,
|
|
80
|
+
tables=[
|
|
81
|
+
Table(
|
|
82
|
+
ddl="CREATE TABLE items (name TEXT)",
|
|
83
|
+
glob="items/*.json",
|
|
84
|
+
extract=_noop_extract,
|
|
85
|
+
)
|
|
86
|
+
],
|
|
87
|
+
)
|
|
88
|
+
# Read state before awaiting ready -- must not raise.
|
|
89
|
+
state = vars(db)
|
|
90
|
+
assert state["root"] == empty_dir
|
|
91
|
+
# And still works after ready, with the same shape.
|
|
92
|
+
await db.ready()
|
|
93
|
+
assert vars(db) == state
|
|
94
|
+
|
|
95
|
+
def describe_table_shape():
|
|
96
|
+
@pytest.mark.asyncio
|
|
97
|
+
async def it_excludes_extract_from_tables(empty_dir):
|
|
98
|
+
"""Each serialized table must NOT include the `extract` closure."""
|
|
99
|
+
db = DirSQL(
|
|
100
|
+
empty_dir,
|
|
101
|
+
tables=[
|
|
102
|
+
Table(
|
|
103
|
+
ddl="CREATE TABLE items (name TEXT)",
|
|
104
|
+
glob="items/*.json",
|
|
105
|
+
extract=_noop_extract,
|
|
106
|
+
)
|
|
107
|
+
],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
state = vars(db)
|
|
111
|
+
assert state["tables"]
|
|
112
|
+
for t in state["tables"]:
|
|
113
|
+
assert "extract" not in t
|
|
114
|
+
|
|
115
|
+
@pytest.mark.asyncio
|
|
116
|
+
async def it_excludes_name_from_tables(empty_dir):
|
|
117
|
+
"""Each serialized table must NOT include a `name` field."""
|
|
118
|
+
db = DirSQL(
|
|
119
|
+
empty_dir,
|
|
120
|
+
tables=[
|
|
121
|
+
Table(
|
|
122
|
+
ddl="CREATE TABLE items (name TEXT)",
|
|
123
|
+
glob="items/*.json",
|
|
124
|
+
extract=_noop_extract,
|
|
125
|
+
)
|
|
126
|
+
],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
state = vars(db)
|
|
130
|
+
for t in state["tables"]:
|
|
131
|
+
assert "name" not in t
|
|
132
|
+
|
|
133
|
+
def describe_defaults():
|
|
134
|
+
@pytest.mark.asyncio
|
|
135
|
+
async def it_defaults_strict_to_false(empty_dir):
|
|
136
|
+
db = DirSQL(
|
|
137
|
+
empty_dir,
|
|
138
|
+
tables=[
|
|
139
|
+
Table(
|
|
140
|
+
ddl="CREATE TABLE items (name TEXT)",
|
|
141
|
+
glob="items/*.json",
|
|
142
|
+
extract=_noop_extract,
|
|
143
|
+
)
|
|
144
|
+
],
|
|
145
|
+
)
|
|
146
|
+
state = vars(db)
|
|
147
|
+
assert state["tables"][0]["strict"] is False
|
|
148
|
+
|
|
149
|
+
@pytest.mark.asyncio
|
|
150
|
+
async def it_defaults_persist_to_false_and_persist_path_to_none(empty_dir):
|
|
151
|
+
db = DirSQL(
|
|
152
|
+
empty_dir,
|
|
153
|
+
tables=[
|
|
154
|
+
Table(
|
|
155
|
+
ddl="CREATE TABLE items (name TEXT)",
|
|
156
|
+
glob="items/*.json",
|
|
157
|
+
extract=_noop_extract,
|
|
158
|
+
)
|
|
159
|
+
],
|
|
160
|
+
)
|
|
161
|
+
state = vars(db)
|
|
162
|
+
assert state["persist"] is False
|
|
163
|
+
assert state["persist_path"] is None
|
|
164
|
+
|
|
165
|
+
def describe_persist_overrides():
|
|
166
|
+
@pytest.mark.asyncio
|
|
167
|
+
async def it_reflects_persist_true_and_custom_persist_path(empty_dir):
|
|
168
|
+
persist_path = os.path.join(empty_dir, "custom-cache.db")
|
|
169
|
+
db = DirSQL(
|
|
170
|
+
empty_dir,
|
|
171
|
+
tables=[
|
|
172
|
+
Table(
|
|
173
|
+
ddl="CREATE TABLE items (name TEXT)",
|
|
174
|
+
glob="items/*.json",
|
|
175
|
+
extract=_noop_extract,
|
|
176
|
+
)
|
|
177
|
+
],
|
|
178
|
+
persist=True,
|
|
179
|
+
persist_path=persist_path,
|
|
180
|
+
)
|
|
181
|
+
state = vars(db)
|
|
182
|
+
assert state["persist"] is True
|
|
183
|
+
assert state["persist_path"] == persist_path
|
|
184
|
+
|
|
185
|
+
def describe_ignore_patterns():
|
|
186
|
+
@pytest.mark.asyncio
|
|
187
|
+
async def it_includes_ignore_list(empty_dir):
|
|
188
|
+
db = DirSQL(
|
|
189
|
+
empty_dir,
|
|
190
|
+
tables=[
|
|
191
|
+
Table(
|
|
192
|
+
ddl="CREATE TABLE items (name TEXT)",
|
|
193
|
+
glob="items/*.json",
|
|
194
|
+
extract=_noop_extract,
|
|
195
|
+
)
|
|
196
|
+
],
|
|
197
|
+
ignore=["**/skip/**", "**/temp/**"],
|
|
198
|
+
)
|
|
199
|
+
state = vars(db)
|
|
200
|
+
assert state["ignore"] == ["**/skip/**", "**/temp/**"]
|
|
201
|
+
|
|
202
|
+
def describe_config_file():
|
|
203
|
+
@pytest.mark.asyncio
|
|
204
|
+
async def it_merges_root_tables_ignore_from_config(empty_dir):
|
|
205
|
+
"""A `.dirsql.toml` config feeds into the serialized state."""
|
|
206
|
+
cfg_path = os.path.join(empty_dir, ".dirsql.toml")
|
|
207
|
+
with open(cfg_path, "w") as f:
|
|
208
|
+
f.write(
|
|
209
|
+
"[dirsql]\n"
|
|
210
|
+
'root = "data"\n'
|
|
211
|
+
'ignore = ["node_modules/**"]\n'
|
|
212
|
+
"persist = true\n"
|
|
213
|
+
'persist_path = "cache.db"\n'
|
|
214
|
+
"\n"
|
|
215
|
+
"[[table]]\n"
|
|
216
|
+
'ddl = "CREATE TABLE items (_path TEXT)"\n'
|
|
217
|
+
'glob = "*.json"\n'
|
|
218
|
+
"strict = true\n"
|
|
219
|
+
)
|
|
220
|
+
# Create the directory the config expects so the background
|
|
221
|
+
# scan doesn't blow up later -- we don't await ready() here,
|
|
222
|
+
# but the underlying Rust constructor still needs it on its
|
|
223
|
+
# own thread.
|
|
224
|
+
os.makedirs(os.path.join(empty_dir, "data"), exist_ok=True)
|
|
225
|
+
|
|
226
|
+
db = DirSQL(config=cfg_path)
|
|
227
|
+
state = vars(db)
|
|
228
|
+
assert state["root"] == os.path.join(empty_dir, "data")
|
|
229
|
+
assert state["ignore"] == ["node_modules/**"]
|
|
230
|
+
assert state["persist"] is True
|
|
231
|
+
assert state["persist_path"] == os.path.join(empty_dir, "cache.db")
|
|
232
|
+
assert len(state["tables"]) == 1
|
|
233
|
+
assert state["tables"][0] == {
|
|
234
|
+
"ddl": "CREATE TABLE items (_path TEXT)",
|
|
235
|
+
"glob": "*.json",
|
|
236
|
+
"strict": True,
|
|
237
|
+
}
|
|
@@ -141,6 +141,26 @@ for await (const event of db.watch()) { // AsyncIterable<RowEvent>
|
|
|
141
141
|
|
|
142
142
|
Returns an async iterable of `RowEvent` objects. The file watcher starts automatically on first iteration. The iterator never terminates on its own.
|
|
143
143
|
|
|
144
|
+
#### `serialization`
|
|
145
|
+
|
|
146
|
+
::: code-group
|
|
147
|
+
|
|
148
|
+
```python [Python]
|
|
149
|
+
vars(db) -> dict
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```rust [Rust]
|
|
153
|
+
db.config() -> DirSQLConfig
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```typescript [TypeScript]
|
|
157
|
+
JSON.stringify(db) // via db.toJSON()
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
:::
|
|
161
|
+
|
|
162
|
+
Returns the resolved construction state as a JSON-compatible value with fields `root`, `tables`, `ignore`, `persist`, `persist_path` (camelCase `persistPath` in TypeScript). Each table is `{ ddl, glob, strict }`. Excludes the original `config` path (already merged into `root` / `tables` / `ignore`), per-table `extract`, and per-table `name`. Available immediately after construction in Python and TypeScript; Rust's sync `build()` returns a ready instance.
|
|
163
|
+
|
|
144
164
|
---
|
|
145
165
|
|
|
146
166
|
## Table
|