From 4ed3f8e7c64594bb4ea26abb1c4fb51a5a8258d6 Mon Sep 17 00:00:00 2001 From: Marius Gavrilescu Date: Wed, 19 Mar 2014 18:31:42 +0200 Subject: [PATCH] From Redis to Postgres - Part 1 --- Makefile.PL | 1 - db.sql | 74 +++ lib/Gruntmaster/Data.pm | 546 +----------------- lib/Gruntmaster/Data/Result/Contest.pm | 157 +++++ lib/Gruntmaster/Data/Result/ContestProblem.pm | 99 ++++ lib/Gruntmaster/Data/Result/Job.pm | 205 +++++++ lib/Gruntmaster/Data/Result/Open.pm | 131 +++++ lib/Gruntmaster/Data/Result/Problem.pm | 260 +++++++++ lib/Gruntmaster/Data/Result/User.pm | 160 +++++ lib/Gruntmaster/OldData.pm | 542 +++++++++++++++++ redis-to-postgres | 102 ++++ 11 files changed, 1751 insertions(+), 526 deletions(-) create mode 100644 db.sql create mode 100644 lib/Gruntmaster/Data/Result/Contest.pm create mode 100644 lib/Gruntmaster/Data/Result/ContestProblem.pm create mode 100644 lib/Gruntmaster/Data/Result/Job.pm create mode 100644 lib/Gruntmaster/Data/Result/Open.pm create mode 100644 lib/Gruntmaster/Data/Result/Problem.pm create mode 100644 lib/Gruntmaster/Data/Result/User.pm create mode 100644 lib/Gruntmaster/OldData.pm create mode 100755 redis-to-postgres diff --git a/Makefile.PL b/Makefile.PL index eaac7d8..783bf6c 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -4,7 +4,6 @@ use ExtUtils::MakeMaker; WriteMakefile( NAME => 'Gruntmaster::Data', VERSION_FROM => 'lib/Gruntmaster/Data.pm', - EXE_FILES => [ qw/gruntmaster-contest gruntmaster-problem gruntmaster-job/ ], ABSTRACT_FROM => 'lib/Gruntmaster/Data.pm', AUTHOR => 'Marius Gavrilescu ', MIN_PERL_VERSION => '5.14.0', diff --git a/db.sql b/db.sql new file mode 100644 index 0000000..5992b41 --- /dev/null +++ b/db.sql @@ -0,0 +1,74 @@ +CREATE TYPE ULEVEL AS ENUM ('Highschool', 'Undergraduate', 'Master', 'Doctorate', 'Other'); +CREATE TYPE PLEVEL AS ENUM ('beginner', 'easy', 'medium', 'hard'); +CREATE TYPE GENERATOR AS ENUM ('File', 'Run', 'Undef'); +CREATE TYPE RUNNER AS ENUM ('File', 'Verifier', 'Interactive'); +CREATE TYPE JUDGE AS ENUM ('Absolute', 'Points'); + +CREATE TABLE users ( + id TEXT PRIMARY KEY, + name TEXT, -- NOT NULL, + email TEXT, -- NOT NULL, + town TEXT, -- NOT NULL, + university TEXT, -- NOT NULL, + level TEXT, --ULEVEL NOT NULL, + lastjob BIGINT +); + +CREATE TABLE contests ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + start INT NOT NULL, + stop INT NOT NULL, + owner TEXT NOT NULL REFERENCES users, + CONSTRAINT positive_duration CHECK (stop > start) +); + +CREATE TABLE problems ( + id TEXT PRIMARY KEY, + author TEXT NOT NULL, + generator GENERATOR NOT NULL, + judge JUDGE NOT NULL, + level PLEVEL NOT NULL, + name TEXT NOT NULL, + olimit INT, + owner TEXT NOT NULL REFERENCES users, + private BOOLEAN NOT NULL DEFAULT FALSE, + runner RUNNER NOT NULL, + statement TEXT NOT NULL, + testcnt INT NOT NULL, + timeout REAL NOT NULL, + value INT, + verformat TEXT, + versource TEXT +); + +CREATE TABLE contest_problems ( + contest TEXT REFERENCES contests, + problem TEXT NOT NULL REFERENCES problems, + PRIMARY KEY (contest, problem) +); + +CREATE TABLE jobs ( + id SERIAL PRIMARY KEY, + contest TEXT REFERENCES contests, + daemon TEXT, + date BIGINT NOT NULL, + errors TEXT, + extension TEXT NOT NULL, + format TEXT NOT NULL, + private BOOLEAN NOT NULL DEFAULT FALSE, + problem TEXT NOT NULL REFERENCES problems, + result INT, + result_text TEXT, + results JSON, + source TEXT NOT NULL, + owner TEXT NOT NULL REFERENCES users +); + +CREATE TABLE opens ( + contest TEXT NOT NULL REFERENCES contests, + problem TEXT NOT NULL REFERENCES problems, + owner TEXT NOT NULL REFERENCES users, + time BIGINT NOT NULL, + PRIMARY KEY (contest, problem, owner) +); diff --git a/lib/Gruntmaster/Data.pm b/lib/Gruntmaster/Data.pm index f1564e8..9fee650 100644 --- a/lib/Gruntmaster/Data.pm +++ b/lib/Gruntmaster/Data.pm @@ -1,17 +1,24 @@ +use utf8; package Gruntmaster::Data; -use v5.14; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +use strict; use warnings; -use parent qw/Exporter/; -use JSON qw/encode_json decode_json/; -use Redis; -use Sub::Name qw/subname/; +use base 'DBIx::Class::Schema'; + +__PACKAGE__->load_namespaces; -our $VERSION = '5999.000_002'; -our $contest; -my $redis = Redis->new; -my $pubsub = Redis->new; +# Created by DBIx::Class::Schema::Loader v0.07039 @ 2014-03-05 13:11:39 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:dAEmtAexvUaNXLgYz2rNEg + +our $VERSION = 5999.000_003; + +use Lingua::EN::Inflect qw/PL_N/; +use Sub::Name qw/subname/; sub dynsub{ our ($name, $sub) = @_; @@ -20,523 +27,12 @@ sub dynsub{ } BEGIN { - for my $cmd (qw/multi exec smembers get hget hgetall hdel hset sadd srem incr hmset hsetnx publish del/) { - dynsub uc $cmd, sub { $redis->$cmd(@_) }; - } - - for my $cmd (qw/subscribe wait_for_messages/) { - dynsub uc $cmd, sub { $pubsub->$cmd(@_) }; + for my $rs (qw/contest contest_problem job open problem user/) { + my $rsname = ucfirst $rs; + $rsname =~ s/_([a-z])/\u$1/g; + dynsub PL_N($rs) => sub { $_[0]->resultset($rsname) }; + dynsub $rs => sub { $_[0]->resultset($rsname)->find($_[1]) }; } } -sub cp { defined $contest ? "contest.$contest." : '' } - -sub problems () { SMEMBERS cp . 'problem' } -sub contests () { SMEMBERS cp . 'contest' } -sub users () { SMEMBERS cp . 'user' } -sub jobcard () { GET cp . 'job' } - -sub job_results (_) { decode_json HGET cp . "job.$_[0]", 'results' } -sub set_job_results ($+) { HSET cp . "job.$_[0]", 'results', encode_json $_[1] } -sub job_inmeta (_) { decode_json HGET cp . "job.$_[0]", 'inmeta' } -sub set_job_inmeta ($+) { HSET cp . "job.$_[0]", 'inmeta', encode_json $_[1] } -sub problem_meta (_) { decode_json HGET cp . "problem.$_[0]", 'meta' } -sub set_problem_meta ($+) { HSET cp . "problem.$_[0]", 'meta', encode_json $_[1] } -sub job_daemon (_) { HGET cp . "job.$_[0]", 'daemon' } -sub set_job_daemon ($$) { HSETNX cp . "job.$_[0]", 'daemon', $_[1] }; - -sub defhash{ - my ($name, @keys) = @_; - for my $key (@keys) { - dynsub "${name}_$key", sub (_) { HGET cp . "$name.$_[0]", $key }; - dynsub "set_${name}_$key", sub ($$) { HSET cp . "$name.$_[0]", $key, $_[1] }; - } - - dynsub "edit_$name", sub { - my ($key, %values) = @_; - HMSET cp . "$name.$key", %values; - }; - - dynsub "insert_$name", sub { - my ($key, %values) = @_; - SADD cp . $name, $key or return; - HMSET cp . "$name.$key", %values; - }; - dynsub "remove_$name", sub (_) { - my $key = shift; - SREM cp . $name, $key; - DEL cp . "$name.$key"; - }; - - dynsub "push_$name", sub { - my $nr = INCR cp . $name; - HMSET cp . "$name.$nr", @_; - $nr - }; -} - -defhash problem => qw/name level difficulty statement owner author private generator runner judge testcnt timeout olimit/; -defhash contest => qw/start end name owner description/; -defhash job => qw/date errors extension filesize private problem result result_text user/; -defhash user => qw/name email lastjob town university level/; - -sub clean_job (_){ - HDEL cp . "job.$_[0]", qw/result result_text results daemon/ -} - -sub mark_open { - my ($problem, $user) = @_; - HSETNX cp . 'open', "$problem.$user", time; -} - -sub get_open { - my ($problem, $user) = @_; - HGET cp . 'open', "$problem.$user"; -} - -our @EXPORT = do { - no strict 'refs'; - grep { $_ =~ /^[a-zA-Z]/ and exists &$_ } keys %{__PACKAGE__ . '::'}; -}; - 1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Gruntmaster::Data - Gruntmaster 6000 Online Judge -- database interface and tools - -=head1 SYNOPSIS - - for my $problem (problems) { - say "Problem name: " . problem_name $problem; - say "Problem level: " . problem_level $problem; - ... - } - -=head1 DESCRIPTION - -Gruntmaster::Data is the Redis interface used by the Gruntmaster 6000 Online Judge. It exports many functions for talking to the database. All functions are exported by default. - -The current contest is selected by setting the C<< $Gruntmaster::Data::contest >> variable. - - local $Gruntmaster::Data::contest = 'mycontest'; - say 'There are' . jobcard . ' jobs in my contest'; - -=head1 FUNCTIONS - -=head2 Redis - -Gruntmaster::Data exports some functions for talking directly to the Redis server. These functions should not normally be used, except for B, B, B, B and B. - -These functions correspond to Redis commands. The current list is: B<< MULTI EXEC SMEMBERS GET HGET HGETALL HDEL HSET SADD SREM INCR HMSET HSETNX DEL PUBLISH SUBSCRIBE WAIT_FOR_MESSAGES >>. - -=head2 Problems - -=over - -=item B - -Returns a list of problems in the current contest. - -=item B I<$problem> - -Returns a problem's meta. - -=item B I<$problem>, I<$meta> - -Sets a problem's meta. - -=item B I<$problem> - -Returns a problem's name. - -=item B I<$problem>, I<$name> - -Sets a problem's name. - -=item B I<$problem> - -Returns a problem's level. The levels are beginner, easy, medium, hard. - -=item B I<$problem>, I<$level> - -Sets a problem's level. The levels are beginner, easy, medium, hard. - -=item B I<$problem> - -Returns a problem's difficulty. - -=item B I<$problem>, I<$name> - -Sets a problem's difficulty. - -=item B I<$problem> - -Returns a problem's statement. - -=item B I<$problem>, I<$statement> - -Sets a problem's statement. - -=item B I<$problem> - -Returns a problem's owner. - -=item B I<$problem>, I<$owner> - -Sets a problem's owner. - -=item B I<$problem> - -Returns a problem's author. - -=item B I<$problem>, I<$author> - -Sets a problem's author. - -=item B I<$problem> - -Returns a problem's private flag (true if the problem is private, false otherwise). - -=item B I<$problem>, I<$private> - -Sets a problem's private flag. - -=item B I<$problem> - -Returns a problem's generator. The generators are File, Run and Undef. More might be added in the future. - -=item B I<$problem>, I<$generator> - -Sets a problem's generator. - -=item B I<$problem> - -Returns a problem's runner. The runners are File, Verifier and Interactive. More might be added in the future. - -=item B I<$problem>, I<$runner> - -Sets a problem's runner. - -=item B I<$problem> - -Returns a problem's judge. The judges are Absolute and Points. More might be added in the future. - -=item B I<$problem>, I<$judge> - -Sets a problem's judge. - -=item B I<$problem> - -Returns a problem's test count. - -=item B I<$problem>, I<$testcnt> - -Sets a problem's test count. - -=item B I<$problem> - -Returns a problem's time limit, in seconds. - -=item B I<$problem>, I<$timeout> - -Sets a problem's time limit, in seconds. - -=item B I<$problem> - -Returns a problem's output limit, in bytes. - -=item B I<$problem>, I<$olimit> - -Sets a problem's output limit, in bytes. - -=item B I<$problem>, I<$user> - -Returns the time when I<$user> opened I<$problem>. - -=item B I<$problem>, I<$user> - -Sets the time when I<$user> opened I<$problem> to the current time. Does nothing if I<$user> has already opened I<$problem>. - -=item B I<$id>, I<$key> => I<$value>, ... - -Inserts a problem with id I<$id> and the given initial configuration. Does nothing if a problem with id I<$id> already exists. Returns true if the problem was added, false otherwise. - -=item B I<$id>, I<$key> => I<$value>, ... - -Updates the configuration of a problem. The values of the given keys are updated. All other keys/values are left intact. - -=item B I<$id> - -Removes a problem. - -=back - -=head2 Contests - -B<<< WARNING: these functions only work correctly when C<< $Gruntmaster::Data::contest >> is undef >>> - -=over - -=item B - -Returns a list of contests. - -=item B I<$contest> - -Returns a contest's start time. - -=item B I<$contest>, I<$start> - -Sets a contest's start time. - -=item B I<$contest> - -Returns a contest's end time. - -=item B I<$contest>, I<$end> - -Sets a contest's end time. - -=item B I<$contest> - -Returns a contest's name. - -=item B I<$contest>, I<$name> - -Sets a contest's name. - -=item B I<$contest> - -Returns a contest's owner. - -=item B I<$contest>, I<$owner> - -Sets a contest's owner. - -=item B I<$id>, I<$key> => I<$value>, ... - -Inserts a contest with id I<$id> and the given initial configuration. Does nothing if a contest with id I<$id> already exists. Returns true if the contest was added, false otherwise. - -=item B I<$id>, I<$key> => I<$value>, ... - -Updates the configuration of a contest. The values of the given keys are updated. All other keys/values are left intact. - -=item B I<$id> - -Removes a contest. - -=back - -=head2 Jobs - -=over - -=item B - -Returns the number of jobs in the database. - -=item B I<$job> - -Returns an array of job results. Each element corresponds to a test and is a hashref with keys B (test number), B (result code, see L), B (result description) and B + +=back + +=cut + +__PACKAGE__->set_primary_key("id"); + +=head1 RELATIONS + +=head2 contest_problems + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "contest_problems", + "Gruntmaster::Data::Result::ContestProblem", + { "foreign.contest" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 jobs + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "jobs", + "Gruntmaster::Data::Result::Job", + { "foreign.contest" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 opens + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "opens", + "Gruntmaster::Data::Result::Open", + { "foreign.contest" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 owner + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "owner", + "Gruntmaster::Data::Result::User", + { id => "owner" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + +=head2 problems + +Type: many_to_many + +Composing rels: L -> problem + +=cut + +__PACKAGE__->many_to_many("problems", "contest_problems", "problem"); + + +# Created by DBIx::Class::Schema::Loader v0.07039 @ 2014-03-06 12:41:16 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:T5tUpU1TOahLKzx9iVie3A + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/lib/Gruntmaster/Data/Result/ContestProblem.pm b/lib/Gruntmaster/Data/Result/ContestProblem.pm new file mode 100644 index 0000000..a1fe4b4 --- /dev/null +++ b/lib/Gruntmaster/Data/Result/ContestProblem.pm @@ -0,0 +1,99 @@ +use utf8; +package Gruntmaster::Data::Result::ContestProblem; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Gruntmaster::Data::Result::ContestProblem + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("contest_problems"); + +=head1 ACCESSORS + +=head2 contest + + data_type: 'text' + is_foreign_key: 1 + is_nullable: 0 + +=head2 problem + + data_type: 'text' + is_foreign_key: 1 + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "contest", + { data_type => "text", is_foreign_key => 1, is_nullable => 0 }, + "problem", + { data_type => "text", is_foreign_key => 1, is_nullable => 0 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("contest", "problem"); + +=head1 RELATIONS + +=head2 contest + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "contest", + "Gruntmaster::Data::Result::Contest", + { id => "contest" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + +=head2 problem + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "problem", + "Gruntmaster::Data::Result::Problem", + { id => "problem" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07039 @ 2014-03-06 12:41:16 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:2vVP0Z6QcLz8DiobdOceyQ + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/lib/Gruntmaster/Data/Result/Job.pm b/lib/Gruntmaster/Data/Result/Job.pm new file mode 100644 index 0000000..6f1084f --- /dev/null +++ b/lib/Gruntmaster/Data/Result/Job.pm @@ -0,0 +1,205 @@ +use utf8; +package Gruntmaster::Data::Result::Job; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Gruntmaster::Data::Result::Job + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("jobs"); + +=head1 ACCESSORS + +=head2 id + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + sequence: 'jobs_id_seq' + +=head2 contest + + data_type: 'text' + is_foreign_key: 1 + is_nullable: 0 + +=head2 daemon + + data_type: 'text' + is_nullable: 1 + +=head2 date + + data_type: 'bigint' + is_nullable: 0 + +=head2 errors + + data_type: 'text' + is_nullable: 1 + +=head2 extension + + data_type: 'text' + is_nullable: 0 + +=head2 format + + data_type: 'text' + is_nullable: 0 + +=head2 private + + data_type: 'boolean' + default_value: false + is_nullable: 0 + +=head2 problem + + data_type: 'text' + is_foreign_key: 1 + is_nullable: 0 + +=head2 result + + data_type: 'integer' + is_nullable: 1 + +=head2 result_text + + data_type: 'text' + is_nullable: 1 + +=head2 results + + data_type: 'json' + is_nullable: 1 + +=head2 source + + data_type: 'text' + is_nullable: 0 + +=head2 owner + + data_type: 'text' + is_foreign_key: 1 + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "id", + { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + sequence => "jobs_id_seq", + }, + "contest", + { data_type => "text", is_foreign_key => 1, is_nullable => 0 }, + "daemon", + { data_type => "text", is_nullable => 1 }, + "date", + { data_type => "bigint", is_nullable => 0 }, + "errors", + { data_type => "text", is_nullable => 1 }, + "extension", + { data_type => "text", is_nullable => 0 }, + "format", + { data_type => "text", is_nullable => 0 }, + "private", + { data_type => "boolean", default_value => \"false", is_nullable => 0 }, + "problem", + { data_type => "text", is_foreign_key => 1, is_nullable => 0 }, + "result", + { data_type => "integer", is_nullable => 1 }, + "result_text", + { data_type => "text", is_nullable => 1 }, + "results", + { data_type => "json", is_nullable => 1 }, + "source", + { data_type => "text", is_nullable => 0 }, + "owner", + { data_type => "text", is_foreign_key => 1, is_nullable => 0 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("id"); + +=head1 RELATIONS + +=head2 contest + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "contest", + "Gruntmaster::Data::Result::Contest", + { id => "contest" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + +=head2 owner + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "owner", + "Gruntmaster::Data::Result::User", + { id => "owner" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + +=head2 problem + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "problem", + "Gruntmaster::Data::Result::Problem", + { id => "problem" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07039 @ 2014-03-06 12:41:16 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:lIETgGgMTSOUUuDuFa/+SQ + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/lib/Gruntmaster/Data/Result/Open.pm b/lib/Gruntmaster/Data/Result/Open.pm new file mode 100644 index 0000000..cd0c7e1 --- /dev/null +++ b/lib/Gruntmaster/Data/Result/Open.pm @@ -0,0 +1,131 @@ +use utf8; +package Gruntmaster::Data::Result::Open; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Gruntmaster::Data::Result::Open + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("opens"); + +=head1 ACCESSORS + +=head2 contest + + data_type: 'text' + is_foreign_key: 1 + is_nullable: 0 + +=head2 problem + + data_type: 'text' + is_foreign_key: 1 + is_nullable: 0 + +=head2 owner + + data_type: 'text' + is_foreign_key: 1 + is_nullable: 0 + +=head2 time + + data_type: 'bigint' + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "contest", + { data_type => "text", is_foreign_key => 1, is_nullable => 0 }, + "problem", + { data_type => "text", is_foreign_key => 1, is_nullable => 0 }, + "owner", + { data_type => "text", is_foreign_key => 1, is_nullable => 0 }, + "time", + { data_type => "bigint", is_nullable => 0 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=item * L + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("contest", "problem", "owner"); + +=head1 RELATIONS + +=head2 contest + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "contest", + "Gruntmaster::Data::Result::Contest", + { id => "contest" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + +=head2 owner + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "owner", + "Gruntmaster::Data::Result::User", + { id => "owner" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + +=head2 problem + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "problem", + "Gruntmaster::Data::Result::Problem", + { id => "problem" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07039 @ 2014-03-06 12:41:16 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:+zXnz+V4BYSNwxN65ovu2w + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/lib/Gruntmaster/Data/Result/Problem.pm b/lib/Gruntmaster/Data/Result/Problem.pm new file mode 100644 index 0000000..5359df7 --- /dev/null +++ b/lib/Gruntmaster/Data/Result/Problem.pm @@ -0,0 +1,260 @@ +use utf8; +package Gruntmaster::Data::Result::Problem; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Gruntmaster::Data::Result::Problem + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("problems"); + +=head1 ACCESSORS + +=head2 id + + data_type: 'text' + is_nullable: 0 + +=head2 author + + data_type: 'text' + is_nullable: 0 + +=head2 generator + + data_type: 'enum' + extra: {custom_type_name => "generator",list => ["File","Run","Undef"]} + is_nullable: 0 + +=head2 judge + + data_type: 'enum' + extra: {custom_type_name => "judge",list => ["Absolute","Points"]} + is_nullable: 0 + +=head2 level + + data_type: 'enum' + extra: {custom_type_name => "plevel",list => ["beginner","easy","medium","hard"]} + is_nullable: 0 + +=head2 name + + data_type: 'text' + is_nullable: 0 + +=head2 olimit + + data_type: 'integer' + is_nullable: 1 + +=head2 owner + + data_type: 'text' + is_foreign_key: 1 + is_nullable: 0 + +=head2 private + + data_type: 'boolean' + is_nullable: 0 + +=head2 runner + + data_type: 'enum' + extra: {custom_type_name => "runner",list => ["File","Verifier","Interactive"]} + is_nullable: 0 + +=head2 statement + + data_type: 'text' + is_nullable: 0 + +=head2 testcnt + + data_type: 'integer' + is_nullable: 0 + +=head2 timeout + + data_type: 'real' + is_nullable: 0 + +=head2 value + + data_type: 'integer' + is_nullable: 1 + +=head2 verformat + + data_type: 'text' + is_nullable: 1 + +=head2 versource + + data_type: 'text' + is_nullable: 1 + +=cut + +__PACKAGE__->add_columns( + "id", + { data_type => "text", is_nullable => 0 }, + "author", + { data_type => "text", is_nullable => 0 }, + "generator", + { + data_type => "enum", + extra => { custom_type_name => "generator", list => ["File", "Run", "Undef"] }, + is_nullable => 0, + }, + "judge", + { + data_type => "enum", + extra => { custom_type_name => "judge", list => ["Absolute", "Points"] }, + is_nullable => 0, + }, + "level", + { + data_type => "enum", + extra => { + custom_type_name => "plevel", + list => ["beginner", "easy", "medium", "hard"], + }, + is_nullable => 0, + }, + "name", + { data_type => "text", is_nullable => 0 }, + "olimit", + { data_type => "integer", is_nullable => 1 }, + "owner", + { data_type => "text", is_foreign_key => 1, is_nullable => 0 }, + "private", + { data_type => "boolean", is_nullable => 0 }, + "runner", + { + data_type => "enum", + extra => { + custom_type_name => "runner", + list => ["File", "Verifier", "Interactive"], + }, + is_nullable => 0, + }, + "statement", + { data_type => "text", is_nullable => 0 }, + "testcnt", + { data_type => "integer", is_nullable => 0 }, + "timeout", + { data_type => "real", is_nullable => 0 }, + "value", + { data_type => "integer", is_nullable => 1 }, + "verformat", + { data_type => "text", is_nullable => 1 }, + "versource", + { data_type => "text", is_nullable => 1 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("id"); + +=head1 RELATIONS + +=head2 contest_problems + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "contest_problems", + "Gruntmaster::Data::Result::ContestProblem", + { "foreign.problem" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 jobs + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "jobs", + "Gruntmaster::Data::Result::Job", + { "foreign.problem" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 opens + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "opens", + "Gruntmaster::Data::Result::Open", + { "foreign.problem" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 owner + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "owner", + "Gruntmaster::Data::Result::User", + { id => "owner" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + +=head2 contests + +Type: many_to_many + +Composing rels: L -> contest + +=cut + +__PACKAGE__->many_to_many("contests", "contest_problems", "contest"); + + +# Created by DBIx::Class::Schema::Loader v0.07039 @ 2014-03-06 12:41:16 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:y1LUGcxNJxUMgMXqvAkKYQ + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/lib/Gruntmaster/Data/Result/User.pm b/lib/Gruntmaster/Data/Result/User.pm new file mode 100644 index 0000000..740d87a --- /dev/null +++ b/lib/Gruntmaster/Data/Result/User.pm @@ -0,0 +1,160 @@ +use utf8; +package Gruntmaster::Data::Result::User; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Gruntmaster::Data::Result::User + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("users"); + +=head1 ACCESSORS + +=head2 id + + data_type: 'text' + is_nullable: 0 + +=head2 name + + data_type: 'text' + is_nullable: 1 + +=head2 email + + data_type: 'text' + is_nullable: 1 + +=head2 town + + data_type: 'text' + is_nullable: 1 + +=head2 university + + data_type: 'text' + is_nullable: 1 + +=head2 level + + data_type: 'text' + is_nullable: 1 + +=head2 lastjob + + data_type: 'bigint' + is_nullable: 1 + +=cut + +__PACKAGE__->add_columns( + "id", + { data_type => "text", is_nullable => 0 }, + "name", + { data_type => "text", is_nullable => 1 }, + "email", + { data_type => "text", is_nullable => 1 }, + "town", + { data_type => "text", is_nullable => 1 }, + "university", + { data_type => "text", is_nullable => 1 }, + "level", + { data_type => "text", is_nullable => 1 }, + "lastjob", + { data_type => "bigint", is_nullable => 1 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("id"); + +=head1 RELATIONS + +=head2 contests + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "contests", + "Gruntmaster::Data::Result::Contest", + { "foreign.owner" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 jobs + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "jobs", + "Gruntmaster::Data::Result::Job", + { "foreign.owner" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 opens + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "opens", + "Gruntmaster::Data::Result::Open", + { "foreign.owner" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 problems + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "problems", + "Gruntmaster::Data::Result::Problem", + { "foreign.owner" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07039 @ 2014-03-06 12:41:16 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:CHRtUlZf3hs+lg6Nqi2LPA + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/lib/Gruntmaster/OldData.pm b/lib/Gruntmaster/OldData.pm new file mode 100644 index 0000000..d3ac62d --- /dev/null +++ b/lib/Gruntmaster/OldData.pm @@ -0,0 +1,542 @@ +package Gruntmaster::OldData; +use v5.14; +use warnings; +use parent qw/Exporter/; + +use JSON qw/encode_json decode_json/; +use Redis; +use Sub::Name qw/subname/; + +our $VERSION = '5999.000_002'; + +our $contest; +my $redis = Redis->new; +my $pubsub = Redis->new; + +sub dynsub{ + our ($name, $sub) = @_; + no strict 'refs'; + *$name = subname $name => $sub +} + +BEGIN { + for my $cmd (qw/multi exec smembers get hget hgetall hdel hset sadd srem incr hmset hsetnx publish del/) { + dynsub uc $cmd, sub { $redis->$cmd(@_) }; + } + + for my $cmd (qw/subscribe wait_for_messages/) { + dynsub uc $cmd, sub { $pubsub->$cmd(@_) }; + } +} + +sub cp { defined $contest ? "contest.$contest." : '' } + +sub problems () { SMEMBERS cp . 'problem' } +sub contests () { SMEMBERS cp . 'contest' } +sub users () { SMEMBERS cp . 'user' } +sub jobcard () { GET cp . 'job' } + +sub job_results (_) { decode_json HGET cp . "job.$_[0]", 'results' } +sub set_job_results ($+) { HSET cp . "job.$_[0]", 'results', encode_json $_[1] } +sub job_inmeta (_) { decode_json HGET cp . "job.$_[0]", 'inmeta' } +sub set_job_inmeta ($+) { HSET cp . "job.$_[0]", 'inmeta', encode_json $_[1] } +sub problem_meta (_) { decode_json HGET cp . "problem.$_[0]", 'meta' } +sub set_problem_meta ($+) { HSET cp . "problem.$_[0]", 'meta', encode_json $_[1] } +sub job_daemon (_) { HGET cp . "job.$_[0]", 'daemon' } +sub set_job_daemon ($$) { HSETNX cp . "job.$_[0]", 'daemon', $_[1] }; + +sub defhash{ + my ($name, @keys) = @_; + for my $key (@keys) { + dynsub "${name}_$key", sub (_) { HGET cp . "$name.$_[0]", $key }; + dynsub "set_${name}_$key", sub ($$) { HSET cp . "$name.$_[0]", $key, $_[1] }; + } + + dynsub "edit_$name", sub { + my ($key, %values) = @_; + HMSET cp . "$name.$key", %values; + }; + + dynsub "insert_$name", sub { + my ($key, %values) = @_; + SADD cp . $name, $key or return; + HMSET cp . "$name.$key", %values; + }; + dynsub "remove_$name", sub (_) { + my $key = shift; + SREM cp . $name, $key; + DEL cp . "$name.$key"; + }; + + dynsub "push_$name", sub { + my $nr = INCR cp . $name; + HMSET cp . "$name.$nr", @_; + $nr + }; +} + +defhash problem => qw/name level difficulty statement owner author private generator runner judge testcnt timeout olimit/; +defhash contest => qw/start end name owner/; +defhash job => qw/date errors extension filesize private problem result result_text user/; +defhash user => qw/name email lastjob town university level/; + +sub clean_job (_){ + HDEL cp . "job.$_[0]", qw/result result_text results daemon/ +} + +sub mark_open { + my ($problem, $user) = @_; + HSETNX cp . 'open', "$problem.$user", time; +} + +sub get_open { + my ($problem, $user) = @_; + HGET cp . 'open', "$problem.$user"; +} + +our @EXPORT = do { + no strict 'refs'; + grep { $_ =~ /^[a-zA-Z]/ and exists &$_ } keys %{__PACKAGE__ . '::'}; +}; + +1; +__END__ + +=encoding utf-8 + +=head1 NAME + +Gruntmaster::Data - Gruntmaster 6000 Online Judge -- database interface and tools + +=head1 SYNOPSIS + + for my $problem (problems) { + say "Problem name: " . problem_name $problem; + say "Problem level: " . problem_level $problem; + ... + } + +=head1 DESCRIPTION + +Gruntmaster::Data is the Redis interface used by the Gruntmaster 6000 Online Judge. It exports many functions for talking to the database. All functions are exported by default. + +The current contest is selected by setting the C<< $Gruntmaster::Data::contest >> variable. + + local $Gruntmaster::Data::contest = 'mycontest'; + say 'There are' . jobcard . ' jobs in my contest'; + +=head1 FUNCTIONS + +=head2 Redis + +Gruntmaster::Data exports some functions for talking directly to the Redis server. These functions should not normally be used, except for B, B, B, B and B. + +These functions correspond to Redis commands. The current list is: B<< MULTI EXEC SMEMBERS GET HGET HGETALL HDEL HSET SADD SREM INCR HMSET HSETNX DEL PUBLISH SUBSCRIBE WAIT_FOR_MESSAGES >>. + +=head2 Problems + +=over + +=item B + +Returns a list of problems in the current contest. + +=item B I<$problem> + +Returns a problem's meta. + +=item B I<$problem>, I<$meta> + +Sets a problem's meta. + +=item B I<$problem> + +Returns a problem's name. + +=item B I<$problem>, I<$name> + +Sets a problem's name. + +=item B I<$problem> + +Returns a problem's level. The levels are beginner, easy, medium, hard. + +=item B I<$problem>, I<$level> + +Sets a problem's level. The levels are beginner, easy, medium, hard. + +=item B I<$problem> + +Returns a problem's difficulty. + +=item B I<$problem>, I<$name> + +Sets a problem's difficulty. + +=item B I<$problem> + +Returns a problem's statement. + +=item B I<$problem>, I<$statement> + +Sets a problem's statement. + +=item B I<$problem> + +Returns a problem's owner. + +=item B I<$problem>, I<$owner> + +Sets a problem's owner. + +=item B I<$problem> + +Returns a problem's author. + +=item B I<$problem>, I<$author> + +Sets a problem's author. + +=item B I<$problem> + +Returns a problem's private flag (true if the problem is private, false otherwise). + +=item B I<$problem>, I<$private> + +Sets a problem's private flag. + +=item B I<$problem> + +Returns a problem's generator. The generators are File, Run and Undef. More might be added in the future. + +=item B I<$problem>, I<$generator> + +Sets a problem's generator. + +=item B I<$problem> + +Returns a problem's runner. The runners are File, Verifier and Interactive. More might be added in the future. + +=item B I<$problem>, I<$runner> + +Sets a problem's runner. + +=item B I<$problem> + +Returns a problem's judge. The judges are Absolute and Points. More might be added in the future. + +=item B I<$problem>, I<$judge> + +Sets a problem's judge. + +=item B I<$problem> + +Returns a problem's test count. + +=item B I<$problem>, I<$testcnt> + +Sets a problem's test count. + +=item B I<$problem> + +Returns a problem's time limit, in seconds. + +=item B I<$problem>, I<$timeout> + +Sets a problem's time limit, in seconds. + +=item B I<$problem> + +Returns a problem's output limit, in bytes. + +=item B I<$problem>, I<$olimit> + +Sets a problem's output limit, in bytes. + +=item B I<$problem>, I<$user> + +Returns the time when I<$user> opened I<$problem>. + +=item B I<$problem>, I<$user> + +Sets the time when I<$user> opened I<$problem> to the current time. Does nothing if I<$user> has already opened I<$problem>. + +=item B I<$id>, I<$key> => I<$value>, ... + +Inserts a problem with id I<$id> and the given initial configuration. Does nothing if a problem with id I<$id> already exists. Returns true if the problem was added, false otherwise. + +=item B I<$id>, I<$key> => I<$value>, ... + +Updates the configuration of a problem. The values of the given keys are updated. All other keys/values are left intact. + +=item B I<$id> + +Removes a problem. + +=back + +=head2 Contests + +B<<< WARNING: these functions only work correctly when C<< $Gruntmaster::Data::contest >> is undef >>> + +=over + +=item B + +Returns a list of contests. + +=item B I<$contest> + +Returns a contest's start time. + +=item B I<$contest>, I<$start> + +Sets a contest's start time. + +=item B I<$contest> + +Returns a contest's end time. + +=item B I<$contest>, I<$end> + +Sets a contest's end time. + +=item B I<$contest> + +Returns a contest's name. + +=item B I<$contest>, I<$name> + +Sets a contest's name. + +=item B I<$contest> + +Returns a contest's owner. + +=item B I<$contest>, I<$owner> + +Sets a contest's owner. + +=item B I<$id>, I<$key> => I<$value>, ... + +Inserts a contest with id I<$id> and the given initial configuration. Does nothing if a contest with id I<$id> already exists. Returns true if the contest was added, false otherwise. + +=item B I<$id>, I<$key> => I<$value>, ... + +Updates the configuration of a contest. The values of the given keys are updated. All other keys/values are left intact. + +=item B I<$id> + +Removes a contest. + +=back + +=head2 Jobs + +=over + +=item B + +Returns the number of jobs in the database. + +=item B I<$job> + +Returns an array of job results. Each element corresponds to a test and is a hashref with keys B (test number), B (result code, see L), B (result description) and B