Skip to content

Commit 6a6af2b

Browse files
committed
Keep read checks independent of write limits and database creation
1 parent 72b2019 commit 6a6af2b

7 files changed

Lines changed: 64 additions & 7 deletions

File tree

‎src/openstatspec/api.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
install_in_place_transformation_schema as _install_in_place_schema,
3434
)
3535
from .transform import TransformationPlan
36+
from .sql.database_urls import require_existing_database_url
3637
from .sql.capabilities import (
3738
SPECIFICATION_COMMIT, SPECIFICATION_RELEASE, SPECIFICATION_STATUS,
3839
active_connection, catalog_binding,
@@ -53,8 +54,11 @@ def capability_matrix(
5354
blocks export unless the documented explicit loss opt-in exists; a plain
5455
fail-closed feature has no faithful writer route at all.
5556
"""
57+
if database_url:
58+
require_existing_database_url(database_url)
5659
declaration = {
5760
"specification": "OpenStatSpec",
61+
"database_io_policy": "openstatspec-database-io-v1",
5862
"specification_status": SPECIFICATION_STATUS,
5963
"specification_release": SPECIFICATION_RELEASE,
6064
"specification_commit": SPECIFICATION_COMMIT,

‎src/openstatspec/sql/capabilities.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import re
6+
import sys
67
from dataclasses import replace
78
from pathlib import Path
89
from typing import Any, Mapping
@@ -12,6 +13,7 @@
1213

1314
from .dolt_conformance import DoltConformanceSource, effective_limits as dolt_effective_limits
1415
from .normative import catalog
16+
from .database_urls import require_existing_database_url
1517
from .profiles import DOLT, MYSQL, POSTGRESQL, SQLITE, MYSQL_WIRE_PROFILES, SqlProfile
1618
from .profiles import validate_connection_url
1719
from ..core import UnsupportedOperationError
@@ -138,6 +140,8 @@ def profile_declarations(
138140
dolt_conformance_source: DoltConformanceSource | None = None,
139141
) -> dict[str, dict[str, Any]]:
140142
"""Declare every profile and, optionally, one active connection."""
143+
if database_url:
144+
require_existing_database_url(database_url)
141145
source = dolt_conformance_source
142146
active = (
143147
active_connection(database_url, dolt_conformance_source=source)
@@ -307,6 +311,7 @@ def read_profile(
307311
dolt_conformance_source: DoltConformanceSource | None = None,
308312
) -> tuple[SqlProfile, dict[str, Any]]:
309313
"""Identify a read connection without requiring evidence for Dolt writes."""
314+
require_existing_database_url(database_url)
310315
return _effective_profile(database_url, dolt_conformance_source, for_write=False)
311316

312317

@@ -331,7 +336,7 @@ def _effective_profile(
331336
if active["profile"] == "dolt" and not for_write:
332337
# Existing data is validated against the adapter's data model, not a
333338
# write-conformance envelope or INSERT statement/packet limits.
334-
return DOLT, active
339+
return replace(DOLT, max_source_variables=sys.maxsize), active
335340
dolt_declaration = None
336341
if active["profile"] == "dolt" and source is not None:
337342
dolt_declaration = source.require_exact_match(

‎src/openstatspec/sql/catalog_api.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
CATALOG_CONTRACT_ID, CATALOG_SCHEMA_VERSION, catalog as core_catalog,
1111
)
1212
from .profiles import validate_connection_url
13+
from .database_urls import require_existing_database_url
1314
from .workflow import (
1415
PROFILE_ID, PROFILE_SCHEMA_VERSION, TransformationError, workflow_catalog,
1516
)
@@ -55,6 +56,7 @@ def _assert_workflow(connection: Any, tables: Any) -> None:
5556

5657
def catalog_datasets(*, database_url: str, kind: str | None = None) -> dict[str, Any]:
5758
"""List source and/or derived datasets without mutating either catalog."""
59+
require_existing_database_url(database_url)
5860
if kind not in {None, "core", "derived"}:
5961
raise ValueError("kind must be core, derived, or None.")
6062
engine = create_engine(database_url)
@@ -96,6 +98,7 @@ def catalog_datasets(*, database_url: str, kind: str | None = None) -> dict[str,
9698

9799
def catalog_dataset(*, database_url: str, dataset_id: str, kind: str) -> dict[str, Any]:
98100
"""Return one public catalog record, variables, weight, and derived lineage."""
101+
require_existing_database_url(database_url)
99102
dataset_id = _uuid(dataset_id)
100103
if kind not in {"core", "derived"}:
101104
raise ValueError("kind must be core or derived.")

‎src/openstatspec/sql/database_urls.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
11
"""Database URL invariants shared by persistent catalog operations."""
22

3+
from pathlib import Path
4+
from urllib.parse import unquote
5+
36
from sqlalchemy.engine import make_url
47

58
from ..core import UnsupportedOperationError
69

710

11+
def require_existing_database_url(database_url: str) -> None:
12+
"""Prevent SQLite's implicit file creation during a read operation."""
13+
url = make_url(database_url)
14+
if url.get_backend_name() != "sqlite":
15+
return
16+
database = url.database or ""
17+
if database in {"", ":memory:", "file::memory:"} or url.query.get("mode") == "memory":
18+
return
19+
if str(url.query.get("uri", "")).lower() == "true" and database.startswith("file:"):
20+
database = unquote(database[5:])
21+
if not Path(database).is_file():
22+
raise UnsupportedOperationError("Read operations require an existing SQLite database file.")
23+
24+
825
def require_persistent_database_url(database_url: str) -> None:
926
"""Reject SQLite URLs whose catalog disappears with a connection or engine."""
1027
parsed_url = make_url(database_url)

‎src/openstatspec/sql/wide.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
store_imported_dataset as store_normative_dataset,
3333
)
3434
from .catalog_verification import verify_catalog_relations
35-
from .database_urls import require_persistent_database_url
35+
from .database_urls import require_existing_database_url, require_persistent_database_url
3636

3737
_IDENTIFIER = re.compile(r"[^a-zA-Z0-9_]+")
3838

@@ -648,6 +648,7 @@ def dolt_state_snapshot(
648648
*, database_url: str, dolt_conformance_source: Any | None = None,
649649
) -> dict[str, Any]:
650650
"""Return read-only branch, HEAD, status, and diff evidence for Dolt."""
651+
require_existing_database_url(database_url)
651652
validate_connection_url(database_url)
652653
active = active_connection(
653654
database_url, dolt_conformance_source=dolt_conformance_source,
@@ -1049,6 +1050,7 @@ def read_wide_dataset(
10491050
dolt_conformance_source: Any | None = None,
10501051
) -> tuple[dict[str, Any], list[dict[str, Any]], list[dict[str, Any]]]:
10511052
"""Read an export descriptor from the normative catalog without mutation."""
1053+
require_existing_database_url(database_url)
10521054
if profile is None:
10531055
profile, _active = read_profile(
10541056
database_url, dolt_conformance_source=dolt_conformance_source,

‎src/openstatspec/sql/workflow.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2492,7 +2492,9 @@ def reconcile_physical_removals(
24922492
def validate_derived_dataset(*, database_url: str, derived_dataset_id: str) -> dict[str, Any]:
24932493
"""Validate without creating or migrating catalog state."""
24942494
from .catalog_api import _assert_workflow
2495+
from .database_urls import require_existing_database_url
24952496

2497+
require_existing_database_url(database_url)
24962498
derived_id = _uuid(derived_dataset_id, "derived_dataset_id")
24972499
profile = validate_connection_url(database_url)
24982500
engine = _workflow_engine(database_url, profile.name)

‎tests/test_read_only_export.py‎

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,6 @@ def check(_connection, _cursor, statement, _parameters, _context, _many):
135135
def test_export_and_reads_leave_no_database_trace(read_catalog, tmp_path, suffix):
136136
url, snapshot, source = read_catalog
137137
before = snapshot()
138-
if make_url(url).get_backend_name() == "mysql":
139-
# This is the real server identity and the empty packaged registry.
140-
with pytest.raises(UnsupportedOperationError):
141-
wide.initialize_wide_catalog(database_url=url)
142138
output = tmp_path / ("export" + suffix)
143139
result = openstatspec.export_sav(database_url=url, dataset_id="sample", destination=output)
144140
assert "operation_id" not in result
@@ -216,7 +212,11 @@ def test_dolt_reads_do_not_enable_undeclared_writes(monkeypatch):
216212
"server_version": "2.3.0",
217213
}
218214
monkeypatch.setattr(capabilities, "active_connection", lambda *_a, **_k: active)
219-
assert capabilities.read_profile(url)[0].name == "dolt"
215+
profile = capabilities.read_profile(url)[0]
216+
assert profile.name == "dolt"
217+
# A table imported under a larger explicit write envelope stays readable.
218+
from openstatspec.sql.profiles import preflight
219+
preflight(profile, 1_016)
220220
with pytest.raises(UnsupportedOperationError):
221221
capabilities.effective_profile(url)
222222
with pytest.raises(UnsupportedOperationError):
@@ -228,6 +228,30 @@ def test_dolt_reads_do_not_enable_undeclared_writes(monkeypatch):
228228
)
229229

230230

231+
@pytest.mark.parametrize("operation", ["export", "validate", "capabilities", "list", "get", "derived", "dolt_state"])
232+
@pytest.mark.parametrize("uri", [False, True])
233+
def test_reads_do_not_create_a_missing_sqlite_file(tmp_path, operation, uri):
234+
path = tmp_path / "absent.sqlite"
235+
url = f"sqlite:///file:{path}?uri=true" if uri else f"sqlite:///{path}"
236+
calls = {
237+
"export": lambda: openstatspec.export_sav(database_url=url, dataset_id="sample", destination=tmp_path / "out.sav"),
238+
"validate": lambda: openstatspec.validate(database_url=url, dataset_id="sample"),
239+
"capabilities": lambda: openstatspec.capabilities(url),
240+
"list": lambda: openstatspec.list_datasets(database_url=url),
241+
"get": lambda: openstatspec.get_dataset(database_url=url, dataset_id=str(uuid4()), kind="core"),
242+
"derived": lambda: openstatspec.validate_derived(database_url=url, derived_dataset_id=str(uuid4())),
243+
"dolt_state": lambda: openstatspec.dolt_state_snapshot(database_url=url),
244+
}
245+
with pytest.raises(UnsupportedOperationError, match="existing SQLite"):
246+
calls[operation]()
247+
assert not path.exists()
248+
assert not (tmp_path / "out.sav").exists()
249+
250+
251+
def test_database_io_policy_is_explicit():
252+
assert openstatspec.capabilities()["database_io_policy"] == "openstatspec-database-io-v1"
253+
254+
231255
def test_derived_validation_does_not_initialize_catalog(tmp_path):
232256
url = f"sqlite:///{tmp_path / 'catalog.sqlite'}"
233257
openstatspec.initialize_catalog(database_url=url)

0 commit comments

Comments
 (0)