Skip to content

Commit

Permalink
Add JSON column for each RABX column.
Browse files Browse the repository at this point in the history
Include script to migrate back/forth between RABX and JSON.
  • Loading branch information
dracos committed May 12, 2023
1 parent d513991 commit b96bd67
Show file tree
Hide file tree
Showing 40 changed files with 434 additions and 106 deletions.
147 changes: 147 additions & 0 deletions bin/one-off-update-rabx-to-json
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/usr/bin/env perl

use v5.14;
use warnings;

BEGIN {
use File::Basename qw(dirname);
use File::Spec;
my $d = dirname(File::Spec->rel2abs($0));
require "$d/../setenv.pl";
}

use FixMyStreet::DB;
use Getopt::Long::Descriptive;
use IO::String;
use JSON::MaybeXS;
use RABX;

my ($opts, $usage) = describe_options(
'%c %o',
['commit', 'actually change the database'],
['reverse', 'update RABX with data from JSON'],
['help|h', "print usage message and exit" ],
);
$usage->die if $opts->help;

my $DB = FixMyStreet::DB->schema->storage->dbh;
my $JSON = JSON->new->allow_nonref->canonical;

my @tables = qw(body contacts problem comment users moderation_original_data defect_types report_extra_fields roles token);
my %column_name = (
token => 'data',
default => 'extra',
);

foreach (@tables) {
my $rabx_column = $column_name{$_} || $column_name{default};
my $json_column = $rabx_column . '_json';
my $where;
if ($opts->reverse) {
$where = "($json_column IS NOT NULL AND $rabx_column IS NULL)";
} else {
$where = "($json_column IS NULL AND $rabx_column IS NOT NULL)";
}
if ($_ eq 'problem') {
if ($opts->reverse) {
$where .= " OR (geocode_json IS NOT NULL AND geocode IS NULL)";
} else {
$where .= " OR (geocode_json IS NULL AND geocode IS NOT NULL)";
}
}

my $total = $DB->selectrow_array("SELECT COUNT(*) FROM $_ WHERE $where");

print "Migrating data for table $_ - $total rows to migrate\n";

my @cols = ($rabx_column, $json_column);
if ($_ eq 'problem') {
push @cols, 'id', 'geocode', 'geocode_json';
} elsif ($_ eq 'token') {
push @cols, 'scope', 'token';
} else {
push @cols, 'id';
}
my $cols = join(',', @cols);

my $count = 0;
my $query;
$DB->do("BEGIN");
if ($opts->commit) {
$query = $DB->prepare("SELECT $cols FROM $_ WHERE $where LIMIT 1000 FOR UPDATE");
} else {
$DB->do("DECLARE mycursor CURSOR FOR SELECT $cols FROM $_ WHERE $where");
$query = $DB->prepare("FETCH 1000 FROM mycursor");
}
while(1) {
$query->execute();
last if $query->rows == 0;

while (my $r = $query->fetchrow_hashref) {
if ($opts->reverse) {
my $rabx = to_rabx(from_json($r->{$json_column}));
if ($_ eq 'problem') {
my $rabx_geocode = to_rabx(from_json($r->{geocode_json}));
update("UPDATE $_ SET $rabx_column = ?, geocode = ? WHERE id=?", $rabx, $rabx_geocode, $r->{id});
} elsif ($_ eq 'token') {
update("UPDATE $_ SET $rabx_column = ? WHERE token=? AND scope=?", $rabx, $r->{token}, $r->{scope});
} else {
update("UPDATE $_ SET $rabx_column = ? WHERE id=?", $rabx, $r->{id});
}
} else {
my $json = to_json(from_rabx($r->{$rabx_column}, $_));
if ($_ eq 'problem') {
my $json_geocode = to_json(from_rabx($r->{geocode}, 'geocode'));
update("UPDATE $_ SET $json_column = ?, geocode_json = ? WHERE id=?", $json, $json_geocode, $r->{id});
} elsif ($_ eq 'token') {
update("UPDATE $_ SET $json_column = ? WHERE token=? AND scope=?", $json, $r->{token}, $r->{scope});
} else {
update("UPDATE $_ SET $json_column = ? WHERE id=?", $json, $r->{id});
}
}
$count++;
print "\r$count/$total";
}
}
$DB->do("COMMIT");
print "\n";
}

sub update {
my $sql = shift;
return unless $opts->commit;
$DB->do($sql, undef, @_);
}

sub from_json {
my $ser = shift;
return $JSON->decode($ser);
}

sub to_json {
my $data = shift;
my $ser = $JSON->encode($data);
return $ser;
}

