diff --git a/src/rust/liblektor-rs/lektor_db/build.rs b/src/rust/liblektor-rs/lektor_db/build.rs
index ad4572e446309b368c9d5a298a0fbc8e057e0972..b27dd5f67942f72bfc8cbb8ee126474b20f80d03 100644
--- a/src/rust/liblektor-rs/lektor_db/build.rs
+++ b/src/rust/liblektor-rs/lektor_db/build.rs
@@ -9,11 +9,23 @@ macro_rules! cmd {
         let output = Command::new($exec)
             .args($args)
             .stdout(Stdio::piped())
+            .stderr(Stdio::piped())
             .spawn()
             .expect(format!("failed to run {}", $exec).as_str())
             .wait_with_output()
             .expect(format!("failed to wait for {}", $exec).as_str());
-        assert!(output.status.success());
+        if !output.status.success() {
+            let err = std::str::from_utf8(&output.stderr)
+                .expect("invalid utf8 output")
+                .trim();
+            eprintln!("{err}");
+            panic!();
+        } else {
+            std::str::from_utf8(&output.stdout)
+                .expect("invalid utf8")
+                .trim()
+                .to_string()
+        }
     }};
 }
 
@@ -48,6 +60,7 @@ fn main() {
     env::set_var("DATABASE_URL", db_path);
     env::set_current_dir(source_dir).expect("failed to cwd to source folder!");
     cmd!( "diesel" => [ "migration", "run" ] );
+    cmd!( "diesel" => [ "print-schema" ] );
 
     rerun_directory(&migration_dir);
     println!("cargo:rerun-if-changed=build.rs");
diff --git a/src/rust/liblektor-rs/lektor_db/migrations/2023-01-24-125239_triggers/down.sql b/src/rust/liblektor-rs/lektor_db/migrations/2023-01-24-125239_triggers/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..e6740f4044a3cef4e6bcbd0efabc22db020090b2
--- /dev/null
+++ b/src/rust/liblektor-rs/lektor_db/migrations/2023-01-24-125239_triggers/down.sql
@@ -0,0 +1,14 @@
+DROP TRIGGER verify_kara_before_insert;
+DROP TRIGGER verify_kara_before_update;
+
+DROP TRIGGER verify_playlist_before_kara_insert;
+
+DROP TRIGGER forbid_update_kara_lang_table;
+DROP TRIGGER forbid_update_tag_table;
+DROP TRIGGER forbid_update_history_table;
+DROP TRIGGER forbid_update_playlist_table;
+DROP TRIGGER forbid_update_repo_table;
+DROP TRIGGER forbid_update_repo_kara_table;
+
+DROP TRIGGER apply_ot_after_delete_all_langs_on_kara;
+DROP TRIGGER remove_ot_from_kara_after_adding_another_one;
\ No newline at end of file
diff --git a/src/rust/liblektor-rs/lektor_db/migrations/2023-01-24-125239_triggers/up.sql b/src/rust/liblektor-rs/lektor_db/migrations/2023-01-24-125239_triggers/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..ae2013668901da5f86983f127cea275f6a8a33c0
--- /dev/null
+++ b/src/rust/liblektor-rs/lektor_db/migrations/2023-01-24-125239_triggers/up.sql
@@ -0,0 +1,56 @@
+-- Trigger used to verify that we are not inserting karas with incorrect
+-- origins, types or languages. Same thing for the update.
+
+CREATE TRIGGER verify_kara_before_update BEFORE UPDATE ON kara
+BEGIN SELECT CASE 
+  WHEN NEW.song_type   NOT IN ("OP", "ED", "IS", "MV", "OT")            THEN RAISE (ABORT, "invalid type on kara update")
+  WHEN NEW.song_origin NOT IN ("anime", "vn", "game", "music", "autre") THEN RAISE (ABORT, "invalid origin on kara update")
+  WHEN NEW.is_virtual <> OLD.is_virtual                                 THEN RAISE (ABORT, "can't change the virtual status of a kara")
+END; END;
+
+CREATE TRIGGER verify_kara_before_insert BEFORE INSERT ON kara
+BEGIN SELECT CASE 
+  WHEN NEW.song_type   NOT IN ("OP", "ED", "IS", "MV", "OT")            THEN RAISE (ABORT, "invalid type on kara insertion")
+  WHEN NEW.song_origin NOT IN ("anime", "vn", "game", "music", "autre") THEN RAISE (ABORT, "invalid origin on kara insertion")
+  WHEN NEW.is_virtual IS TRUE AND NEW.is_dl IS TRUE                     THEN RAISE (ABORT, "a kara can't be downloaded and virtual at the same time")
+END; END;
+
+-- Verify that we are not inserting virtual karas in playlists.
+
+CREATE TRIGGER verify_playlist_before_kara_insert BEFORE INSERT ON playlist_kara
+BEGIN SELECT CASE
+  WHEN ((SELECT COUNT(*)
+           FROM kara
+          WHERE kara.id = NEW.kara_id
+            AND kara.is_virtual IS TRUE
+        ) > 0)
+  THEN RAISE (ABORT, "can't add a virtual kara to a playlist")
+END; END;
+
+-- Forbid some updates
+
+CREATE TRIGGER forbid_update_kara_lang_table BEFORE UPDATE ON kara_lang BEGIN SELECT CASE WHEN TRUE THEN RAISE(ABORT, "updating the kara_lang table is forbiden") END; END;
+CREATE TRIGGER forbid_update_tag_table       BEFORE UPDATE ON tag       BEGIN SELECT CASE WHEN TRUE THEN RAISE(ABORT, "updating the tag table is forbiden")       END; END;
+CREATE TRIGGER forbid_update_history_table   BEFORE UPDATE ON history   BEGIN SELECT CASE WHEN TRUE THEN RAISE(ABORT, "updating the history table is forbiden")   END; END;
+CREATE TRIGGER forbid_update_playlist_table  BEFORE UPDATE ON playlist  BEGIN SELECT CASE WHEN TRUE THEN RAISE(ABORT, "updating the playlist table is forbiden")  END; END;
+CREATE TRIGGER forbid_update_repo_table      BEFORE UPDATE ON repo      BEGIN SELECT CASE WHEN TRUE THEN RAISE(ABORT, "updating the repo table is forbiden")      END; END;
+CREATE TRIGGER forbid_update_repo_kara_table BEFORE UPDATE ON repo_kara BEGIN SELECT CASE WHEN TRUE THEN RAISE(ABORT, "updating the repo_kara table is forbiden") END; END;
+
+-- Verify some properties on the languages
+
+CREATE TRIGGER apply_ot_after_delete_all_langs_on_kara AFTER DELETE ON kara_lang
+  WHEN ((SELECT COUNT(*)
+           FROM kara_lang
+          WHERE kara_lang.id = OLD.id
+        ) <= 0)
+BEGIN 
+  INSERT INTO kara_lang (id, code) SELECT OLD.id, "ot";
+END;
+
+CREATE TRIGGER remove_ot_from_kara_after_adding_another_one AFTER INSERT ON kara_lang
+  WHEN ((SELECT COUNT(*) FROM kara_lang WHERE kara_lang.id = NEW.id) >= 2)
+BEGIN
+  DELETE FROM kara_lang
+        WHERE kara_lang.id = NEW.id
+          AND kara_lang.code = "ot";
+END;