"discourse_solved_solved_topics" viola a restrição not-null durante a reconstrução

Olá a todos!

Durante uma reconstrução em um fórum na branch stable, ocorreu um erro relacionado ao plugin solved.

Aqui estão os logs:

2025-12-20 03:48:21.715 UTC [1122] discourse@discourse ERROR:  valor nulo na coluna "answer_post_id" da relação "discourse_solved_solved_topics" viola a restrição not-null
2025-12-20 03:48:21.715 UTC [1122] discourse@discourse DETAIL:  A linha com falha contém (3735, 35506, null, -1, null, 2021-05-12 18:38:58.11516, 2021-05-12 18:38:58.11516).
2025-12-20 03:48:21.715 UTC [1122] discourse@discourse STATEMENT:  INSERT INTO discourse_solved_solved_topics (
	  topic_id,
	  answer_post_id,
	  topic_timer_id,
	  accepter_user_id,
	  created_at,
	  updated_at
	)
	SELECT
	  tc.topic_id,
	  tc.answer_post_id,
	  tc.topic_timer_id,
	  tc.accepter_user_id,
	  tc.created_at,
	  tc.updated_at
	FROM (
	  SELECT
	    tc.topic_id,
	    CAST(tc.value AS INTEGER) AS answer_post_id,
	    CAST(tc2.value AS INTEGER) AS topic_timer_id,
	    COALESCE(ua.acting_user_id, -1) AS accepter_user_id,
	    tc.created_at,
	    tc.updated_at,
	    ROW_NUMBER() OVER (PARTITION BY tc.topic_id ORDER BY tc.created_at ASC) AS rn_topic,
	    ROW_NUMBER() OVER (PARTITION BY CAST(tc.value AS INTEGER) ORDER BY tc.created_at ASC) AS rn_answer
	  FROM topic_custom_fields tc
	  LEFT JOIN topic_custom_fields tc2 ON tc2.topic_id = tc.topic_id AND tc2.name = 'solved_auto_close_topic_timer_id'
	  LEFT JOIN user_actions ua ON ua.target_topic_id = tc.topic_id AND ua.action_type = 15
	  WHERE tc.name = 'accepted_answer_post_id'
	    AND tc.id > 0
	    AND tc.id <= 0 + 10000
	) tc
	WHERE tc.rn_topic = 1 AND tc.rn_answer = 1
	ON CONFLICT DO NOTHING
	
rake aborted!
StandardError: Ocorreu um erro, todas as migrações subsequentes foram canceladas: (StandardError)

ERROR:  valor nulo na coluna "answer_post_id" da relação "discourse_solved_solved_topics" viola a restrição not-null
DETAIL:  A linha com falha contém (3735, 35506, null, -1, null, 2021-05-12 18:38:58.11516, 2021-05-12 18:38:58.11516).
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/rack-mini-profiler-4.0.1/lib/patches/db/pg/alias_method.rb:109:in `exec'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/rack-mini-profiler-4.0.1/lib/patches/db/pg/alias_method.rb:109:in `async_exec'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mini_sql-1.6.0/lib/mini_sql/postgres/connection.rb:217:in `run'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mini_sql-1.6.0/lib/mini_sql/active_record_postgres/connection.rb:38:in `block in run'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mini_sql-1.6.0/lib/mini_sql/active_record_postgres/connection.rb:34:in `block in with_lock'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activesupport-8.0.2.1/lib/active_support/concurrency/null_lock.rb:9:in `synchronize'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mini_sql-1.6.0/lib/mini_sql/active_record_postgres/connection.rb:34:in `with_lock'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mini_sql-1.6.0/lib/mini_sql/active_record_postgres/connection.rb:38:in `run'
/var/www/discourse/lib/mini_sql_multisite_connection.rb:109:in `run'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mini_sql-1.6.0/lib/mini_sql/postgres/connection.rb:196:in `exec'
/var/www/discourse/plugins/discourse-solved/db/migrate/20250318024953_copy_solved_topic_custom_field_to_discourse_solved_solved_topics.rb:17:in `up'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:994:in `public_send'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:994:in `exec_migration'
/var/www/discourse/lib/freedom_patches/schema_migration_details.rb:8:in `block in exec_migration'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/benchmark-0.4.1/lib/benchmark.rb:305:in `measure'
/var/www/discourse/lib/freedom_patches/schema_migration_details.rb:8:in `exec_migration'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:975:in `block (2 levels) in migrate'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activesupport-8.0.2.1/lib/active_support/benchmark.rb:17:in `realtime'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:974:in `block in migrate'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/connection_adapters/abstract/connection_pool.rb:412:in `with_connection'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:973:in `migrate'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1187:in `migrate'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1535:in `block in execute_migration_in_transaction'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1588:in `ddl_transaction'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1534:in `execute_migration_in_transaction'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1509:in `each'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1509:in `migrate_without_lock'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1454:in `block in migrate'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1606:in `with_advisory_lock'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1454:in `migrate'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1261:in `up'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1236:in `migrate'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/tasks/database_tasks.rb:270:in `migrate'
/var/www/discourse/lib/tasks/db.rake:267:in `block (2 levels) in <main>'
/var/www/discourse/lib/distributed_mutex.rb:53:in `block in synchronize'
/var/www/discourse/lib/distributed_mutex.rb:49:in `synchronize'
/var/www/discourse/lib/distributed_mutex.rb:49:in `synchronize'
/var/www/discourse/lib/distributed_mutex.rb:34:in `synchronize'
/var/www/discourse/lib/tasks/db.rake:242:in `block in <main>'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/rake-13.3.0/exe/rake:27:in `<top (required)>'
/usr/local/bin/bundle:25:in `load'
/usr/local/bin/bundle:25:in `<main>'

