| 14 |
14 |
|
pub enum DbError {
|
| 15 |
15 |
|
#[error("SQLite error: {0}")]
|
| 16 |
16 |
|
Sqlite(#[from] rusqlite::Error),
|
|
17 |
+ |
|
|
18 |
+ |
/// The vault's schema is ahead of what this build knows how to read, so
|
|
19 |
+ |
/// opening it would mean querying a shape this code has never seen. See
|
|
20 |
+ |
/// [`Database::migrate`] for why that is refused rather than tolerated.
|
|
21 |
+ |
#[error(
|
|
22 |
+ |
"this vault was written by a newer version of audiofiles \
|
|
23 |
+ |
(vault schema {found}, this build understands {supported}). \
|
|
24 |
+ |
Update audiofiles to open it."
|
|
25 |
+ |
)]
|
|
26 |
+ |
VaultTooNew {
|
|
27 |
+ |
/// `PRAGMA user_version` read off the vault.
|
|
28 |
+ |
found: i32,
|
|
29 |
+ |
/// The highest version this build can produce, [`SCHEMA_VERSION`].
|
|
30 |
+ |
supported: i32,
|
|
31 |
+ |
},
|
| 17 |
32 |
|
}
|
| 18 |
33 |
|
|
| 19 |
34 |
|
/// The config store wraps rusqlite; its one failure mode is the database, so it
|
| 1842 |
1857 |
|
Ok(())
|
| 1843 |
1858 |
|
}
|
| 1844 |
1859 |
|
|
|
1860 |
+ |
/// Every migration, in order. Index + 1 is the `PRAGMA user_version` a database
|
|
1861 |
+ |
/// carries once that migration has been applied, so the list's length is the
|
|
1862 |
+ |
/// schema version this build produces.
|
|
1863 |
+ |
///
|
|
1864 |
+ |
/// At module scope rather than inside [`Database::migrate`] so [`SCHEMA_VERSION`]
|
|
1865 |
+ |
/// can be derived from it: the guard against opening a newer vault and the
|
|
1866 |
+ |
/// migration runner have to agree on one number, and deriving it is how they
|
|
1867 |
+ |
/// cannot drift.
|
|
1868 |
+ |
const MIGRATIONS: &[&str] = &[
|
|
1869 |
+ |
MIGRATION_001,
|
|
1870 |
+ |
MIGRATION_002,
|
|
1871 |
+ |
MIGRATION_003,
|
|
1872 |
+ |
MIGRATION_004,
|
|
1873 |
+ |
MIGRATION_005,
|
|
1874 |
+ |
MIGRATION_006,
|
|
1875 |
+ |
MIGRATION_007,
|
|
1876 |
+ |
MIGRATION_008,
|
|
1877 |
+ |
MIGRATION_009,
|
|
1878 |
+ |
MIGRATION_010,
|
|
1879 |
+ |
MIGRATION_011,
|
|
1880 |
+ |
MIGRATION_012,
|
|
1881 |
+ |
MIGRATION_013,
|
|
1882 |
+ |
MIGRATION_014,
|
|
1883 |
+ |
MIGRATION_015,
|
|
1884 |
+ |
MIGRATION_016,
|
|
1885 |
+ |
MIGRATION_017,
|
|
1886 |
+ |
MIGRATION_018,
|
|
1887 |
+ |
MIGRATION_019,
|
|
1888 |
+ |
MIGRATION_020,
|
|
1889 |
+ |
MIGRATION_021,
|
|
1890 |
+ |
MIGRATION_022,
|
|
1891 |
+ |
MIGRATION_023,
|
|
1892 |
+ |
MIGRATION_024,
|
|
1893 |
+ |
MIGRATION_025,
|
|
1894 |
+ |
MIGRATION_026,
|
|
1895 |
+ |
MIGRATION_027,
|
|
1896 |
+ |
MIGRATION_028,
|
|
1897 |
+ |
MIGRATION_029,
|
|
1898 |
+ |
MIGRATION_030,
|
|
1899 |
+ |
MIGRATION_031,
|
|
1900 |
+ |
MIGRATION_032,
|
|
1901 |
+ |
MIGRATION_033,
|
|
1902 |
+ |
MIGRATION_034,
|
|
1903 |
+ |
MIGRATION_035,
|
|
1904 |
+ |
MIGRATION_036,
|
|
1905 |
+ |
MIGRATION_037,
|
|
1906 |
+ |
MIGRATION_038,
|
|
1907 |
+ |
];
|
|
1908 |
+ |
|
|
1909 |
+ |
/// The schema version this build produces, and the highest one it can read.
|
|
1910 |
+ |
///
|
|
1911 |
+ |
/// A vault reporting more than this was written by a newer audiofiles and is
|
|
1912 |
+ |
/// refused; see [`DbError::VaultTooNew`].
|
|
1913 |
+ |
pub const SCHEMA_VERSION: i32 = MIGRATIONS.len() as i32;
|
|
1914 |
+ |
|
| 1845 |
1915 |
|
/// Compile-time proof that a write transaction is open on the connection.
|
| 1846 |
1916 |
|
///
|
| 1847 |
1917 |
|
/// Constructed only by [`Database::transaction`], and required by the row-write
|
| 1959 |
2029 |
|
/// Each migration step runs inside a transaction so the schema change and
|
| 1960 |
2030 |
|
/// version bump are atomic, a crash between the two can no longer leave the
|
| 1961 |
2031 |
|
/// database in an inconsistent state.
|
|
2032 |
+ |
///
|
|
2033 |
+ |
/// The runner is bounded on both sides. Forward is the ordinary case.
|
|
2034 |
+ |
/// Backward is not possible and must not be attempted silently: a vault
|
|
2035 |
+ |
/// carrying a version this build has never heard of was written by a newer
|
|
2036 |
+ |
/// audiofiles, and every query past this point assumes a schema this code
|
|
2037 |
+ |
/// has seen. Applying nothing and returning `Ok` reads as success and then
|
|
2038 |
+ |
/// queries a shape it does not understand, survivable for an added column
|
|
2039 |
+ |
/// and silent data loss for a dropped one, a rename, or a NOT NULL the
|
|
2040 |
+ |
/// older code never populates.
|
|
2041 |
+ |
///
|
|
2042 |
+ |
/// Nothing exotic is needed to reach it: a rollback to an older release
|
|
2043 |
+ |
/// after a bad update, a restored backup, or one vault opened from two
|
|
2044 |
+ |
/// machines running different versions, which is the shape the
|
|
2045 |
+ |
/// vault-per-detachable-drive workflow is made of.
|
| 1962 |
2046 |
|
#[instrument(skip_all)]
|
| 1963 |
2047 |
|
fn migrate(&mut self) -> Result<(), DbError> {
|
| 1964 |
2048 |
|
let version: i32 = self
|
| 1965 |
2049 |
|
.conn
|
| 1966 |
2050 |
|
.query_row("PRAGMA user_version", [], |row| row.get(0))?;
|
| 1967 |
2051 |
|
|
| 1968 |
|
- |
const MIGRATIONS: &[&str] = &[
|
| 1969 |
|
- |
MIGRATION_001,
|
| 1970 |
|
- |
MIGRATION_002,
|
| 1971 |
|
- |
MIGRATION_003,
|
| 1972 |
|
- |
MIGRATION_004,
|
| 1973 |
|
- |
MIGRATION_005,
|
| 1974 |
|
- |
MIGRATION_006,
|
| 1975 |
|
- |
MIGRATION_007,
|
| 1976 |
|
- |
MIGRATION_008,
|
| 1977 |
|
- |
MIGRATION_009,
|
| 1978 |
|
- |
MIGRATION_010,
|
| 1979 |
|
- |
MIGRATION_011,
|
| 1980 |
|
- |
MIGRATION_012,
|
| 1981 |
|
- |
MIGRATION_013,
|
| 1982 |
|
- |
MIGRATION_014,
|
| 1983 |
|
- |
MIGRATION_015,
|
| 1984 |
|
- |
MIGRATION_016,
|
| 1985 |
|
- |
MIGRATION_017,
|
| 1986 |
|
- |
MIGRATION_018,
|
| 1987 |
|
- |
MIGRATION_019,
|
| 1988 |
|
- |
MIGRATION_020,
|
| 1989 |
|
- |
MIGRATION_021,
|
| 1990 |
|
- |
MIGRATION_022,
|
| 1991 |
|
- |
MIGRATION_023,
|
| 1992 |
|
- |
MIGRATION_024,
|
| 1993 |
|
- |
MIGRATION_025,
|
| 1994 |
|
- |
MIGRATION_026,
|
| 1995 |
|
- |
MIGRATION_027,
|
| 1996 |
|
- |
MIGRATION_028,
|
| 1997 |
|
- |
MIGRATION_029,
|
| 1998 |
|
- |
MIGRATION_030,
|
| 1999 |
|
- |
MIGRATION_031,
|
| 2000 |
|
- |
MIGRATION_032,
|
| 2001 |
|
- |
MIGRATION_033,
|
| 2002 |
|
- |
MIGRATION_034,
|
| 2003 |
|
- |
MIGRATION_035,
|
| 2004 |
|
- |
MIGRATION_036,
|
| 2005 |
|
- |
MIGRATION_037,
|
| 2006 |
|
- |
MIGRATION_038,
|
| 2007 |
|
- |
];
|
|
2052 |
+ |
if version > SCHEMA_VERSION {
|
|
2053 |
+ |
return Err(DbError::VaultTooNew {
|
|
2054 |
+ |
found: version,
|
|
2055 |
+ |
supported: SCHEMA_VERSION,
|
|
2056 |
+ |
});
|
|
2057 |
+ |
}
|
| 2008 |
2058 |
|
|
| 2009 |
2059 |
|
for (i, sql) in MIGRATIONS.iter().enumerate() {
|
| 2010 |
2060 |
|
let target = (i + 1) as i32;
|
| 2675 |
2725 |
|
assert_eq!(version, 38);
|
| 2676 |
2726 |
|
}
|
| 2677 |
2727 |
|
|
|
2728 |
+ |
/// A vault written by a newer audiofiles is refused, not opened.
|
|
2729 |
+ |
///
|
|
2730 |
+ |
/// The bug this pins: `migrate()` only ever compared `version < target`, so
|
|
2731 |
+ |
/// a vault ahead of the build applied nothing, returned `Ok`, and left every
|
|
2732 |
+ |
/// query below running against a schema this code has never seen. Two things
|
|
2733 |
+ |
/// are asserted, and the second is the one that matters: the open fails, AND
|
|
2734 |
+ |
/// it fails without having touched the database, so an older build cannot
|
|
2735 |
+ |
/// half-write a newer vault on its way to giving up.
|
|
2736 |
+ |
#[test]
|
|
2737 |
+ |
fn open_refuses_a_vault_from_a_newer_audiofiles() {
|
|
2738 |
+ |
let dir = tempfile::tempdir().unwrap();
|
|
2739 |
+ |
let path = dir.path().join("audiofiles.db");
|
|
2740 |
+ |
|
|
2741 |
+ |
Database::open(&path).unwrap();
|
|
2742 |
+ |
let ahead = SCHEMA_VERSION + 1;
|
|
2743 |
+ |
{
|
|
2744 |
+ |
let conn = Connection::open(&path).unwrap();
|
|
2745 |
+ |
conn.execute_batch(&format!("PRAGMA user_version = {ahead}"))
|
|
2746 |
+ |
.unwrap();
|
|
2747 |
+ |
}
|
|
2748 |
+ |
|
|
2749 |
+ |
let Err(err) = Database::open(&path) else {
|
|
2750 |
+ |
panic!("a newer vault must not open");
|
|
2751 |
+ |
};
|
|
2752 |
+ |
let DbError::VaultTooNew { found, supported } = err else {
|
|
2753 |
+ |
panic!("expected VaultTooNew, got {err:?}");
|
|
2754 |
+ |
};
|
|
2755 |
+ |
assert_eq!(found, ahead);
|
|
2756 |
+ |
assert_eq!(supported, SCHEMA_VERSION);
|
|
2757 |
+ |
// The message is the whole remedy the user gets, so it has to say which
|
|
2758 |
+ |
// side is old rather than printing two bare numbers.
|
|
2759 |
+ |
let text = err.to_string();
|
|
2760 |
+ |
assert!(text.contains("newer version of audiofiles"), "{text}");
|
|
2761 |
+ |
|
|
2762 |
+ |
let after: i32 = Connection::open(&path)
|
|
2763 |
+ |
.unwrap()
|
|
2764 |
+ |
.query_row("PRAGMA user_version", [], |row| row.get(0))
|
|
2765 |
+ |
.unwrap();
|
|
2766 |
+ |
assert_eq!(after, ahead, "a refused open must not rewrite the vault");
|
|
2767 |
+ |
}
|
|
2768 |
+ |
|
|
2769 |
+ |
/// The boundary is `>`, not `>=`: a vault at exactly this build's version is
|
|
2770 |
+ |
/// the ordinary case and opens with no migration run. Guards against a
|
|
2771 |
+ |
/// one-off that would refuse every up-to-date vault.
|
|
2772 |
+ |
#[test]
|
|
2773 |
+ |
fn open_accepts_a_vault_at_the_current_version() {
|
|
2774 |
+ |
let dir = tempfile::tempdir().unwrap();
|
|
2775 |
+ |
let path = dir.path().join("audiofiles.db");
|
|
2776 |
+ |
|
|
2777 |
+ |
Database::open(&path).unwrap();
|
|
2778 |
+ |
let db = Database::open(&path).expect("a current vault opens");
|
|
2779 |
+ |
let version: i32 = db
|
|
2780 |
+ |
.conn()
|
|
2781 |
+ |
.query_row("PRAGMA user_version", [], |row| row.get(0))
|
|
2782 |
+ |
.unwrap();
|
|
2783 |
+ |
assert_eq!(version, SCHEMA_VERSION);
|
|
2784 |
+ |
}
|
|
2785 |
+ |
|
|
2786 |
+ |
/// `SCHEMA_VERSION` is derived from `MIGRATIONS`, and the migration runner
|
|
2787 |
+ |
/// keys the version it writes off the same list. Pinning the number here
|
|
2788 |
+ |
/// means adding a migration without meaning to shows up as a failure.
|
|
2789 |
+ |
#[test]
|
|
2790 |
+ |
fn schema_version_matches_the_migration_list() {
|
|
2791 |
+ |
assert_eq!(SCHEMA_VERSION, 38);
|
|
2792 |
+ |
assert_eq!(SCHEMA_VERSION as usize, MIGRATIONS.len());
|
|
2793 |
+ |
}
|
|
2794 |
+ |
|
| 2678 |
2795 |
|
/// M037 contract: the browse-list sort must not build a temp B-tree.
|
| 2679 |
2796 |
|
///
|
| 2680 |
2797 |
|
/// Asserting the index exists would be the weaker test, because an index
|