-
-
Save brycied00d/339d8460e7b1955bd9e15542bbbee21b to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby | |
=begin | |
Migrate TT-RSS data from MySQL to Postgres | |
Author: Bryce Chidester <[email protected]> | |
Provided as-is, no warranty. Make backups! | |
This script may work in the reverse direction... but probably not. And seeing as | |
TT-RSS doesn't officially support any other database backend at this time, there's | |
really no point in migrating to or from any other database. | |
Really this script was just the quickest and easiest way for me to "convert" my | |
TT-RSS instance from the legacy MySQL instance to Postgres, without losing all | |
the histories, entries, starred entries, tags etc. I found an old Java migrator, | |
which wasn't an option for many reasons, and I messed around with pgloader for | |
awhile but it required too much configuration and coaxing. In the end, it was | |
much simpler to write my own migrator/converter. This script is largely based | |
off sequel's --copy-database mode. | |
Basic Usage: | |
1. Create the target database, and a user. If you don't know how to do this, | |
I refer you to Postgres' documentation. | |
2. Load the bare TT-RSS Postgres schema. | |
psql <connect string> -f <path to schema/ttrss_schema_pgsql.sql> | |
3. Make sure update-daemon2.php is stopped | |
4. Run this script. | |
ttrss-migrator.rb <MySQL connect string> <Postgres connect string> | |
Ex: 'mysql2://localhost/tt-rss?user=<user>&password=<password>&encoding=utf8' | |
Note: I used the mysql2 connector and explicitly specified encoding=utf8 to | |
avoid some encoding issues I encountered early on. If newlines appear as | |
"\012" then you probably messed up this part. | |
Ex: 'postgres://localhost/ttrss?user=<user>&password=<password>&encoding=utf8' | |
Note: Again, I explicitly specified encoding=utf8 to ensure everything is clear. | |
5. Update your TT-RSS config.php with the Postgres connection details. | |
License: | |
Copyright (c) 2016, Bryce Chidester <[email protected]> | |
All rights reserved. | |
Redistribution and use in source and binary forms, with or without | |
modification, are permitted provided that the following conditions are met: | |
1. Redistributions of source code must retain the above copyright notice, this | |
list of conditions and the following disclaimer. | |
2. Redistributions in binary form must reproduce the above copyright notice, | |
this list of conditions and the following disclaimer in the documentation | |
and/or other materials provided with the distribution. | |
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | |
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | |
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
The views and conclusions contained in the software and documentation are those | |
of the authors and should not be interpreted as representing official policies, | |
either expressed or implied, of the FreeBSD Project. | |
=end | |
require 'logger' | |
require 'sequel' | |
def usage | |
"Usage:\n" + | |
"#{$0} <MySQL connect string> <Postgres connect string>\n" + | |
"See file header for more information." | |
end | |
abort usage if ARGV.count < 2 | |
start_time = Time.now | |
Srcloggers = [] | |
Srcloggers << Logger.new($stderr) | |
Srcloggers.each { |l| l.progname="SourceDB" } | |
Tgtloggers = [] | |
Tgtloggers << Logger.new($stderr) | |
Tgtloggers.each { |l| l.progname="TargetDB" } | |
SourceDB = Sequel.connect(ARGV[0]) | |
SourceDB.loggers = Srcloggers | |
SourceDB.test_connection | |
TargetDB = Sequel.connect(ARGV[1]) | |
TargetDB.loggers = Tgtloggers | |
TargetDB.test_connection | |
=begin | |
1. Drop foreign_keys from TargetDB | |
2. Make any custom table modifications to avoid constraints | |
3. Copy data | |
4. Make any custom table modifications to reset constraints | |
5. Re-add foreign_keys to TargetDB | |
=end | |
Sequel.extension :migration | |
TargetDB.extension :schema_dumper | |
#index_migration = eval(TargetDB.dump_indexes_migration(:same_db=>true, :index_names=>true)) | |
fk_migration = eval(TargetDB.dump_foreign_key_migration(:same_db=>true)) | |
SourceDB.transaction do | |
TargetDB.transaction do | |
puts "Begin removing foreign key constraints" | |
fk_migration.apply(TargetDB, :down) | |
puts "Finished removing foreign key constraints" | |
# Temporarily allow null in section_name from ttrss_prefs_sections | |
TargetDB.alter_table(:ttrss_prefs_sections) do | |
set_column_allow_null :section_name | |
end | |
SourceDB.tables.each do |table| | |
puts "Truncating TargetDB table: #{table}" | |
TargetDB[table].truncate | |
puts "Begin copying records for table: #{table}" | |
time = Time.now | |
to_ds = TargetDB.from(table) | |
j = 0 | |
SourceDB.from(table).each do |record| | |
if Time.now - time > 5 | |
puts "Status: #{j} records copied" | |
time = Time.now | |
end | |
to_ds.insert(record) | |
j += 1 | |
end | |
puts "Finished copying #{j} records for table: #{table}" | |
end | |
puts "Finished copying data" | |
# Apply schema updates to ttrss_prefs_sections and reset section_name allowing nulls | |
TargetDB[:ttrss_prefs_sections].where(:id=>1).update(:section_name => 'General') | |
TargetDB[:ttrss_prefs_sections].where(:id=>2).update(:section_name => 'Interface') | |
TargetDB[:ttrss_prefs_sections].where(:id=>3).update(:section_name => 'Advanced') | |
TargetDB[:ttrss_prefs_sections].where(:id=>4).update(:section_name => 'Digest') | |
TargetDB.alter_table(:ttrss_prefs_sections) do | |
set_column_not_null :section_name | |
end | |
puts "Begin adding foreign key constraints" | |
fk_migration.apply(TargetDB, :up) | |
puts "Finished adding foreign key constraints" | |
TargetDB.tables.each{|t| TargetDB.reset_primary_key_sequence(t)} | |
puts "Primary key sequences reset successfully" | |
puts "Database copy finished in #{Time.now - start_time} seconds" | |
end | |
end | |
puts "Finished copying data" | |
exit | |
=begin | |
For reference: | |
if copy_databases | |
Sequel.extension :migration | |
DB.extension :schema_dumper | |
db2 = ARGV.shift | |
error_proc["Error: Must specify database connection string or path to yaml file as second argument for database you want to copy to"] if db2.nil? || db2.empty? | |
extra_proc.call | |
start_time = Time.now | |
TO_DB = connect_proc[db2] | |
same_db = DB.database_type==TO_DB.database_type | |
index_opts = {:same_db=>same_db} | |
index_opts[:index_names] = :namespace if !DB.global_index_namespace? && TO_DB.global_index_namespace? | |
puts "Databases connections successful" | |
schema_migration = eval(DB.dump_schema_migration(:indexes=>false, :same_db=>same_db)) | |
index_migration = eval(DB.dump_indexes_migration(index_opts)) | |
fk_migration = eval(DB.dump_foreign_key_migration(:same_db=>same_db)) | |
puts "Migrations dumped successfully" | |
schema_migration.apply(TO_DB, :up) | |
puts "Tables created" | |
puts "Begin copying data" | |
DB.transaction do | |
TO_DB.transaction do | |
DB.tables.each do |table| | |
puts "Begin copying records for table: #{table}" | |
time = Time.now | |
to_ds = TO_DB.from(table) | |
j = 0 | |
DB.from(table).each do |record| | |
if Time.now - time > 5 | |
puts "Status: #{j} records copied" | |
time = Time.now | |
end | |
to_ds.insert(record) | |
j += 1 | |
end | |
puts "Finished copying #{j} records for table: #{table}" | |
end | |
end | |
end | |
puts "Finished copying data" | |
puts "Begin creating indexes" | |
index_migration.apply(TO_DB, :up) | |
puts "Finished creating indexes" | |
puts "Begin adding foreign key constraints" | |
fk_migration.apply(TO_DB, :up) | |
puts "Finished adding foreign key constraints" | |
if TO_DB.database_type == :postgres | |
TO_DB.tables.each{|t| TO_DB.reset_primary_key_sequence(t)} | |
puts "Primary key sequences reset successfully" | |
end | |
puts "Database copy finished in #{Time.now - start_time} seconds" | |
exit | |
end | |
=end |
Thank you! This seems to be the only reasonable alternative to spamming my server with Java ... :-)
I failed to compile pgloader on my system - some dependencies could just not be found, and I did not want to waste time with understanding the config file of the now-unsupported pgloader 2.x for a one-time conversion. Well, this one it is then. (Meaning, I wholeheartedly agree with the author's motivation.)
For those who need to know the dependencies:
gem install sequel
gem install mysql2
That worked for me. :-)
For the previous gems to install correctly you need some dev headers installed. You also need the Ruby PostgreSQL package. Example for Debian systems:
sudo apt-get install ruby-dev libmysql++-dev
sudo gem install sequel mysql2 pg
And note that the script does not create tables itself: I needed to initialize my PostgreSQL database first.
This script worked beautifully on TT-RSS v19.2, thank you!
I could not get the dependencies installed using the prior comments. Fortunately these gems are provided in Ubuntu's repo:
apt-get install ruby-mysql2 ruby-pg
No need to install dev packages nor run gem install
after.
+1 Thanks! Another success data point here for my v19.2 (088fcf8) migration.
My database is old and has some garbage that needed collecting first. These were my fixes:
(Delete deprecated plugin tables missed by the migration scripts)
DROP TABLE IF EXISTS ttrss_plugin_af_sort_bayes_references ;
DROP TABLE IF EXISTS ttrss_plugin_af_sort_bayes_wordfreqs ;
DROP TABLE IF EXISTS ttrss_plugin_af_sort_bayes_categories ;
(Patch ttrss-migrator.rb
to drop columns that don't exist in destination schema)
130,133c130
< mapped = record.slice(*TargetDB[table].columns)
< if !mapped.empty?
< to_ds.insert(mapped)
< end
---
> to_ds.insert(record)
I know next to nothing about Ruby, so sorry in advance if my edits suck.
+1
My initial installation is super old, currently v19.2 (e40c8da)
I used mstroud's patch, although I didn't check if I needed it or not
First issue was I had a 'theme_id' column on ttrss_users, which the values were null anyway, so I dropped it.
alter table ttrss_users drop column theme_id;
In postgres, I had to drop a bunch of contraints due to violations
alter table ttrss_enclosures drop constraint ttrss_enclosures_post_id_fkey;
alter table ttrss_feeds drop constraint ttrss_feeds_cat_id_fkey;
alter table ttrss_user_entries drop constraint ttrss_user_entries_feed_id_fkey;
alter table ttrss_user_entries drop constraint ttrss_user_entries_orig_feed_id_fkey;
alter table ttrss_user_entries drop constraint ttrss_user_entries_ref_id_fkey;
alter table ttrss_tags drop constraint ttrss_tags_post_int_id_fkey;
Then at the end I put them back, removing any stale records that were violating the contraints
alter table ttrss_enclosures add constraint ttrss_enclosures_post_id_fkey foreign key (post_id) references ttrss_entries(id) ON DELETE cascade;
alter table ttrss_feeds add constraint ttrss_feeds_cat_id_fkey foreign key (cat_id) references ttrss_feed_categories(id) ON DELETE set null;
alter table ttrss_user_entries add constraint ttrss_user_entries_feed_id_fkey foreign key (feed_id) references ttrss_feeds(id) ON DELETE cascade;
alter table ttrss_user_entries add constraint ttrss_user_entries_orig_feed_id_fkey foreign key (orig_feed_id) references ttrss_archived_feeds(id) ON DELETE set null;
alter table ttrss_user_entries add constraint ttrss_user_entries_ref_id_fkey foreign key (ref_id) references ttrss_entries(id) ON DELETE cascade;
alter table ttrss_tags add constraint ttrss_tags_post_int_id_fkey foreign key (post_int_id) references ttrss_user_entries(int_id) ON DELETE cascade;
I had alot of stale data, in mysql ttrss_tags was 4+ million rows, 125k after removing the stale rows
My ttrss runs with docker-compose, this is the Dockerfile I used to make an image to run on the docker-compose network
FROM debian:stable
RUN apt update && apt install -qyy ruby ruby-dev ruby-mysql2 ruby-pg
RUN gem install sequel
ADD ttrss-migrator.rb /
Just in case anyone happens upon this because they're looking to migrate from mysql to postgres like I did today. This worked GREAT (once I got ruby set up along with all of the dependencies needed for the script to work). It took me a while getting all of the dependencies installed, but once I got that done, the migration was nice and quick. After reconfiguring the config.php file to point to the new DB everything worked just like nothing had changed. I honestly thought it was still using the old DB until I shut it down and tried again. Thanks for this script!