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.
Files changed (135) hide show
  1. {dirsql-0.3.12 → dirsql-0.3.13}/Cargo.lock +1 -1
  2. {dirsql-0.3.12 → dirsql-0.3.13}/PKG-INFO +1 -1
  3. {dirsql-0.3.12 → dirsql-0.3.13}/dirsql/_async.py +17 -0
  4. dirsql-0.3.13/dirsql/resolve_config.py +43 -0
  5. dirsql-0.3.13/dirsql/resolve_config_test.py +231 -0
  6. {dirsql-0.3.12 → dirsql-0.3.13}/docs/api/index.md +20 -0
  7. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/Cargo.toml +1 -1
  8. {dirsql-0.3.12/packages/rust → dirsql-0.3.13/packages/python}/docs/api/index.md +20 -0
  9. dirsql-0.3.13/packages/python/tests/integration/test_serialization.py +237 -0
  10. {dirsql-0.3.12/packages/python → dirsql-0.3.13/packages/rust}/docs/api/index.md +20 -0
  11. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/lib.rs +90 -0
  12. dirsql-0.3.13/packages/rust/tests/serialization.rs +161 -0
  13. {dirsql-0.3.12 → dirsql-0.3.13}/Cargo.toml +0 -0
  14. {dirsql-0.3.12 → dirsql-0.3.13}/README.md +0 -0
  15. {dirsql-0.3.12 → dirsql-0.3.13}/dirsql/__init__.py +0 -0
  16. {dirsql-0.3.12 → dirsql-0.3.13}/dirsql/_cli/__init__.py +0 -0
  17. {dirsql-0.3.12 → dirsql-0.3.13}/dirsql/_cli/binary_path.py +0 -0
  18. {dirsql-0.3.12 → dirsql-0.3.13}/dirsql/_cli/is_windows.py +0 -0
  19. {dirsql-0.3.12 → dirsql-0.3.13}/dirsql/_cli/main.py +0 -0
  20. {dirsql-0.3.12 → dirsql-0.3.13}/dirsql/test_async.py +0 -0
  21. {dirsql-0.3.12 → dirsql-0.3.13}/docs/.claude/CLAUDE.md +0 -0
  22. {dirsql-0.3.12 → dirsql-0.3.13}/docs/.vitepress/config.ts +0 -0
  23. {dirsql-0.3.12 → dirsql-0.3.13}/docs/.vitepress/theme/index.ts +0 -0
  24. {dirsql-0.3.12 → dirsql-0.3.13}/docs/.vitepress/theme/lang.ts +0 -0
  25. {dirsql-0.3.12 → dirsql-0.3.13}/docs/AGENTS.md +0 -0
  26. {dirsql-0.3.12 → dirsql-0.3.13}/docs/cli/config.md +0 -0
  27. {dirsql-0.3.12 → dirsql-0.3.13}/docs/cli/http-api.md +0 -0
  28. {dirsql-0.3.12 → dirsql-0.3.13}/docs/cli/index.md +0 -0
  29. {dirsql-0.3.12 → dirsql-0.3.13}/docs/cli/init.md +0 -0
  30. {dirsql-0.3.12 → dirsql-0.3.13}/docs/cli/server.md +0 -0
  31. {dirsql-0.3.12 → dirsql-0.3.13}/docs/getting-started.md +0 -0
  32. {dirsql-0.3.12 → dirsql-0.3.13}/docs/guide/async.md +0 -0
  33. {dirsql-0.3.12 → dirsql-0.3.13}/docs/guide/crdt.md +0 -0
  34. {dirsql-0.3.12 → dirsql-0.3.13}/docs/guide/persistence.md +0 -0
  35. {dirsql-0.3.12 → dirsql-0.3.13}/docs/guide/querying.md +0 -0
  36. {dirsql-0.3.12 → dirsql-0.3.13}/docs/guide/tables.md +0 -0
  37. {dirsql-0.3.12 → dirsql-0.3.13}/docs/guide/watching.md +0 -0
  38. {dirsql-0.3.12 → dirsql-0.3.13}/docs/index.md +0 -0
  39. {dirsql-0.3.12 → dirsql-0.3.13}/docs/migrations.md +0 -0
  40. {dirsql-0.3.12 → dirsql-0.3.13}/docs/package.json +0 -0
  41. {dirsql-0.3.12 → dirsql-0.3.13}/docs/playwright.config.ts +0 -0
  42. {dirsql-0.3.12 → dirsql-0.3.13}/docs/pnpm-lock.yaml +0 -0
  43. {dirsql-0.3.12 → dirsql-0.3.13}/docs/pnpm-workspace.yaml +0 -0
  44. {dirsql-0.3.12 → dirsql-0.3.13}/docs/tests/integration/home.spec.ts +0 -0
  45. {dirsql-0.3.12 → dirsql-0.3.13}/docs/tests/integration/language-flag.spec.ts +0 -0
  46. {dirsql-0.3.12 → dirsql-0.3.13}/docs/tests/unit/config.test.ts +0 -0
  47. {dirsql-0.3.12 → dirsql-0.3.13}/docs/tests/unit/lang.test.ts +0 -0
  48. {dirsql-0.3.12 → dirsql-0.3.13}/docs/vitest.config.ts +0 -0
  49. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/README.md +0 -0
  50. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/conftest.py +0 -0
  51. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/.claude/CLAUDE.md +0 -0
  52. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/.vitepress/config.ts +0 -0
  53. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  54. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  55. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/AGENTS.md +0 -0
  56. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/cli/config.md +0 -0
  57. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/cli/http-api.md +0 -0
  58. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/cli/index.md +0 -0
  59. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/cli/init.md +0 -0
  60. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/cli/server.md +0 -0
  61. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/getting-started.md +0 -0
  62. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/guide/async.md +0 -0
  63. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/guide/crdt.md +0 -0
  64. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/guide/persistence.md +0 -0
  65. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/guide/querying.md +0 -0
  66. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/guide/tables.md +0 -0
  67. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/guide/watching.md +0 -0
  68. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/index.md +0 -0
  69. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/migrations.md +0 -0
  70. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/package.json +0 -0
  71. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/playwright.config.ts +0 -0
  72. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/pnpm-lock.yaml +0 -0
  73. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/pnpm-workspace.yaml +0 -0
  74. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/tests/integration/home.spec.ts +0 -0
  75. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
  76. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/tests/unit/config.test.ts +0 -0
  77. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/tests/unit/lang.test.ts +0 -0
  78. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/docs/vitest.config.ts +0 -0
  79. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/src/lib.rs +0 -0
  80. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/__init__.py +0 -0
  81. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/conftest.py +0 -0
  82. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/e2e/__init__.py +0 -0
  83. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/integration/__init__.py +0 -0
  84. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/integration/test_async_dirsql.py +0 -0
  85. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/integration/test_binding.py +0 -0
  86. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/integration/test_dirsql.py +0 -0
  87. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/integration/test_docs_examples.py +0 -0
  88. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/integration/test_docs_gaps.py +0 -0
  89. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/integration/test_from_config.py +0 -0
  90. {dirsql-0.3.12 → dirsql-0.3.13}/packages/python/tests/integration/test_persist.py +0 -0
  91. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/Cargo.toml +0 -0
  92. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/README.md +0 -0
  93. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/benches/db_bench.rs +0 -0
  94. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/benches/differ_bench.rs +0 -0
  95. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/benches/matcher_bench.rs +0 -0
  96. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/benches/scanner_bench.rs +0 -0
  97. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/cli/config.md +0 -0
  98. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/cli/http-api.md +0 -0
  99. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/cli/index.md +0 -0
  100. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/cli/init.md +0 -0
  101. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/cli/server.md +0 -0
  102. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/getting-started.md +0 -0
  103. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/guide/async.md +0 -0
  104. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/guide/crdt.md +0 -0
  105. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/guide/persistence.md +0 -0
  106. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/guide/querying.md +0 -0
  107. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/guide/tables.md +0 -0
  108. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/guide/watching.md +0 -0
  109. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/index.md +0 -0
  110. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/docs/migrations.md +0 -0
  111. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/bin/dirsql.rs +0 -0
  112. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/cli/init.rs +0 -0
  113. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/cli/mod.rs +0 -0
  114. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/cli/router.rs +0 -0
  115. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/cli/serialize.rs +0 -0
  116. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/cli/server.rs +0 -0
  117. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/config.rs +0 -0
  118. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/db.rs +0 -0
  119. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/differ.rs +0 -0
  120. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/matcher.rs +0 -0
  121. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/persist.rs +0 -0
  122. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/scanner.rs +0 -0
  123. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/src/watcher.rs +0 -0
  124. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/async_sdk.rs +0 -0
  125. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/cli_e2e.rs +0 -0
  126. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/cli_integration.rs +0 -0
  127. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/docs_examples.rs +0 -0
  128. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/docs_gaps.rs +0 -0
  129. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/from_config.rs +0 -0
  130. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/init_e2e.rs +0 -0
  131. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/init_integration.rs +0 -0
  132. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/persist.rs +0 -0
  133. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/readonly_query.rs +0 -0
  134. {dirsql-0.3.12 → dirsql-0.3.13}/packages/rust/tests/sdk.rs +0 -0
  135. {dirsql-0.3.12 → dirsql-0.3.13}/pyproject.toml +0 -0
@@ -499,7 +499,7 @@ dependencies = [
499
499
 
500
500
  [[package]]
501
501
  name = "dirsql-py-ext"
502
- version = "0.3.12"
502
+ version = "0.3.13"
503
503
  dependencies = [
504
504
  "dirsql",
505
505
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.3.12
3
+ Version: 0.3.13
4
4
  Requires-Dist: pytest>=8 ; extra == 'dev'
5
5
  Requires-Dist: pytest-describe>=2 ; extra == 'dev'
6
6
  Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
@@ -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.12"
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