Skip to content

Commit ed1e959

Browse files
committed
fix: improve packaging of the schema and versioning on rust
1 parent 3603bc8 commit ed1e959

17 files changed

Lines changed: 118 additions & 299 deletions

File tree

packages/ducpy/.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# Test files
22
dist/
33
inputs/
4-
schema/*
5-
!schema/.gitkeep
64

75

86

packages/ducpy/crate/src/lib.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,45 @@ fn list_external_files(py: Python<'_>, buf: &[u8]) -> PyResult<PyObject> {
5858
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))
5959
}
6060

61+
/// Returns the current DUC schema version as a semver string (e.g. "3.0.0").
62+
#[pyfunction]
63+
fn get_schema_version() -> PyResult<String> {
64+
Ok(duc::db::bootstrap::CURRENT_SCHEMA_VERSION_SEMVER.into())
65+
}
66+
67+
/// Returns the raw integer schema version (e.g. 3000000).
68+
#[pyfunction]
69+
fn get_schema_version_int() -> PyResult<i64> {
70+
Ok(duc::db::bootstrap::current_schema_version_int())
71+
}
72+
73+
/// Returns the canonical `duc.sql` schema string.
74+
#[pyfunction]
75+
fn get_duc_schema_sql() -> PyResult<String> {
76+
Ok(duc::db::bootstrap::DUC_SCHEMA_SQL.into())
77+
}
78+
79+
/// Returns the `version_control.sql` schema string.
80+
#[pyfunction]
81+
fn get_version_control_schema_sql() -> PyResult<String> {
82+
Ok(duc::db::bootstrap::VERSION_CONTROL_SCHEMA_SQL.into())
83+
}
84+
85+
/// Returns the `search.sql` schema string.
86+
#[pyfunction]
87+
fn get_search_schema_sql() -> PyResult<String> {
88+
Ok(duc::db::bootstrap::SEARCH_SCHEMA_SQL.into())
89+
}
90+
91+
/// Returns all migrations as a list of `(from_version, to_version, sql)` tuples.
92+
#[pyfunction]
93+
fn get_migrations(_py: Python<'_>) -> PyResult<Vec<(i64, i64, String)>> {
94+
Ok(duc::db::bootstrap::MIGRATIONS
95+
.iter()
96+
.map(|(f, t, sql)| (*f, *t, sql.to_string()))
97+
.collect())
98+
}
99+
61100
/// Native duc file format operations.
62101
#[pymodule]
63102
fn ducpy_native(m: &Bound<'_, PyModule>) -> PyResult<()> {
@@ -66,5 +105,11 @@ fn ducpy_native(m: &Bound<'_, PyModule>) -> PyResult<()> {
66105
m.add_function(wrap_pyfunction!(serialize_duc, m)?)?;
67106
m.add_function(wrap_pyfunction!(get_external_file, m)?)?;
68107
m.add_function(wrap_pyfunction!(list_external_files, m)?)?;
108+
m.add_function(wrap_pyfunction!(get_schema_version, m)?)?;
109+
m.add_function(wrap_pyfunction!(get_schema_version_int, m)?)?;
110+
m.add_function(wrap_pyfunction!(get_duc_schema_sql, m)?)?;
111+
m.add_function(wrap_pyfunction!(get_version_control_schema_sql, m)?)?;
112+
m.add_function(wrap_pyfunction!(get_search_schema_sql, m)?)?;
113+
m.add_function(wrap_pyfunction!(get_migrations, m)?)?;
69114
Ok(())
70115
}

packages/ducpy/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@
33
"version": "0.0.0-development",
44
"private": true,
55
"scripts": {
6-
"sync:schema": "uv run python scripts/sync_schema.py",
7-
"build": "bash -c 'uv run python scripts/sync_schema.py && SETUPTOOLS_SCM_PRETEND_VERSION=${1} uv build' --",
6+
"build": "bash -c 'SETUPTOOLS_SCM_PRETEND_VERSION=${1} uv build' --",
87
"gen:docs": "bun clean:docs && cd docs && uv run make html",
98
"dev:docs": "uv run --with sphinx-autobuild sphinx-autobuild docs docs/_build/html --port 8240 --open-browser",
109
"clean:docs": "cd docs && uv run make clean",
1110
"test": "uv run -m pytest src/tests/src/test_*.py",
1211
"test:verbose": "uv run -m pytest -v src/tests/src/test_*.py",
13-
"prerelease": "uv sync && uv run python scripts/sync_schema.py && bun run test",
12+
"prerelease": "uv sync && bun run test",
1413
"semantic-release": "semantic-release"
1514
},
1615
"devDependencies": {

packages/ducpy/pyproject.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ python-packages = ["ducpy", "ducpy_native"]
3636
module-name = "ducpy_native"
3737
include = [
3838
{ path = "LICENSE", format = "sdist" },
39-
{ path = "schema/**/*", format = "sdist" },
40-
{ path = "schema/**/*", format = "wheel" }
4139
]
4240

