max / audiofiles
1 file changed,
+83 insertions,
-6 deletions
| @@ -1507,6 +1507,53 @@ CREATE VIEW IF NOT EXISTS live_samples AS | |||
| 1507 | 1507 | SELECT * FROM samples WHERE deleted_at IS NULL; | |
| 1508 | 1508 | "#; | |
| 1509 | 1509 | ||
| 1510 | + | const MIGRATION_032: &str = r#" | |
| 1511 | + | -- M018 recreated the audio_analysis sync triggers (to salt row_id) but copied | |
| 1512 | + | -- the pre-M011 column list, silently dropping spectral_bandwidth, | |
| 1513 | + | -- centroid_variance, crest_factor, attack_time, and classification_confidence | |
| 1514 | + | -- from the sync_changelog payload. The initial snapshot still carries them, but | |
| 1515 | + | -- ongoing edits to those five columns never propagate across devices. Recreate | |
| 1516 | + | -- the insert/update triggers with M018's salted row_id AND the full column set. | |
| 1517 | + | DROP TRIGGER IF EXISTS sync_audio_analysis_insert; | |
| 1518 | + | DROP TRIGGER IF EXISTS sync_audio_analysis_update; | |
| 1519 | + | ||
| 1520 | + | CREATE TRIGGER sync_audio_analysis_insert AFTER INSERT ON audio_analysis | |
| 1521 | + | WHEN (SELECT value FROM sync_state WHERE key = 'applying_remote') != '1' | |
| 1522 | + | BEGIN | |
| 1523 | + | INSERT INTO sync_changelog (table_name, op, row_id, data) | |
| 1524 | + | VALUES ('audio_analysis', 'INSERT', | |
| 1525 | + | hash_row_id((SELECT value FROM sync_state WHERE key = 'row_id_salt'), NEW.hash), | |
| 1526 | + | json_object('hash', NEW.hash, 'bpm', NEW.bpm, 'musical_key', NEW.musical_key, | |
| 1527 | + | 'duration', NEW.duration, 'sample_rate', NEW.sample_rate, 'channels', NEW.channels, | |
| 1528 | + | 'peak_db', NEW.peak_db, 'rms_db', NEW.rms_db, 'is_loop', NEW.is_loop, | |
| 1529 | + | 'spectral_centroid', NEW.spectral_centroid, 'onset_strength', NEW.onset_strength, | |
| 1530 | + | 'analyzed_at', NEW.analyzed_at, 'lufs', NEW.lufs, | |
| 1531 | + | 'spectral_flatness', NEW.spectral_flatness, 'spectral_rolloff', NEW.spectral_rolloff, | |
| 1532 | + | 'zero_crossing_rate', NEW.zero_crossing_rate, 'classification', NEW.classification, | |
| 1533 | + | 'spectral_bandwidth', NEW.spectral_bandwidth, 'centroid_variance', NEW.centroid_variance, | |
| 1534 | + | 'crest_factor', NEW.crest_factor, 'attack_time', NEW.attack_time, | |
| 1535 | + | 'classification_confidence', NEW.classification_confidence)); | |
| 1536 | + | END; | |
| 1537 | + | ||
| 1538 | + | CREATE TRIGGER sync_audio_analysis_update AFTER UPDATE ON audio_analysis | |
| 1539 | + | WHEN (SELECT value FROM sync_state WHERE key = 'applying_remote') != '1' | |
| 1540 | + | BEGIN | |
| 1541 | + | INSERT INTO sync_changelog (table_name, op, row_id, data) | |
| 1542 | + | VALUES ('audio_analysis', 'UPDATE', | |
| 1543 | + | hash_row_id((SELECT value FROM sync_state WHERE key = 'row_id_salt'), NEW.hash), | |
| 1544 | + | json_object('hash', NEW.hash, 'bpm', NEW.bpm, 'musical_key', NEW.musical_key, | |
| 1545 | + | 'duration', NEW.duration, 'sample_rate', NEW.sample_rate, 'channels', NEW.channels, | |
| 1546 | + | 'peak_db', NEW.peak_db, 'rms_db', NEW.rms_db, 'is_loop', NEW.is_loop, | |
| 1547 | + | 'spectral_centroid', NEW.spectral_centroid, 'onset_strength', NEW.onset_strength, | |
| 1548 | + | 'analyzed_at', NEW.analyzed_at, 'lufs', NEW.lufs, | |
| 1549 | + | 'spectral_flatness', NEW.spectral_flatness, 'spectral_rolloff', NEW.spectral_rolloff, | |
| 1550 | + | 'zero_crossing_rate', NEW.zero_crossing_rate, 'classification', NEW.classification, | |
| 1551 | + | 'spectral_bandwidth', NEW.spectral_bandwidth, 'centroid_variance', NEW.centroid_variance, | |
| 1552 | + | 'crest_factor', NEW.crest_factor, 'attack_time', NEW.attack_time, | |
| 1553 | + | 'classification_confidence', NEW.classification_confidence)); | |
| 1554 | + | END; | |
| 1555 | + | "#; | |
| 1556 | + | ||
| 1510 | 1557 | /// Register `hash_row_id(salt, key) -> TEXT` as a deterministic SQLite | |
| 1511 | 1558 | /// function on the given connection. Used by the M018 sync triggers so the | |
| 1512 | 1559 | /// `sync_changelog.row_id` field never carries cleartext content (tag strings, | |
| @@ -1643,6 +1690,7 @@ impl Database { | |||
| 1643 | 1690 | MIGRATION_029, | |
| 1644 | 1691 | MIGRATION_030, | |
| 1645 | 1692 | MIGRATION_031, | |
| 1693 | + | MIGRATION_032, | |
| 1646 | 1694 | ]; | |
| 1647 | 1695 | ||
| 1648 | 1696 | for (i, sql) in MIGRATIONS.iter().enumerate() { | |
| @@ -1886,7 +1934,7 @@ mod tests { | |||
| 1886 | 1934 | .conn() | |
| 1887 | 1935 | .query_row("PRAGMA user_version", [], |row| row.get(0)) | |
| 1888 | 1936 | .unwrap(); | |
| 1889 | - | assert_eq!(version, 31); | |
| 1937 | + | assert_eq!(version, 32); | |
| 1890 | 1938 | } | |
| 1891 | 1939 | ||
| 1892 | 1940 | #[test] | |
| @@ -1897,7 +1945,36 @@ mod tests { | |||
| 1897 | 1945 | .conn() | |
| 1898 | 1946 | .query_row("PRAGMA user_version", [], |row| row.get(0)) | |
| 1899 | 1947 | .unwrap(); | |
| 1900 | - | assert_eq!(version, 31); | |
| 1948 | + | assert_eq!(version, 32); | |
| 1949 | + | } | |
| 1950 | + | ||
| 1951 | + | #[test] | |
| 1952 | + | fn audio_analysis_sync_triggers_carry_all_columns() { | |
| 1953 | + | // Regression guard for the M018 -> M032 fix: M018 recreated these | |
| 1954 | + | // triggers with the pre-M011 column list, so edits to the five columns | |
| 1955 | + | // below stopped propagating to sync_changelog (and thus across devices). | |
| 1956 | + | // Assert the live trigger bodies emit every later-added analysis column. | |
| 1957 | + | let db = Database::open_in_memory().unwrap(); | |
| 1958 | + | let later_columns = [ | |
| 1959 | + | "spectral_bandwidth", | |
| 1960 | + | "centroid_variance", | |
| 1961 | + | "crest_factor", | |
| 1962 | + | "attack_time", | |
| 1963 | + | "classification_confidence", | |
| 1964 | + | ]; | |
| 1965 | + | for trigger in ["sync_audio_analysis_insert", "sync_audio_analysis_update"] { | |
| 1966 | + | let sql: String = db | |
| 1967 | + | .conn() | |
| 1968 | + | .query_row( | |
| 1969 | + | "SELECT sql FROM sqlite_master WHERE type = 'trigger' AND name = ?1", | |
| 1970 | + | [trigger], | |
| 1971 | + | |row| row.get(0), | |
| 1972 | + | ) | |
| 1973 | + | .unwrap(); | |
| 1974 | + | for col in later_columns { | |
| 1975 | + | assert!(sql.contains(col), "{trigger} is missing column {col}"); | |
| 1976 | + | } | |
| 1977 | + | } | |
| 1901 | 1978 | } | |
| 1902 | 1979 | ||
| 1903 | 1980 | /// Open a fresh file-backed DB, close, reopen. The second open re-enters | |
| @@ -1916,7 +1993,7 @@ mod tests { | |||
| 1916 | 1993 | .conn() | |
| 1917 | 1994 | .query_row("PRAGMA user_version", [], |row| row.get(0)) | |
| 1918 | 1995 | .unwrap(); | |
| 1919 | - | assert_eq!(version, 31); | |
| 1996 | + | assert_eq!(version, 32); | |
| 1920 | 1997 | } | |
| 1921 | 1998 | ||
| 1922 | 1999 | /// Simulates the worst-case recovery path: a prior partial migration left | |
| @@ -1960,7 +2037,7 @@ mod tests { | |||
| 1960 | 2037 | .conn() | |
| 1961 | 2038 | .query_row("PRAGMA user_version", [], |row| row.get(0)) | |
| 1962 | 2039 | .unwrap(); | |
| 1963 | - | assert_eq!(version, 31); | |
| 2040 | + | assert_eq!(version, 32); | |
| 1964 | 2041 | } | |
| 1965 | 2042 | ||
| 1966 | 2043 | /// M018 contract: the `sync_changelog.row_id` for sensitive tables must | |
| @@ -2182,7 +2259,7 @@ mod tests { | |||
| 2182 | 2259 | let initial_version: i32 = conn | |
| 2183 | 2260 | .query_row("PRAGMA user_version", [], |row| row.get(0)) | |
| 2184 | 2261 | .unwrap(); | |
| 2185 | - | assert_eq!(initial_version, 31); | |
| 2262 | + | assert_eq!(initial_version, 32); | |
| 2186 | 2263 | ||
| 2187 | 2264 | let batch = format!( | |
| 2188 | 2265 | "BEGIN;\n{}\nPRAGMA user_version = 999;\nCOMMIT;", | |
| @@ -2245,7 +2322,7 @@ mod tests { | |||
| 2245 | 2322 | .conn() | |
| 2246 | 2323 | .query_row("PRAGMA user_version", [], |row| row.get(0)) | |
| 2247 | 2324 | .unwrap(); | |
| 2248 | - | assert_eq!(version, 31); | |
| 2325 | + | assert_eq!(version, 32); | |
| 2249 | 2326 | } | |
| 2250 | 2327 | ||
| 2251 | 2328 | #[test] |