From 66aec99aaabf6ac16c25b8ada3a1fb98d1cd0646 Mon Sep 17 00:00:00 2001 From: Jorge Soares Date: Wed, 3 Jun 2026 00:51:22 +0100 Subject: [PATCH 1/2] fix(ducrs): expose the schema folder in the bundle --- packages/ducrs/.gitignore | 6 +++- packages/ducrs/build.rs | 68 +++++++++++++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/packages/ducrs/.gitignore b/packages/ducrs/.gitignore index d01bd1a9..5a93358b 100644 --- a/packages/ducrs/.gitignore +++ b/packages/ducrs/.gitignore @@ -18,4 +18,8 @@ Cargo.lock # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +#.idea/ + + +schema/* +!schema/.gitkeep \ No newline at end of file diff --git a/packages/ducrs/build.rs b/packages/ducrs/build.rs index c187ba52..17dbc5fa 100644 --- a/packages/ducrs/build.rs +++ b/packages/ducrs/build.rs @@ -25,6 +25,7 @@ fn decode_user_version_to_semver(user_version: u32) -> String { } fn find_schema_dir(manifest_dir: &Path) -> Result> { + // 1. Explicit override. if let Ok(path) = env::var("DUC_SCHEMA_DIR") { let candidate = PathBuf::from(path); if candidate.join("duc.sql").is_file() { @@ -32,14 +33,21 @@ fn find_schema_dir(manifest_dir: &Path) -> Result Result Result<(), Box> { + if dst.exists() { + let _ = fs::remove_dir_all(dst); + } + fs::create_dir_all(dst)?; + + for entry in fs::read_dir(src)? { + let entry = entry?; + let from = entry.path(); + let name = match from.file_name() { + Some(n) => n, + None => continue, + }; + let to = dst.join(name); + + let meta = entry.metadata()?; + if meta.is_dir() { + mirror_dir(&from, &to)?; + } else { + fs::copy(&from, &to)?; + } + } + Ok(()) +} + +fn mirror_dir(src: &Path, dst: &Path) -> Result<(), Box> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let from = entry.path(); + let name = match from.file_name() { + Some(n) => n, + None => continue, + }; + let to = dst.join(name); + let meta = entry.metadata()?; + if meta.is_dir() { + mirror_dir(&from, &to)?; + } else { + fs::copy(&from, &to)?; + } + } + Ok(()) +} + fn main() -> Result<(), Box> { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); let schema_dir = find_schema_dir(&manifest_dir)?; From 9a1c3008f566705ba512c64489bf8fd48947a571 Mon Sep 17 00:00:00 2001 From: Jorge Soares Date: Wed, 3 Jun 2026 11:21:50 +0100 Subject: [PATCH 2/2] fix: improve packaging of the schema and versioning on rust --- packages/ducpy/.gitignore | 2 - packages/ducpy/crate/src/lib.rs | 45 ++++++++++ packages/ducpy/package.json | 5 +- packages/ducpy/pyproject.toml | 2 - packages/ducpy/release.config.cjs | 2 +- packages/ducpy/scripts/sync_schema.py | 24 ------ packages/ducpy/setup.py | 73 +---------------- packages/ducpy/src/ducpy/_version.py | 3 - .../ducpy/src/ducpy/builders/sql_builder.py | 72 ++++------------ packages/ducpy/src/ducpy/serialize.py | 57 +------------ packages/ducpy/src/ducpy_native/__init__.py | 27 ++++-- packages/ducrs/.gitignore | 2 - packages/ducrs/Cargo.toml | 4 +- packages/ducrs/build.rs | 82 ++++--------------- packages/ducrs/schema | 1 + packages/ducrs/src/db/bootstrap.rs | 14 ++++ packages/ducrs/src/db/mod.rs | 2 +- 17 files changed, 118 insertions(+), 299 deletions(-) delete mode 100644 packages/ducpy/scripts/sync_schema.py delete mode 100644 packages/ducpy/src/ducpy/_version.py create mode 120000 packages/ducrs/schema diff --git a/packages/ducpy/.gitignore b/packages/ducpy/.gitignore index 0b586b78..9bf61158 100644 --- a/packages/ducpy/.gitignore +++ b/packages/ducpy/.gitignore @@ -1,8 +1,6 @@ # Test files dist/ inputs/ -schema/* -!schema/.gitkeep diff --git a/packages/ducpy/crate/src/lib.rs b/packages/ducpy/crate/src/lib.rs index b6c9b35b..68418990 100644 --- a/packages/ducpy/crate/src/lib.rs +++ b/packages/ducpy/crate/src/lib.rs @@ -58,6 +58,45 @@ fn list_external_files(py: Python<'_>, buf: &[u8]) -> PyResult { .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}"))) } +/// Returns the current DUC schema version as a semver string (e.g. "3.0.0"). +#[pyfunction] +fn get_schema_version() -> PyResult { + Ok(duc::db::bootstrap::CURRENT_SCHEMA_VERSION_SEMVER.into()) +} + +/// Returns the raw integer schema version (e.g. 3000000). +#[pyfunction] +fn get_schema_version_int() -> PyResult { + Ok(duc::db::bootstrap::current_schema_version_int()) +} + +/// Returns the canonical `duc.sql` schema string. +#[pyfunction] +fn get_duc_schema_sql() -> PyResult { + Ok(duc::db::bootstrap::DUC_SCHEMA_SQL.into()) +} + +/// Returns the `version_control.sql` schema string. +#[pyfunction] +fn get_version_control_schema_sql() -> PyResult { + Ok(duc::db::bootstrap::VERSION_CONTROL_SCHEMA_SQL.into()) +} + +/// Returns the `search.sql` schema string. +#[pyfunction] +fn get_search_schema_sql() -> PyResult { + Ok(duc::db::bootstrap::SEARCH_SCHEMA_SQL.into()) +} + +/// Returns all migrations as a list of `(from_version, to_version, sql)` tuples. +#[pyfunction] +fn get_migrations(_py: Python<'_>) -> PyResult> { + Ok(duc::db::bootstrap::MIGRATIONS + .iter() + .map(|(f, t, sql)| (*f, *t, sql.to_string())) + .collect()) +} + /// Native duc file format operations. #[pymodule] fn ducpy_native(m: &Bound<'_, PyModule>) -> PyResult<()> { @@ -66,5 +105,11 @@ fn ducpy_native(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(serialize_duc, m)?)?; m.add_function(wrap_pyfunction!(get_external_file, m)?)?; m.add_function(wrap_pyfunction!(list_external_files, m)?)?; + m.add_function(wrap_pyfunction!(get_schema_version, m)?)?; + m.add_function(wrap_pyfunction!(get_schema_version_int, m)?)?; + m.add_function(wrap_pyfunction!(get_duc_schema_sql, m)?)?; + m.add_function(wrap_pyfunction!(get_version_control_schema_sql, m)?)?; + m.add_function(wrap_pyfunction!(get_search_schema_sql, m)?)?; + m.add_function(wrap_pyfunction!(get_migrations, m)?)?; Ok(()) } diff --git a/packages/ducpy/package.json b/packages/ducpy/package.json index da0ff355..befb3498 100644 --- a/packages/ducpy/package.json +++ b/packages/ducpy/package.json @@ -3,14 +3,13 @@ "version": "0.0.0-development", "private": true, "scripts": { - "sync:schema": "uv run python scripts/sync_schema.py", - "build": "bash -c 'uv run python scripts/sync_schema.py && SETUPTOOLS_SCM_PRETEND_VERSION=${1} uv build' --", + "build": "bash -c 'SETUPTOOLS_SCM_PRETEND_VERSION=${1} uv build' --", "gen:docs": "bun clean:docs && cd docs && uv run make html", "dev:docs": "uv run --with sphinx-autobuild sphinx-autobuild docs docs/_build/html --port 8240 --open-browser", "clean:docs": "cd docs && uv run make clean", "test": "uv run -m pytest src/tests/src/test_*.py", "test:verbose": "uv run -m pytest -v src/tests/src/test_*.py", - "prerelease": "uv sync && uv run python scripts/sync_schema.py && bun run test", + "prerelease": "uv sync && bun run test", "semantic-release": "semantic-release" }, "devDependencies": { diff --git a/packages/ducpy/pyproject.toml b/packages/ducpy/pyproject.toml index b1797b25..7ef10e21 100644 --- a/packages/ducpy/pyproject.toml +++ b/packages/ducpy/pyproject.toml @@ -36,8 +36,6 @@ python-packages = ["ducpy", "ducpy_native"] module-name = "ducpy_native" include = [ { path = "LICENSE", format = "sdist" }, - { path = "schema/**/*", format = "sdist" }, - { path = "schema/**/*", format = "wheel" } ] [tool.setuptools] diff --git a/packages/ducpy/release.config.cjs b/packages/ducpy/release.config.cjs index dc766d5b..6db63b4c 100644 --- a/packages/ducpy/release.config.cjs +++ b/packages/ducpy/release.config.cjs @@ -16,7 +16,7 @@ module.exports = { "@semantic-release/exec", { prepareCmd: - "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", + "uv sync && sed -i 's/^version = \".*\"$/version = \"${nextRelease.version}\"/' crate/Cargo.toml && SETUPTOOLS_SCM_PRETEND_VERSION=${nextRelease.version} uv build --sdist", publishCmd: "uv publish --token ${process.env.PYPI_TOKEN} dist/*.tar.gz", }, ], diff --git a/packages/ducpy/scripts/sync_schema.py b/packages/ducpy/scripts/sync_schema.py deleted file mode 100644 index cd653500..00000000 --- a/packages/ducpy/scripts/sync_schema.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -import shutil -from pathlib import Path - - -def main() -> None: - package_root = Path(__file__).resolve().parents[1] - repo_root = package_root.parents[1] - source = repo_root / "schema" - destination = package_root / "schema" - - if not source.joinpath("duc.sql").exists(): - raise FileNotFoundError(f"Missing source schema at {source}") - - if destination.exists(): - shutil.rmtree(destination) - - shutil.copytree(source, destination) - print(f"Synced schema: {source} -> {destination}") - - -if __name__ == "__main__": - main() diff --git a/packages/ducpy/setup.py b/packages/ducpy/setup.py index 3ff7ca20..cd40803a 100644 --- a/packages/ducpy/setup.py +++ b/packages/ducpy/setup.py @@ -1,79 +1,8 @@ -import os -import re -from pathlib import Path - from setuptools import setup -# Path where _version.py will be written -VERSION_PY_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'src', 'ducpy', '_version.py') - - -def find_schema_file_path(): - env_path = os.environ.get('DUC_SCHEMA_DIR') - if env_path: - candidate = Path(env_path) / 'duc.sql' - if candidate.exists(): - return str(candidate) - - current = Path(__file__).resolve() - for parent in current.parents: - candidate = parent / 'schema' / 'duc.sql' - if candidate.exists(): - return str(candidate) - - return None - -def get_schema_version_from_sql(sql_file_path): - """Extract schema version from PRAGMA user_version in duc.sql. - - Format: user_version = 3000000 → version "3.0.0" - Encoding: major * 1_000_000 + minor * 1_000 + patch - """ - try: - with open(sql_file_path, 'r') as f: - content = f.read() - match = re.search(r'PRAGMA\s+user_version\s*=\s*(\d+)', content) - if match: - version_int = int(match.group(1)) - major = version_int // 1_000_000 - minor = (version_int % 1_000_000) // 1_000 - patch = version_int % 1_000 - return f"{major}.{minor}.{patch}" - except FileNotFoundError: - return None - except Exception as e: - print(f"Warning: Could not parse schema version from {sql_file_path}: {e}. Defaulting to 0.0.0.") - return "0.0.0" - -def generate_version_py(): - """Generates the _version.py file with DUC_SCHEMA_VERSION.""" - schema_file_path = find_schema_file_path() - schema_version = get_schema_version_from_sql(schema_file_path) if schema_file_path else None - - if schema_version is not None: - print(f"Generating DUC schema version file at: {VERSION_PY_PATH} with version: {schema_version}") - os.makedirs(os.path.dirname(VERSION_PY_PATH), exist_ok=True) - with open(VERSION_PY_PATH, 'w') as f: - f.write(f'# This file is auto-generated by setup.py when the package is built.\n') - f.write(f'# Do not edit this file manually.\n') - f.write(f'DUC_SCHEMA_VERSION = "{schema_version}"\n') - elif os.path.exists(VERSION_PY_PATH): - print(f"DUC schema file not found. Using existing _version.py: {VERSION_PY_PATH}") - else: - print(f"DUC schema file not found and _version.py does not exist. Generating with default version 0.0.0 at: {VERSION_PY_PATH}") - os.makedirs(os.path.dirname(VERSION_PY_PATH), exist_ok=True) - with open(VERSION_PY_PATH, 'w') as f: - f.write(f'# This file is auto-generated by setup.py (fallback).\n') - f.write(f'DUC_SCHEMA_VERSION = "0.0.0"\n') - -# --- Main part of setup.py --- - -# Generate the version file when setup.py is processed by the build system. -generate_version_py() - # Minimal setup call. # Most package metadata (name, version via setuptools-scm, etc.) and # package discovery are handled by pyproject.toml. # This setup.py is present for compatibility and can be used for tasks # like C-extension building or build-time file generation if needed. -setup() +setup() \ No newline at end of file diff --git a/packages/ducpy/src/ducpy/_version.py b/packages/ducpy/src/ducpy/_version.py deleted file mode 100644 index 4cbfd218..00000000 --- a/packages/ducpy/src/ducpy/_version.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by setup.py when the package is built. -# Do not edit this file manually. -DUC_SCHEMA_VERSION = "3.0.0" diff --git a/packages/ducpy/src/ducpy/builders/sql_builder.py b/packages/ducpy/src/ducpy/builders/sql_builder.py index 1064ff90..b9eb111f 100644 --- a/packages/ducpy/src/ducpy/builders/sql_builder.py +++ b/packages/ducpy/src/ducpy/builders/sql_builder.py @@ -29,67 +29,33 @@ from __future__ import annotations import os -import re import sqlite3 import tempfile from pathlib import Path from typing import Any, List, Optional, Union -__all__ = ["DucSQL"] - - -_MIGRATION_RE = re.compile(r"^(\d+)_to_(\d+)$") +import ducpy_native - -def _find_schema_dir() -> Optional[Path]: - env_path = os.environ.get("DUC_SCHEMA_DIR") - if env_path: - candidate = Path(env_path) - if (candidate / "duc.sql").exists(): - return candidate - - current = Path(__file__).resolve() - for parent in current.parents: - candidate = parent / "schema" - if (candidate / "duc.sql").exists(): - return candidate - return None +__all__ = ["DucSQL"] def _get_current_schema_version() -> int: - """Read the target user_version from duc.sql — mirrors Rust's CURRENT_VERSION.""" - schema_dir = _find_schema_dir() - if schema_dir is None: - return 0 - duc_sql = (schema_dir / "duc.sql").read_text(encoding="utf-8") - m = re.search(r"PRAGMA\s+user_version\s*=\s*(\d+)", duc_sql, re.IGNORECASE) - return int(m.group(1)) if m else 0 + """Current schema version integer (e.g. 3000000) — from the Rust crate.""" + return ducpy_native.get_schema_version_int() def _read_migrations() -> list[tuple[int, int, str]]: - """Load all migration SQL files from schema/migrations/, sorted by from_version.""" - schema_dir = _find_schema_dir() - if schema_dir is None: - return [] - migrations_dir = schema_dir / "migrations" - if not migrations_dir.exists(): - return [] - result: list[tuple[int, int, str]] = [] - for path in sorted(migrations_dir.glob("*.sql")): - m = _MIGRATION_RE.match(path.stem) - if m: - result.append((int(m.group(1)), int(m.group(2)), path.read_text(encoding="utf-8"))) - result.sort(key=lambda x: x[0]) - return result + """All migrations as (from_version, to_version, sql) — from the Rust crate.""" + raw = ducpy_native.get_migrations() + # get_migrations returns list of tuples with i64 values + return [(int(f), int(t), sql) for f, t, sql in raw] def _apply_migrations(conn: sqlite3.Connection) -> None: """Walk the migration chain until user_version reaches the current schema version. - Mirrors the migration logic in Rust's ``bootstrap.rs``: - reads ``schema/migrations/{from}_to_{to}.sql`` files in order and executes - them until ``PRAGMA user_version`` matches the version declared in ``duc.sql``. - Safe to call on already-current or brand-new databases. + Mirrors the migration logic in Rust's ``bootstrap.rs``. Safe to call on + already-current or brand-new databases. """ user_version: int = conn.execute("PRAGMA user_version").fetchone()[0] if user_version == 0: @@ -112,18 +78,12 @@ def _apply_migrations(conn: sqlite3.Connection) -> None: def _read_schema_sql() -> str: - schema_dir = _find_schema_dir() - if schema_dir is None: - raise FileNotFoundError( - "Could not locate schema/duc.sql. " - "Ensure you are running from within the DUC repository." - ) - parts: list[str] = [] - for filename in ("duc.sql", "version_control.sql", "search.sql"): - path = schema_dir / filename - if path.exists(): - parts.append(path.read_text(encoding="utf-8")) - return "\n".join(parts) + """Concatenate the three schema SQL strings shipped by the Rust crate.""" + return "\n".join([ + ducpy_native.get_duc_schema_sql(), + ducpy_native.get_version_control_schema_sql(), + ducpy_native.get_search_schema_sql(), + ]) def _apply_pragmas(conn: sqlite3.Connection) -> None: diff --git a/packages/ducpy/src/ducpy/serialize.py b/packages/ducpy/src/ducpy/serialize.py index 93a25730..4ecfbf2b 100644 --- a/packages/ducpy/src/ducpy/serialize.py +++ b/packages/ducpy/src/ducpy/serialize.py @@ -29,61 +29,8 @@ def __init__(self, failures: List[str]): super().__init__("Embedded code validation failed:\n" + "\n".join(f"- {failure}" for failure in failures)) -def _find_schema_file() -> Path | None: - env_path = Path(os.environ["DUC_SCHEMA_DIR"]) if "DUC_SCHEMA_DIR" in os.environ else None - if env_path is not None: - candidate = env_path / "duc.sql" - if candidate.exists(): - return candidate - - current = Path(__file__).resolve() - for parent in current.parents: - candidate = parent / "schema" / "duc.sql" - if candidate.exists(): - return candidate - - return None - - -def _decode_user_version_to_semver(user_version: int) -> str: - """Decode sqlite-style schema user_version to semver. - - Encoding convention: - major * 1_000_000 + minor * 1_000 + patch - """ - if user_version < 0: - return "0.0.0" - - major = user_version // 1_000_000 - minor = (user_version % 1_000_000) // 1_000 - patch = user_version % 1_000 - return f"{major}.{minor}.{patch}" - - -def _read_schema_version_fallback() -> str: - """Resolve schema version directly from repository `schema/duc.sql`. - - This is used when `ducpy._version` isn't available (for example, in clean - CI environments before setup-time generation has run). - """ - try: - schema_path = _find_schema_file() - if schema_path is None: - return "0.0.0" - content = schema_path.read_text(encoding="utf-8") - match = re.search(r"PRAGMA\s+user_version\s*=\s*(\d+)\s*;", content) - if match: - return _decode_user_version_to_semver(int(match.group(1))) - except Exception as exc: # pragma: no cover - defensive fallback for CI/runtime variance - logger.warning("Failed to resolve schema version fallback from duc.sql: %s", exc) - - return "0.0.0" - - -try: - from ducpy._version import DUC_SCHEMA_VERSION -except ModuleNotFoundError: - DUC_SCHEMA_VERSION = _read_schema_version_fallback() +# Single source of truth for the schema version — comes from the Rust crate. +DUC_SCHEMA_VERSION = ducpy_native.get_schema_version() # Map Python element class names → Rust serde type tag strings. _ELEMENT_CLASS_TO_TYPE: Dict[str, str] = { diff --git a/packages/ducpy/src/ducpy_native/__init__.py b/packages/ducpy/src/ducpy_native/__init__.py index 97e65450..1ed54221 100644 --- a/packages/ducpy/src/ducpy_native/__init__.py +++ b/packages/ducpy/src/ducpy_native/__init__.py @@ -1,19 +1,28 @@ """Python shim for the compiled :mod:`ducpy_native` extension. -When built with maturin (``module-name = \"ducpy_native\"`` and -``python-source = \"src\"``), the compiled extension is emitted as the +When built with maturin (``module-name = "ducpy_native"`` and +``python-source = "src"``), the compiled extension is emitted as the submodule ``ducpy_native.ducpy_native``. Re-export the extension symbols at the package root so existing imports like ``import ducpy_native`` continue to work. """ from .ducpy_native import (get_external_file, # type: ignore[attr-defined] list_external_files, parse_duc, parse_duc_lazy, - serialize_duc) + serialize_duc, get_schema_version, + get_schema_version_int, get_duc_schema_sql, + get_version_control_schema_sql, + get_search_schema_sql, get_migrations) __all__ = [ - "parse_duc", - "parse_duc_lazy", - "serialize_duc", - "get_external_file", - "list_external_files", -] + "parse_duc", + "parse_duc_lazy", + "serialize_duc", + "get_external_file", + "list_external_files", + "get_schema_version", + "get_schema_version_int", + "get_duc_schema_sql", + "get_version_control_schema_sql", + "get_search_schema_sql", + "get_migrations", +] \ No newline at end of file diff --git a/packages/ducrs/.gitignore b/packages/ducrs/.gitignore index 5a93358b..79ae88bb 100644 --- a/packages/ducrs/.gitignore +++ b/packages/ducrs/.gitignore @@ -21,5 +21,3 @@ Cargo.lock #.idea/ -schema/* -!schema/.gitkeep \ No newline at end of file diff --git a/packages/ducrs/Cargo.toml b/packages/ducrs/Cargo.toml index 02e835ab..95784fa2 100644 --- a/packages/ducrs/Cargo.toml +++ b/packages/ducrs/Cargo.toml @@ -13,9 +13,7 @@ include = [ "Cargo.toml", "README.md", "LICENSE", - "schema/duc.sql", - "schema/version_control.sql", - "schema/search.sql", + "schema/*.sql", "schema/migrations/*.sql", ] diff --git a/packages/ducrs/build.rs b/packages/ducrs/build.rs index 17dbc5fa..ac556a6b 100644 --- a/packages/ducrs/build.rs +++ b/packages/ducrs/build.rs @@ -33,75 +33,25 @@ fn find_schema_dir(manifest_dir: &Path) -> Result Result<(), Box> { - if dst.exists() { - let _ = fs::remove_dir_all(dst); - } - fs::create_dir_all(dst)?; - - for entry in fs::read_dir(src)? { - let entry = entry?; - let from = entry.path(); - let name = match from.file_name() { - Some(n) => n, - None => continue, - }; - let to = dst.join(name); - - let meta = entry.metadata()?; - if meta.is_dir() { - mirror_dir(&from, &to)?; - } else { - fs::copy(&from, &to)?; - } - } - Ok(()) -} - -fn mirror_dir(src: &Path, dst: &Path) -> Result<(), Box> { - fs::create_dir_all(dst)?; - for entry in fs::read_dir(src)? { - let entry = entry?; - let from = entry.path(); - let name = match from.file_name() { - Some(n) => n, - None => continue, - }; - let to = dst.join(name); - let meta = entry.metadata()?; - if meta.is_dir() { - mirror_dir(&from, &to)?; - } else { - fs::copy(&from, &to)?; - } - } - Ok(()) + // 3. Local workspace: walk up to the repo root `schema/`. + let source = manifest_dir + .ancestors() + .map(|a| a.join("schema")) + .find(|p| p.join("duc.sql").is_file()) + .ok_or_else(|| { + format!( + "Could not locate schema/duc.sql from {}. Set DUC_SCHEMA_DIR to a directory containing duc.sql.", + manifest_dir.display() + ) + })?; + Ok(source) } fn main() -> Result<(), Box> { @@ -160,7 +110,7 @@ fn main() -> Result<(), Box> { let mut reg = String::from( "// Auto-generated by build.rs — do not edit by hand.\n\ // Each entry: (from_version, to_version, sql).\n\ - pub(crate) static MIGRATIONS: &[(i64, i64, &str)] = &[\n", + pub static MIGRATIONS: &[(i64, i64, &str)] = &[\n", ); for (from, to, sql) in &migrations { reg.push_str(&format!(" ({from}i64, {to}i64, \"")); @@ -203,4 +153,4 @@ fn main() -> Result<(), Box> { println!("cargo:rerun-if-changed=build.rs"); Ok(()) -} +} \ No newline at end of file diff --git a/packages/ducrs/schema b/packages/ducrs/schema new file mode 120000 index 00000000..56c062f7 --- /dev/null +++ b/packages/ducrs/schema @@ -0,0 +1 @@ +../../schema \ No newline at end of file diff --git a/packages/ducrs/src/db/bootstrap.rs b/packages/ducrs/src/db/bootstrap.rs index 1734ca5b..593834e5 100644 --- a/packages/ducrs/src/db/bootstrap.rs +++ b/packages/ducrs/src/db/bootstrap.rs @@ -32,10 +32,24 @@ const CURRENT_VERSION: i64 = { val }; +/// Runtime accessor for the compile-time schema version integer. +pub fn current_schema_version_int() -> i64 { + CURRENT_VERSION +} + // Migration registry auto-generated by build.rs from `schema/migrations/*.sql`. // Each entry is (from_version, to_version, sql). Sorted ascending by from_version. include!(concat!(env!("OUT_DIR"), "/migrations_registry.rs")); +// ── Public re-exports for downstream crates ───────────────────────────── + +/// Current schema version as a semver string (generated at build time). +pub const CURRENT_SCHEMA_VERSION_SEMVER: &str = env!("DUC_SCHEMA_VERSION"); + +pub const DUC_SCHEMA_SQL: &str = DUC_SCHEMA; +pub const VERSION_CONTROL_SCHEMA_SQL: &str = VERSION_CONTROL_SCHEMA; +pub const SEARCH_SCHEMA_SQL: &str = SEARCH_SCHEMA; + /// Per-connection PRAGMAs that must be re-applied on every open (they are /// either ephemeral or take effect only after the first statement on the /// connection). diff --git a/packages/ducrs/src/db/mod.rs b/packages/ducrs/src/db/mod.rs index 85c737b7..047ea944 100644 --- a/packages/ducrs/src/db/mod.rs +++ b/packages/ducrs/src/db/mod.rs @@ -3,7 +3,7 @@ //! Call [`open_file`] or [`open_memory`] instead of calling `rusqlite::Connection` //! directly. Internally they dispatch to the correct backend for each compile target. -mod bootstrap; +pub mod bootstrap; #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] mod native;