4341
[tool.setuptools]

packages/ducpy/release.config.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ module.exports = {
1616
"@semantic-release/exec",
1717
{
1818
prepareCmd:
19-
"uv sync && uv run python scripts/sync_schema.py && sed -i 's/^version = \".*\"$/version = \"${nextRelease.version}\"/' crate/Cargo.toml && SETUPTOOLS_SCM_PRETEND_VERSION=${nextRelease.version} uv build --sdist",
19+
"uv sync && sed -i 's/^version = \".*\"$/version = \"${nextRelease.version}\"/' crate/Cargo.toml && SETUPTOOLS_SCM_PRETEND_VERSION=${nextRelease.version} uv build --sdist",
2020
publishCmd: "uv publish --token ${process.env.PYPI_TOKEN} dist/*.tar.gz",
2121
},
2222
],

packages/ducpy/scripts/sync_schema.py

Lines changed: 0 additions & 24 deletions
This file was deleted.

packages/ducpy/setup.py

Lines changed: 1 addition & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,8 @@
1-
import os
2-
import re
3-
from pathlib import Path
4-
51
from setuptools import setup
62

7-
# Path where _version.py will be written
8-
VERSION_PY_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'src', 'ducpy', '_version.py')
9-
10-
11-
def find_schema_file_path():
12-
env_path = os.environ.get('DUC_SCHEMA_DIR')
13-
if env_path:
14-
candidate = Path(env_path) / 'duc.sql'
15-
if candidate.exists():
16-
return str(candidate)
17-
18-
current = Path(__file__).resolve()
19-
for parent in current.parents:
20-
candidate = parent / 'schema' / 'duc.sql'
21-
if candidate.exists():
22-
return str(candidate)
23-
24-
return None
25-
26-
def get_schema_version_from_sql(sql_file_path):
27-
"""Extract schema version from PRAGMA user_version in duc.sql.
28-
29-
Format: user_version = 3000000 → version "3.0.0"
30-
Encoding: major * 1_000_000 + minor * 1_000 + patch
31-
"""
32-
try:
33-
with open(sql_file_path, 'r') as f:
34-
content = f.read()
35-
match = re.search(r'PRAGMA\s+user_version\s*=\s*(\d+)', content)
36-
if match:
37-
version_int = int(match.group(1))
38-
major = version_int // 1_000_000
39-
minor = (version_int % 1_000_000) // 1_000
40-
patch = version_int % 1_000
41-
return f"{major}.{minor}.{patch}"
42-
except FileNotFoundError:
43-
return None
44-
except Exception as e:
45-
print(f"Warning: Could not parse schema version from {sql_file_path}: {e}. Defaulting to 0.0.0.")
46-
return "0.0.0"
47-
48-
def generate_version_py():
49-
"""Generates the _version.py file with DUC_SCHEMA_VERSION."""
50-
schema_file_path = find_schema_file_path()
51-
schema_version = get_schema_version_from_sql(schema_file_path) if schema_file_path else None
52-
53-
if schema_version is not None:
54-
print(f"Generating DUC schema version file at: {VERSION_PY_PATH} with version: {schema_version}")
55-
os.makedirs(os.path.dirname(VERSION_PY_PATH), exist_ok=True)
56-
with open(VERSION_PY_PATH, 'w') as f:
57-
f.write(f'# This file is auto-generated by setup.py when the package is built.\n')
58-
f.write(f'# Do not edit this file manually.\n')
59-
f.write(f'DUC_SCHEMA_VERSION = "{schema_version}"\n')
60-
elif os.path.exists(VERSION_PY_PATH):
61-
print(f"DUC schema file not found. Using existing _version.py: {VERSION_PY_PATH}")
62-
else:
63-
print(f"DUC schema file not found and _version.py does not exist. Generating with default version 0.0.0 at: {VERSION_PY_PATH}")
64-
os.makedirs(os.path.dirname(VERSION_PY_PATH), exist_ok=True)
65-
with open(VERSION_PY_PATH, 'w') as f:
66-
f.write(f'# This file is auto-generated by setup.py (fallback).\n')
67-
f.write(f'DUC_SCHEMA_VERSION = "0.0.0"\n')
68-
69-
# --- Main part of setup.py ---
70-
71-
# Generate the version file when setup.py is processed by the build system.
72-
generate_version_py()
73-
743
# Minimal setup call.
754
# Most package metadata (name, version via setuptools-scm, etc.) and
765
# package discovery are handled by pyproject.toml.
776
# This setup.py is present for compatibility and can be used for tasks
787
# like C-extension building or build-time file generation if needed.
79-
setup()
8+
setup()