Caused by:
PG::NotNullViolation: ERROR:  valor nulo na coluna "answer_post_id" da relação "discourse_solved_solved_topics" viola a restrição not-null (PG::NotNullViolation)
DETAIL:  A linha com falha contém (3735, 35506, null, -1, null, 2021-05-12 18:38:58.11516, 2021-05-12 18:38:58.11516).
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/rack-mini-profiler-4.0.1/lib/patches/db/pg/alias_method.rb:109:in `exec'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/rack-mini-profiler-4.0.1/lib/patches/db/pg/alias_method.rb:109:in `async_exec'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mini_sql-1.6.0/lib/mini_sql/postgres/connection.rb:217:in `run'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mini_sql-1.6.0/lib/mini_sql/active_record_postgres/connection.rb:38:in `block in run'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mini_sql-1.6.0/lib/mini_sql/active_record_postgres/connection.rb:34:in `block in with_lock'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activesupport-8.0.2.1/lib/active_support/concurrency/null_lock.rb:9:in `synchronize'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mini_sql-1.6.0/lib/mini_sql/active_record_postgres/connection.rb:34:in `with_lock'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mini_sql-1.6.0/lib/mini_sql/active_record_postgres/connection.rb:38:in `run'
/var/www/discourse/lib/mini_sql_multisite_connection.rb:109:in `run'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mini_sql-1.6.0/lib/mini_sql/postgres/connection.rb:196:in `exec'
/var/www/discourse/plugins/discourse-solved/db/migrate/20250318024953_copy_solved_topic_custom_field_to_discourse_solved_solved_topics.rb:17:in `up'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:994:in `public_send'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:994:in `exec_migration'
/var/www/discourse/lib/freedom_patches/schema_migration_details.rb:8:in `block in exec_migration'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/benchmark-0.4.1/lib/benchmark.rb:305:in `measure'
/var/www/discourse/lib/freedom_patches/schema_migration_details.rb:8:in `exec_migration'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:975:in `block (2 levels) in migrate'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activesupport-8.0.2.1/lib/active_support/benchmark.rb:17:in `realtime'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:974:in `block in migrate'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/connection_adapters/abstract/connection_pool.rb:412:in `with_connection'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:973:in `migrate'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1187:in `migrate'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1535:in `block in execute_migration_in_transaction'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1588:in `ddl_transaction'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1534:in `execute_migration_in_transaction'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1509:in `each'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1509:in `migrate_without_lock'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1454:in `block in migrate'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1606:in `with_advisory_lock'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1454:in `migrate'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1261:in `up'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/migration.rb:1236:in `migrate'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-8.0.2.1/lib/active_record/tasks/database_tasks.rb:270:in `migrate'
/var/www/discourse/lib/tasks/db.rake:267:in `block (2 levels) in <main>'
/var/www/discourse/lib/distributed_mutex.rb:53:in `block in synchronize'
/var/www/discourse/lib/distributed_mutex.rb:49:in `synchronize'
/var/www/discourse/lib/distributed_mutex.rb:49:in `synchronize'
/var/www/discourse/lib/distributed_mutex.rb:34:in `synchronize'
/var/www/discourse/lib/tasks/db.rake:242:in `block in <main>'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/rake-13.3.0/exe/rake:27:in `<top (required)>'
/usr/local/bin/bundle:25:in `load'
/usr/local/bin/bundle:25:in `<main>'
Tasks: TOP => db:migrate
(Consulte o rastreamento completo executando a tarefa com --trace)
I, [2025-12-20T03:48:21.794538 #1]  INFO -- : gem install geocoder -v 1.8.3 -i /var/www/discourse/plugins/discourse-locations/gems/3.3.8 --no-document --ignore-dependencies --no-user-install
Geocoder 1.8.3 instalado com sucesso
1 gem instalado
== 20250318024953 CopySolvedTopicCustomFieldToDiscourseSolvedSolvedTopics: migrando 

I, [2025-12-20T03:48:21.795736 #1]  INFO -- : Encerrando processos assíncronos
I, [2025-12-20T03:48:21.795838 #1]  INFO -- : Enviando INT para HOME=/var/lib/postgresql USER=postgres exec chpst -u postgres:postgres:ssl-cert -U postgres:postgres:ssl-cert /usr/lib/postgresql/15/bin/postmaster -D /etc/postgresql/15/main pid: 45
I, [2025-12-20T03:48:21.795934 #1]  INFO -- : Enviando TERM para exec chpst -u redis -U redis /usr/bin/redis-server /etc/redis/redis.conf pid: 112
112:signal-handler (1766202501) Recebeu SIGTERM agendando desligamento...
2025-12-20 03:48:21.796 UTC [45] LOG:  recebida solicitação de desligamento rápido
2025-12-20 03:48:21.800 UTC [45] LOG:  abortando quaisquer transações ativas
2025-12-20 03:48:21.808 UTC [45] LOG:  worker em segundo plano "logical replication launcher" (PID 59) saiu com código de saída 1
2025-12-20 03:48:21.809 UTC [54] LOG:  desligando
2025-12-20 03:48:21.811 UTC [54] LOG:  checkpoint iniciando: desligamento imediato
2025-12-20 03:48:21.861 UTC [54] LOG:  checkpoint concluído: escreveu 18 buffers (0.0%); 0 arquivo(s) WAL adicionado(s), 0 removido(s), 0 reciclado(s); write=0.039 s, sync=0.006 s, total=0.052 s; sync files=10, longest=0.003 s, average=0.001 s; distance=161 kB, estimate=161 kB
112:M 20 Dec 2025 03:48:21.892 # Desligamento solicitado pelo usuário...
112:M 20 Dec 2025 03:48:21.892 * Salvando o snapshot RDB final antes de sair.
2025-12-20 03:48:21.918 UTC [45] LOG:  sistema de banco de dados desligado
112:M 20 Dec 2025 03:48:22.621 * DB salvo no disco
112:M 20 Dec 2025 03:48:22.621 # Redis agora está pronto para sair, tchau tchau...


FALHA
--------------------
Pups::ExecError: cd /var/www/discourse && su discourse -c 'bundle exec rake db:migrate' falhou com retorno #<Process::Status: pid 1016 exit 1>
Localização da falha: /usr/local/lib/ruby/gems/3.3.0/gems/pups-1.3.0/lib/pups/exec_command.rb:131:in `spawn'
exec falhou com os parâmetros {"cd"=>"$home", "tag"=>"migrate", "hook"=>"db_migrate", "cmd"=>["su discourse -c 'bundle exec rake db:migrate'"]}
bootstrap falhou com código de saída 1
** FALHA NO BOOTSTRAP ** por favor, role para cima e procure mensagens de erro anteriores, pode haver mais de uma.
./discourse-doctor pode ajudar a diagnosticar o problema.

Consegui reconstruir excluindo o plugin solved no app.yml (usando o comando - rm -rf discourse-solved), e o dono do fórum provavelmente poderia viver sem esse plugin, mas eu ainda gostaria de corrigir isso.

Não acho que o problema venha do plugin, pode ter sido uma manipulação inadequada no fórum. Mas, ao explorar o banco de dados, encontrei pelo menos 20-30 entradas \N no campo value

Aqui está um extrato:

id topic_id name value created at updated at
1579 35497 accepted_answer_post_id 301435 2021-05-10 20:18:57.780444 2021-05-10 20:18:57.780444
1583 35506 accepted_answer_post_id \N 2021-05-12 18:38:58.11516 2021-05-12 18:38:58.11516
423 21211 accepted_answer_post_id 153039 2019-09-04 21:01:37.811417 2019-09-04 21:01:37.811417
424 21685 accepted_answer_post_id 156850 2019-09-09 04:39:28.388591 2019-09-09 04:39:28.388591
425 21711 accepted_answer_post_id 157323 2019-09-11 11:51:14.375185 2019-09-11 11:51:14.375185
428 20982 accepted_answer_post_id 157807 2019-09-14 08:53:16.079032 2019-09-14 08:53:16.079032
429 4949 accepted_answer_post_id 157515 2019-09-16 07:57:00.862694 2019-09-16 07:57:00.862694
430 15439 accepted_answer_post_id 158645 2019-09-19 00:56:20.112603 2019-09-19 00:56:20.112603
1603 35710 accepted_answer_post_id \N 2021-05-19 20:58:06.598697 2021-05-19 20:58:06.598697
701 25377 accepted_answer_post_id 195459 2020-03-26 20:42:14.168898 2020-03-26 20:42:14.168898
434 21868 accepted_answer_post_id 159024 2019-09-22 10:09:09.671605 2019-09-22 10:09:09.671605

Preciso te dizer que não sou muito bom em consultas SQL, especialmente via linha de comando. Não confio muito em apagar essas linhas diretamente no terminal.

Mas, de qualquer forma, qual caminho você recomenda para eu corrigir isso? Uma consulta (que tipo de consulta?)? Um script, ou algo que possa ser feito diretamente no fórum?

Exceto que foi movido para o núcleo (core).

Aqueles em que o post_id é Newline são o problema. Você deve apenas excluir esses registros do banco de dados (pode ser possível a partir do rails). Você pode provavelmente pedir ao Ask.discourse.com para lhe dizer como.

Como um possível palpite… Eu acredito que excluir um tópico agora remove automaticamente a solução e apaga o registro em discourse_solved_solved_topics. Não tenho 100% de certeza de que isso funcionaria sem o plugin solved instalado, mas pode valer a pena verificar?

Se você executar uma consulta no explorador de dados para pegar todos eles, excluir um e executar a consulta novamente para ver se ele desapareceu, essa pode ser uma maneira fácil de ver se funcionaria?

Restaurar o tópico não restaura o registro em discourse_solved_solved_topics, então (potencialmente :slight_smile:) um rápido ‘excluir e restaurar’ poderia limpá-los. :crossed_fingers:

Tenho quase certeza de que não funcionará, já que o código que faz isso está no plugin.

Também tenho quase certeza de que o banco de dados está corrompido. Não deveria haver quebras de linha nessa coluna value. O que precisa ser feito é excluí-las do banco de dados. Se você não fizer isso, esquecerá quando tentar migrar para um novo servidor e restaurar este banco de dados (e ele terá solved porque está no núcleo) e terá que descobrir isso tudo de novo.

Ah, pensei que seria um tiro no escuro. Eu não tinha certeza de onde a limpeza era mantida. :slight_smile:

Parece que Alexander teve um problema semelhante e seguiu o caminho do banco de dados: Error upgrading to latest Discourse -- solved - #4 by Alexander_Wright

Obrigado @pfaffman e @JammyDodger, vou investigar o banco de dados para tentar corrigir isso.

Com o explorador de dados, tenho a lista de linhas problemáticas, podemos ver que isso é muito limitado no tempo, talvez um pequeno problema com o plugin durante esse período?

Eu vi este commit que tentou corrigir um problema semelhante?

Interessante de qualquer forma. Vou tentar corrigir isso, vou levar meu tempo, já que é muito sensível.

Deve ser bem fácil fazer com que o Ask.discourse.com lhe forneça uma consulta que encontrará todos os que você deseja excluir e (depois de ver que essa parte da consulta funciona) excluí-los todos.

Ter o usuário do sistema como quem marcou como resolvido é um pouco incomum lá também. Acho que foi o mesmo para Alexander, olhando o erro dele.

Com dois relatórios semelhantes, você acha que isso pode ser um caso de a migração não ter considerado um caso extremo, @pfaffman?

Eu não me arriscaria a adivinhar.

Suponho que dois em oito meses não seja exatamente uma enxurrada, então talvez não seja o suficiente para investigar mais a fundo. :slight_smile:

Parece que o outro relatório também tem um intervalo de datas semelhante:

Não é muito útil saber para a tarefa em questão, mas pode ser útil se mais relatórios chegarem.