sub from_rabx {
my $ser = shift;
my $table = shift;
return undef unless defined $ser;
# Some RABX columns are text, when they should be bytea. For
# these we must re-encode the string returned from the
# database, so that it is decoded again by RABX.
utf8::encode($ser) if $table ne 'token' && $table ne 'geocode';
my $h = new IO::String($ser);
return RABX::wire_rd($h);
}

sub to_rabx {
my $data = shift;
my $ser = '';
my $h = new IO::String($ser);
RABX::wire_wr( $data, $h );
# Decode from UTF-8 as the saving to the db will re-encode it
utf8::decode($ser);
return $ser;
}
5 changes: 5 additions & 0 deletions bin/update-schema
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ if ($upgrade && $current_version eq 'EMPTY') {
if (@statements) {
run_statements(@statements);
}

if ($name =~ /^0082/) {
system("bin/one-off-update-rabx-to-json --commit");
}
}

if ( $commit && $current_version lt '0028' ) {
Expand Down Expand Up @@ -215,6 +219,7 @@ else {
# (assuming schema change files are never half-applied, which should be the case)
sub get_db_version {
return 'EMPTY' if ! table_exists('problem');
return '0082' if column_exists('problem', 'extra_json');
return '0081' if constraint_delete_cascade('alert_sent_alert_id_fkey');
return '0080' if column_exists('roles', 'extra');
return '0079' if column_exists('response_templates', 'email_text');
Expand Down
11 changes: 11 additions & 0 deletions db/downgrade_0082---0081.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ALTER TABLE users DROP COLUMN extra_json;
ALTER TABLE body DROP COLUMN extra_json;
ALTER TABLE contacts DROP COLUMN extra_json;
ALTER TABLE problem DROP COLUMN extra_json;
ALTER TABLE problem DROP COLUMN geocode_json;
ALTER TABLE comment DROP COLUMN extra_json;
ALTER TABLE moderation_original_data DROP COLUMN extra_json;
ALTER TABLE defect_types DROP COLUMN extra_json;
ALTER TABLE report_extra_fields DROP COLUMN extra_json;
ALTER TABLE roles DROP COLUMN extra_json;
ALTER TABLE token DROP COLUMN data_json;
2 changes: 1 addition & 1 deletion db/rerun_dbic_loader.pl
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ BEGIN
resultset_namespace => '+FixMyStreet::DB::ResultSet',

# add in some extra components
components => [ 'FilterColumn', 'FixMyStreet::InflateColumn::DateTime', 'FixMyStreet::EncodedColumn' ],
components => [ 'FilterColumn', '+FixMyStreet::DB::JSONBColumn', 'FixMyStreet::InflateColumn::DateTime', 'FixMyStreet::EncodedColumn' ],

},
[ FixMyStreet->dbic_connect_info ],
Expand Down
18 changes: 15 additions & 3 deletions db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ create table users (
facebook_id bigint unique,
oidc_ids text ARRAY,
area_ids integer ARRAY,
extra text
extra text,
extra_json jsonb
);
CREATE UNIQUE INDEX users_email_verified_unique ON users (email) WHERE email_verified;
CREATE UNIQUE INDEX users_phone_verified_unique ON users (phone) WHERE phone_verified;
Expand Down Expand Up @@ -67,7 +68,8 @@ create table body (
blank_updates_permitted boolean not null default 'f',
convert_latlong boolean not null default 'f',
deleted boolean not null default 'f',
extra text
extra text,
extra_json jsonb
);

create table body_areas (
Expand All @@ -87,6 +89,7 @@ create table roles (
name text,
permissions text ARRAY,
extra text,
extra_json jsonb,
unique(body_id, name)
);

Expand Down Expand Up @@ -120,6 +123,7 @@ create table contacts (

-- extra fields required for open311
extra text,
extra_json jsonb,

-- for things like missed bin collections
non_public boolean default 'f',
Expand Down Expand Up @@ -223,8 +227,10 @@ create table problem (
whensent timestamp,
send_questionnaire boolean not null default 't',
extra text, -- extra fields required for open311
extra_json jsonb,
flagged boolean not null default 'f',
geocode bytea,
geocode_json jsonb,
response_priority_id int REFERENCES response_priorities(id),

-- logging sending failures (used by webservices)
Expand Down Expand Up @@ -360,6 +366,7 @@ create table comment (
-- and should be highlighted in the display?
external_id text,
extra text,
extra_json jsonb,
send_state text not null default 'unprocessed' check (
send_state = 'unprocessed'
or send_state = 'processed'
Expand Down Expand Up @@ -389,9 +396,11 @@ create table token (
scope text not null,
token text not null,
data bytea not null,
data_json jsonb,
created timestamp not null default current_timestamp,
primary key (scope, token)
);
ALTER TABLE token ADD CONSTRAINT token_data_not_null CHECK (data_json IS NOT NULL) NOT VALID;

-- Alerts

Expand Down Expand Up @@ -507,6 +516,7 @@ create table moderation_original_data (
created timestamp not null default current_timestamp,

extra text,
extra_json jsonb,
category text,
latitude double precision,
longitude double precision
Expand Down Expand Up @@ -560,6 +570,7 @@ CREATE TABLE defect_types (
name text not null,
description text not null,
extra text,
extra_json jsonb,
unique(body_id, name)
);

Expand Down Expand Up @@ -587,7 +598,8 @@ CREATE TABLE report_extra_fields (
name text not null,
cobrand text,
language text,
extra text
extra text,
extra_json jsonb
);

CREATE TABLE state (
Expand Down
13 changes: 13 additions & 0 deletions db/schema_0082-add-json-columns.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
ALTER TABLE users ADD COLUMN extra_json jsonb;
ALTER TABLE body ADD COLUMN extra_json jsonb;
ALTER TABLE contacts ADD COLUMN extra_json jsonb;
ALTER TABLE problem ADD COLUMN extra_json jsonb;
ALTER TABLE problem ADD COLUMN geocode_json jsonb;
ALTER TABLE comment ADD COLUMN extra_json jsonb;
ALTER TABLE moderation_original_data ADD COLUMN extra_json jsonb;
ALTER TABLE defect_types ADD COLUMN extra_json jsonb;
ALTER TABLE report_extra_fields ADD COLUMN extra_json jsonb;
ALTER TABLE roles ADD COLUMN extra_json jsonb;
ALTER TABLE token ADD COLUMN data_json jsonb;

ALTER TABLE token ADD CONSTRAINT token_data_not_null CHECK (data_json IS NOT NULL) NOT VALID;
46 changes: 46 additions & 0 deletions perllib/FixMyStreet/DB/JSONBColumn.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package FixMyStreet::DB::JSONBColumn;

use strict;
use warnings;
use JSON::MaybeXS;

my $JSON;

sub register_column {
my ($self, $column, $info, @rest) = @_;

$self->next::method($column, $info, @rest);

return unless ($info->{data_type} || '') eq 'jsonb';

$JSON ||= JSON->new->allow_nonref->canonical;

$self->filter_column(
$column => {
filter_from_storage => sub {
my ($self, $value) = @_;
return undef unless defined $value;
return $JSON->decode($value);
},
filter_to_storage => sub {
my ($self, $value) = @_;
return $JSON->encode($value);
},
}
);
}

1;

__END__
=head1 NAME
FixMyStreet::DB::JSONBColumn
=head1 DESCRIPTION
Causes 'jsonb' type columns to automatically be JSON encoded and
decoded.
=cut
5 changes: 3 additions & 2 deletions perllib/FixMyStreet/DB/Result/Abuse.pm
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->load_components(
"FilterColumn",
"+FixMyStreet::DB::JSONBColumn",
"FixMyStreet::InflateColumn::DateTime",
"FixMyStreet::EncodedColumn",
);
Expand All @@ -18,8 +19,8 @@ __PACKAGE__->add_columns("email", { data_type => "text", is_nullable => 0 });
__PACKAGE__->set_primary_key("email");


# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:6XdWpymMMUEC4WT9Yh0RLw
# Created by DBIx::Class::Schema::Loader v0.07035 @ 2020-10-14 22:49:08
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:TSod5kxrwSBlXxLSnhVelQ

# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;
8 changes: 4 additions & 4 deletions perllib/FixMyStreet/DB/Result/AdminLog.pm
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->load_components(
"FilterColumn",
"+FixMyStreet::DB::JSONBColumn",
"FixMyStreet::InflateColumn::DateTime",
"FixMyStreet::EncodedColumn",
);
Expand All @@ -33,9 +34,8 @@ __PACKAGE__->add_columns(
"whenedited",
{
data_type => "timestamp",
default_value => \"current_timestamp",
default_value => \"CURRENT_TIMESTAMP",
is_nullable => 0,
original => { default_value => \"now()" },
},
"user_id",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
Expand All @@ -58,8 +58,8 @@ __PACKAGE__->belongs_to(
);


# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:BLPP1KitphuY56ptaXhzgg
# Created by DBIx::Class::Schema::Loader v0.07035 @ 2020-10-14 22:49:08
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:nJv8L2Ggpe1F11T59Bdg3A

sub link {
my $self = shift;
Expand Down
Loading

0 comments on commit b96bd67

Please sign in to comment.