packages/ducpy/src/ducpy/_version.py

Lines changed: 0 additions & 3 deletions
This file was deleted.

packages/ducpy/src/ducpy/builders/sql_builder.py

Lines changed: 16 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -29,67 +29,33 @@
2929
from __future__ import annotations
3030

3131
import os
32-
import re
3332
import sqlite3
3433
import tempfile
3534
from pathlib import Path
3635
from typing import Any, List, Optional, Union
3736

38-
__all__ = ["DucSQL"]
39-
40-
41-
_MIGRATION_RE = re.compile(r"^(\d+)_to_(\d+)$")
37+
import ducpy_native
4238

43-
44-
def _find_schema_dir() -> Optional[Path]:
45-
env_path = os.environ.get("DUC_SCHEMA_DIR")
46-
if env_path:
47-
candidate = Path(env_path)
48-
if (candidate / "duc.sql").exists():
49-
return candidate
50-
51-
current = Path(__file__).resolve()
52-
for parent in current.parents:
53-
candidate = parent / "schema"
54-
if (candidate / "duc.sql").exists():
55-
return candidate
56-
return None
39+
__all__ = ["DucSQL"]
5740

5841

5942
def _get_current_schema_version() -> int:
60-
"""Read the target user_version from duc.sql — mirrors Rust's CURRENT_VERSION."""
61-
schema_dir = _find_schema_dir()
62-
if schema_dir is None:
63-
return 0
64-
duc_sql = (schema_dir / "duc.sql").read_text(encoding="utf-8")
65-
m = re.search(r"PRAGMA\s+user_version\s*=\s*(\d+)", duc_sql, re.IGNORECASE)
66-
return int(m.group(1)) if m else 0
43+
"""Current schema version integer (e.g. 3000000) — from the Rust crate."""
44+
return ducpy_native.get_schema_version_int()
6745

6846

6947
def _read_migrations() -> list[tuple[int, int, str]]:
70-
"""Load all migration SQL files from schema/migrations/, sorted by from_version."""
71-
schema_dir = _find_schema_dir()
72-
if schema_dir is None:
73-
return []
74-
migrations_dir = schema_dir / "migrations"
75-
if not migrations_dir.exists():
76-
return []
77-
result: list[tuple[int, int, str]] = []
78-
for path in sorted(migrations_dir.glob("*.sql")):
79-
m = _MIGRATION_RE.match(path.stem)
80-
if m:
81-
result.append((int(m.group(1)), int(m.group(2)), path.read_text(encoding="utf-8")))
82-
result.sort(key=lambda x: x[0])
83-
return result
48+
"""All migrations as (from_version, to_version, sql) — from the Rust crate."""
49+
raw = ducpy_native.get_migrations()
50+
# get_migrations returns list of tuples with i64 values
51+
return [(int(f), int(t), sql) for f, t, sql in raw]
8452

8553

