If you interessted as well, this is what will happen with my request:
Thanks — valid point, and no, there is currently no automatic cleanup. Here is both the why and the full manual list.
Why there is no auto-cleanup today
Nextcloud never drops migration-created tables on its own. The core Installer::removeApp() docblock states it plainly:
The function will not delete preferences, tables and the configuration, this has to be done by the function oc_app_uninstall().
The only hook an app gets is <repair-steps><uninstall> in info.xml. The catch: that step is executed from AppManager::disableApp() — the very same code path that runs when an admin merely clicks Disable in the Apps UI. The step cannot tell “disable” from “remove”. Putting DROP TABLE there means one accidental disable wipes every course, question pool and certificate on the instance. That is why virtually no Nextcloud app auto-drops its schema, and why we did not either.
The right shape is an explicit, opt-in occ learning:uninstall command. It is on the list now (see below).
Manual cleanup list
Verified against a live NC 33 / PostgreSQL 16 instance running Learning 5.2.1. Table names are shown with the default oc_ prefix — substitute your own dbtableprefix from config.php.
1. Drop the 54 app tables
oc_learning_ai_chat_memory oc_learning_gameshow_answers
oc_learning_analytics oc_learning_gameshow_players
oc_learning_ans_translations oc_learning_gameshow_sessions
oc_learning_answers oc_learning_kudos
oc_learning_assignments oc_learning_league_challenges
oc_learning_audit_chain_state oc_learning_league_results
oc_learning_audit_checkpoints oc_learning_league_seasons
oc_learning_audit_events oc_learning_leitner_items
oc_learning_campaign_state oc_learning_mission_claims
oc_learning_certificates oc_learning_oversight
oc_learning_cert_keys oc_learning_pools
oc_learning_coop_players oc_learning_pool_shares
oc_learning_coop_sessions oc_learning_qst_translations
oc_learning_coop_votes oc_learning_questions
oc_learning_course_announcements oc_learning_rag_chunks
oc_learning_course_curriculum_scopes oc_learning_recert_reminders
oc_learning_course_documents oc_learning_sessions
oc_learning_course_exam_slots oc_learning_story_progress
oc_learning_course_members oc_learning_support_tickets
oc_learning_course_pools oc_learning_user_answers
oc_learning_course_question_overrides oc_learning_user_badges
oc_learning_courses oc_learning_user_stats
oc_learning_course_schedule oc_learning_user_telos
oc_learning_course_snapshots oc_learning_video_progress
oc_learning_course_videos
oc_learning_duel_answers
oc_learning_duel_invites
oc_learning_duel_sessions
oc_learning_epoch_progress
oc_learning_feed_items
Five tables were renamed over the app’s history (in v4.4.2 and again in v5.2.x). The migrations are self-healing, so on an up-to-date install only the new names exist — but if an upgrade ever failed midway, a legacy name may still be sitting there. Include them defensively; DROP TABLE IF EXISTS makes it harmless either way:
oc_learning_q_translations → oc_learning_qst_translations
oc_learning_a_translations → oc_learning_ans_translations
oc_learning_q_overrides → oc_learning_course_question_overrides
oc_learning_curriculum_scopes → oc_learning_course_curriculum_scopes
oc_learning_announcements → oc_learning_course_announcements
The LIKE 'oc_learning_%' queries below catch both variants automatically.
PostgreSQL:
DO $$
DECLARE t text;
BEGIN
FOR t IN SELECT tablename FROM pg_tables
WHERE schemaname = current_schema() AND tablename LIKE 'oc_learning\_%'
LOOP EXECUTE format('DROP TABLE IF EXISTS %I CASCADE', t); END LOOP;
END $$;
MySQL / MariaDB:
SELECT GROUP_CONCAT(CONCAT('`', table_name, '`') SEPARATOR ', ')
FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name LIKE 'oc\_learning\_%';
-- then: SET FOREIGN_KEY_CHECKS=0; DROP TABLE <paste>; SET FOREIGN_KEY_CHECKS=1;
2. Delete the non-table rows — do not skip this
DELETE FROM oc_migrations WHERE app = 'learning'; -- ← critical, see warning
DELETE FROM oc_appconfig WHERE appid = 'learning';
DELETE FROM oc_preferences WHERE appid = 'learning';
DELETE FROM oc_jobs WHERE class LIKE 'OCA\\Learning\\%';
DELETE FROM oc_notifications WHERE app = 'learning';
DELETE FROM oc_activity WHERE app = 'learning';
oc_migrations is the one that bites. Drop the 54 tables but leave those ~85 rows behind, and a later reinstall believes every migration has already run. It creates nothing, and the app boots into a broken state with no obvious error. If that already happened to you: delete the rows, then occ upgrade.
3. Files — nothing to clean
The app stores no data of its own on disk. Course documents, videos and images are ordinary Nextcloud files in the users’ own folders; the app only references them by path. Removing the app leaves them untouched, which is intended. There is no appdata_*/learning folder.
4. Then remove the app
occ app:remove learning
Before you run any of this: certificates
If your instance has ever issued a certificate, oc_learning_cert_keys and the issuer entries in oc_appconfig hold the Ed25519 private key of your issuer identity. Deleting it is irreversible: every certificate already handed out becomes permanently unverifiable, including through the public /verify route. There is no way to regenerate the key — a new one produces a different did:web identity and will not validate old signatures.
If that matters to you, export the key material before the oc_appconfig delete, or keep the two cert* tables and the issuer config rows and drop everything else.
What we will add
An occ learning:uninstall command that does all of the above in one transaction — dry-run by default, listing exactly what it would delete, with an explicit confirmation flag to actually execute, and a --keep-certificates escape hatch. I will link the commit here.