8654
def _apply_migrations(conn: sqlite3.Connection) -> None:
8755
"""Walk the migration chain until user_version reaches the current schema version.
8856
89-
Mirrors the migration logic in Rust's ``bootstrap.rs``:
90-
reads ``schema/migrations/{from}_to_{to}.sql`` files in order and executes
91-
them until ``PRAGMA user_version`` matches the version declared in ``duc.sql``.
92-
Safe to call on already-current or brand-new databases.
57+
Mirrors the migration logic in Rust's ``bootstrap.rs``. Safe to call on
58+
already-current or brand-new databases.
9359
"""
9460
user_version: int = conn.execute("PRAGMA user_version").fetchone()[0]
9561
if user_version == 0:
@@ -112,18 +78,12 @@ def _apply_migrations(conn: sqlite3.Connection) -> None:
11278

11379

11480
def _read_schema_sql() -> str:
115-
schema_dir = _find_schema_dir()
116-
if schema_dir is None:
117-
raise FileNotFoundError(
118-
"Could not locate schema/duc.sql. "
119-
"Ensure you are running from within the DUC repository."
120-
)
121-
parts: list[str] = []
122-
for filename in ("duc.sql", "version_control.sql", "search.sql"):
123-
path = schema_dir / filename
124-
if path.exists():
125-
parts.append(path.read_text(encoding="utf-8"))
126-
return "\n".join(parts)
81+
"""Concatenate the three schema SQL strings shipped by the Rust crate."""
82+
return "\n".join([
83+
ducpy_native.get_duc_schema_sql(),
84+
ducpy_native.get_version_control_schema_sql(),
85+
ducpy_native.get_search_schema_sql(),
86+
])
12787

12888

12989
def _apply_pragmas(conn: sqlite3.Connection) -> None:

packages/ducpy/src/ducpy/serialize.py

Lines changed: 2 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -29,61 +29,8 @@ def __init__(self, failures: List[str]):
2929
super().__init__("Embedded code validation failed:\n" + "\n".join(f"- {failure}" for failure in failures))
3030

3131

32-
def _find_schema_file() -> Path | None:
33-
env_path = Path(os.environ["DUC_SCHEMA_DIR"]) if "DUC_SCHEMA_DIR" in os.environ else None
34-
if env_path is not None:
35-
candidate = env_path / "duc.sql"
36-
if candidate.exists():
37-
return candidate
38-
39-
current = Path(__file__).resolve()
40-
for parent in current.parents:
41-
candidate = parent / "schema" / "duc.sql"
42-
if candidate.exists():
43-
return candidate
44-
45-
return None
46-
47-
48-
def _decode_user_version_to_semver(user_version: int) -> str:
49-
"""Decode sqlite-style schema user_version to semver.
50-
51-
Encoding convention:
52-
major * 1_000_000 + minor * 1_000 + patch
53-
"""
54-
if user_version < 0:
55-
return "0.0.0"
56-
57-
major = user_version // 1_000_000
58-
minor = (user_version % 1_000_000) // 1_000
59-
patch = user_version % 1_000
60-
return f"{major}.{minor}.{patch}"
61-
62-
63-
def _read_schema_version_fallback() -> str:
64-
"""Resolve schema version directly from repository `schema/duc.sql`.
65-
66-
This is used when `ducpy._version` isn't available (for example, in clean
67-
CI environments before setup-time generation has run).
68-
"""
69-
try:
70-
schema_path = _find_schema_file()
71-
if schema_path is None:
72-
return "0.0.0"
73-
content = schema_path.read_text(encoding="utf-8")
74-
match = re.search(r"PRAGMA\s+user_version\s*=\s*(\d+)\s*;", content)
75-
if match:
76-
return _decode_user_version_to_semver(int(match.group(1)))
77-
except Exception as exc: # pragma: no cover - defensive fallback for CI/runtime variance
78-
logger.warning("Failed to resolve schema version fallback from duc.sql: %s", exc)
79-
80-
return "0.0.0"
81-
82-
83-
try:
84-
from ducpy._version import DUC_SCHEMA_VERSION
85-
except ModuleNotFoundError:
86-
DUC_SCHEMA_VERSION = _read_schema_version_fallback()
32+
# Single source of truth for the schema version — comes from the Rust crate.
33+
DUC_SCHEMA_VERSION = ducpy_native.get_schema_version()
8734

8835
# Map Python element class names → Rust serde type tag strings.
8936
_ELEMENT_CLASS_TO_TYPE: Dict[str, str] = {

0 commit comments

Comments
 (0)