]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 1532409 - Introduce Bugzilla::Model (a DBIx::Class::Schema)
authorDylan William Hardison <dylan@hardison.net>
Fri, 8 Mar 2019 00:30:05 +0000 (19:30 -0500)
committerGitHub <noreply@github.com>
Fri, 8 Mar 2019 00:30:05 +0000 (19:30 -0500)
15 files changed:
Bugzilla/DB.pm
Bugzilla/Model.pm [new file with mode: 0644]
Bugzilla/Model/Result/Bug.pm [new file with mode: 0644]
Bugzilla/Model/Result/BugGroup.pm [new file with mode: 0644]
Bugzilla/Model/Result/BugKeyword.pm [new file with mode: 0644]
Bugzilla/Model/Result/Component.pm [new file with mode: 0644]
Bugzilla/Model/Result/Flag.pm [new file with mode: 0644]
Bugzilla/Model/Result/FlagType.pm [new file with mode: 0644]
Bugzilla/Model/Result/Group.pm [new file with mode: 0644]
Bugzilla/Model/Result/Keyword.pm [new file with mode: 0644]
Bugzilla/Model/Result/Product.pm [new file with mode: 0644]
Bugzilla/Model/Result/User.pm [new file with mode: 0644]
Bugzilla/Model/ResultSet.pm [new file with mode: 0644]
Bugzilla/Object.pm
t/model.t [new file with mode: 0644]

index dc66b8bfb764cf47e372c88879f97abc99f0bcf9..4cba3af988f67e363b62484028ecb20bbee2ed94 100644 (file)
@@ -13,8 +13,6 @@ use Moo;
 use DBI;
 use DBIx::Connector;
 
-has 'connector' => (is => 'lazy', handles => [qw( dbh )],);
-
 use Bugzilla::Logging;
 use Bugzilla::Constants;
 use Bugzilla::Install::Requirements;
@@ -33,8 +31,26 @@ use Storable qw(dclone);
 use English qw(-no_match_vars);
 use Module::Runtime qw(require_module);
 
-has [qw(dsn user pass attrs)] => (is => 'ro', required => 1,);
+has 'connector' => (is => 'lazy', handles => [qw( dbh )]);
+has 'model'     => (is => 'lazy');
+has [qw(dsn user pass attrs)] => (is => 'ro', required => 1);
+
+around 'attrs' => sub {
+  my ($method, $self) = @_;
+  my $attrs = dclone($self->$method);
+  my $class = ref $self;
 
+  # This is only used by the DBIx::Class code DBIx::Connector does something
+  # different because the old bugzilla code has its own ideas about
+  # transactions.
+  $attrs->{Callbacks}{connected} = sub {
+    my ($dbh, $dsn) = @_;
+    $class->on_dbi_connected(@_) if $class->can('on_dbi_connected');
+    return;
+  };
+
+  return $attrs;
+};
 
 # Install proxy methods to the DBI object.
 # We can't use handles() as DBIx::Connector->dbh has to be called each
@@ -239,7 +255,7 @@ sub bz_check_server_version {
 
   my $sql_vers = $self->bz_server_version;
 
-  my $sql_want = $db->{db_version};
+  my $sql_want   = $db->{db_version};
   my $version_ok = vers_cmp($sql_vers, $sql_want) > -1 ? 1 : 0;
 
   my $sql_server = $db->{name};
@@ -275,7 +291,7 @@ sub bz_create_database {
 
   # See if we can connect to the actual Bugzilla database.
   my $conn_success = eval { $dbh = connect_main() };
-  my $db_name = Bugzilla->localconfig->{db_name};
+  my $db_name      = Bugzilla->localconfig->{db_name};
 
   if (!$conn_success) {
     $dbh = _get_no_db_connection();
@@ -1152,7 +1168,7 @@ sub _bz_schema {
   my ($self) = @_;
   return $self->{private_bz_schema} if exists $self->{private_bz_schema};
   my @module_parts = split('::', ref $self);
-  my $module_name = pop @module_parts;
+  my $module_name  = pop @module_parts;
   $self->{private_bz_schema} = Bugzilla::DB::Schema->new($module_name);
   return $self->{private_bz_schema};
 }
@@ -1316,6 +1332,12 @@ sub bz_rollback_transaction {
 # Subclass Helpers
 #####################################################################
 
+sub _build_model {
+  my ($self) = @_;
+  require Bugzilla::Model;
+  Bugzilla::Model->connect($self->dsn, $self->user, $self->pass, $self->attrs);
+}
+
 sub _build_connector {
   my ($self) = @_;
   my ($dsn, $user, $pass, $override_attrs)
@@ -1344,6 +1366,9 @@ sub _build_connector {
     }
   }
   my $class = ref $self;
+
+# This is different from the around 'attrs' above because we need a reference to $self.
+# If we ever kill $dbh->bz_start_transaction() this hack can go away.
   weaken($self);
   $attributes->{Callbacks} = {
     connected => sub {
@@ -1518,7 +1543,7 @@ sub _bz_populate_enum_table {
     my $insert
       = $self->prepare("INSERT INTO $sql_table (value,sortkey) VALUES (?,?)");
     my $sortorder = 0;
-    my $maxlen = max(map(length($_), @$valuelist)) + 2;
+    my $maxlen    = max(map(length($_), @$valuelist)) + 2;
     foreach my $value (@$valuelist) {
       $sortorder += 100;
       $insert->execute($value, $sortorder);
diff --git a/Bugzilla/Model.pm b/Bugzilla/Model.pm
new file mode 100644 (file)
index 0000000..901ea54
--- /dev/null
@@ -0,0 +1,51 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Model;
+use Mojo::Base 'DBIx::Class::Schema';
+
+__PACKAGE__->load_namespaces(default_resultset_class => 'ResultSet');
+__PACKAGE__->load_components('Helper::Schema::QuoteNames');
+
+1;
+
+=head1 NAME
+
+Bugzilla::Model - a DBIx::Class::Schema for Bugzilla
+
+=head1 SYNOPSIS
+
+  my $model      = Bugzilla->dbh->model;
+  my $firefox_rs = $model->resultset('Bug')->search({'product.name' => 'Firefox'},
+    {join => ['product', {bug_keywords => 'keyword'}]});
+  my @report = $firefox_rs->group_by('bug_id')->columns({
+    bug_id   => 'bug.bug_id',
+    summary  => 'bug.short_desc',
+    product  => 'product.name',
+    keywords => {group_concat => 'keyword.name'}
+  })->hri->all;
+  is(
+    \@report,
+    [
+      {
+        bug_id   => 1,
+        keywords => 'regression,relnote',
+        product  => 'Firefox',
+        summary  => 'Some bug'
+      },
+      {
+        bug_id   => 2,
+        keywords => undef,
+        product  => 'Firefox',
+        summary  => 'some other bug'
+      }
+    ]
+  );
+
+=head1 SEE ALSO
+
+See L<DBIx::Class> and L<DBIx::Class::Helper::ResultSet::Shortcut> for more examples of usage.
diff --git a/Bugzilla/Model/Result/Bug.pm b/Bugzilla/Model/Result/Bug.pm
new file mode 100644 (file)
index 0000000..e4e052e
--- /dev/null
@@ -0,0 +1,56 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Model::Result::Bug;
+use Mojo::Base 'DBIx::Class::Core';
+
+__PACKAGE__->table(Bugzilla::Bug->DB_TABLE);
+__PACKAGE__->add_columns(Bugzilla::Bug->DB_COLUMN_NAMES);
+__PACKAGE__->set_primary_key(Bugzilla::Bug->ID_FIELD);
+
+__PACKAGE__->has_one(
+  reporter => 'Bugzilla::Model::Result::User',
+  {'foreign.userid' => 'self.reporter'}
+);
+
+__PACKAGE__->has_one(
+  assigned_to => 'Bugzilla::Model::Result::User',
+  {'foreign.userid' => 'self.assigned_to'}
+);
+__PACKAGE__->might_have(
+  qa_contact => 'Bugzilla::Model::Result::User',
+  {'foreign.userid' => 'self.qa_contact'}
+);
+
+__PACKAGE__->has_many(
+  bug_keywords => 'Bugzilla::Model::Result::BugKeyword',
+  'bug_id'
+);
+
+__PACKAGE__->many_to_many(keywords => 'bug_keywords', 'keyword');
+
+__PACKAGE__->has_many(flags => 'Bugzilla::Model::Result::Flag', 'bug_id');
+
+__PACKAGE__->has_many(
+  bug_groups => 'Bugzilla::Model::Result::BugGroup',
+  'bug_id'
+);
+__PACKAGE__->many_to_many(groups => 'bug_groups', 'group');
+
+__PACKAGE__->has_one(
+  product => 'Bugzilla::Model::Result::Product',
+  {'foreign.id' => 'self.product_id'}
+);
+
+__PACKAGE__->has_one(
+  component => 'Bugzilla::Model::Result::Component',
+  {'foreign.id' => 'self.component_id'}
+);
+
+
+1;
+
diff --git a/Bugzilla/Model/Result/BugGroup.pm b/Bugzilla/Model/Result/BugGroup.pm
new file mode 100644 (file)
index 0000000..7b7b044
--- /dev/null
@@ -0,0 +1,19 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Model::Result::BugGroup;
+use Mojo::Base 'DBIx::Class::Core';
+
+__PACKAGE__->table('bug_group_map');
+__PACKAGE__->add_columns('bug_id', 'group_id');
+__PACKAGE__->set_primary_key('bug_id', 'group_id');
+
+__PACKAGE__->belongs_to(bug => 'Bugzilla::Model::Result::Bug', 'bug_id');
+__PACKAGE__->belongs_to(group => 'Bugzilla::Model::Result::Group', 'group_id');
+
+
+1;
diff --git a/Bugzilla/Model/Result/BugKeyword.pm b/Bugzilla/Model/Result/BugKeyword.pm
new file mode 100644 (file)
index 0000000..2243a9e
--- /dev/null
@@ -0,0 +1,18 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Model::Result::BugKeyword;
+use Mojo::Base 'DBIx::Class::Core';
+
+__PACKAGE__->table('keywords');
+__PACKAGE__->add_columns('bug_id', 'keywordid');
+__PACKAGE__->set_primary_key('bug_id', 'keywordid');
+
+__PACKAGE__->belongs_to(bug     => 'Bugzilla::Model::Result::Bug',     'bug_id');
+__PACKAGE__->belongs_to(keyword => 'Bugzilla::Model::Result::Keyword', 'keywordid');
+
+1;
diff --git a/Bugzilla/Model/Result/Component.pm b/Bugzilla/Model/Result/Component.pm
new file mode 100644 (file)
index 0000000..5c09877
--- /dev/null
@@ -0,0 +1,17 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Model::Result::Component;
+use Mojo::Base 'DBIx::Class::Core';
+
+__PACKAGE__->table(Bugzilla::Component->DB_TABLE);
+__PACKAGE__->add_columns(Bugzilla::Component->DB_COLUMN_NAMES);
+__PACKAGE__->set_primary_key(Bugzilla::Component->ID_FIELD);
+
+__PACKAGE__->belongs_to(product => 'Bugzilla::Model::Result::Product', 'product_id');
+
+1;
diff --git a/Bugzilla/Model/Result/Flag.pm b/Bugzilla/Model/Result/Flag.pm
new file mode 100644 (file)
index 0000000..5bb36f0
--- /dev/null
@@ -0,0 +1,28 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Model::Result::Flag;
+use Mojo::Base 'DBIx::Class::Core';
+
+__PACKAGE__->table(Bugzilla::Flag->DB_TABLE);
+__PACKAGE__->add_columns(Bugzilla::Flag->DB_COLUMN_NAMES);
+__PACKAGE__->set_primary_key(Bugzilla::Flag->ID_FIELD);
+
+__PACKAGE__->belongs_to(bug => 'Bugzilla::Model::Result::Bug', 'bug_id');
+__PACKAGE__->belongs_to(type => 'Bugzilla::Model::Result::FlagType', 'type_id');
+__PACKAGE__->has_one(
+  setter => 'Bugzilla::Model::Result::User',
+  {'foreign.userid' => 'self.setter_id'}
+);
+
+__PACKAGE__->might_have(
+  requestee => 'Bugzilla::Model::Result::User',
+  {'foreign.userid' => 'self.requestee_id'}
+);
+
+
+1;
diff --git a/Bugzilla/Model/Result/FlagType.pm b/Bugzilla/Model/Result/FlagType.pm
new file mode 100644 (file)
index 0000000..8e6cdd4
--- /dev/null
@@ -0,0 +1,18 @@
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Model::Result::FlagType;
+use Mojo::Base 'DBIx::Class::Core';
+
+__PACKAGE__->table(Bugzilla::FlagType->DB_TABLE);
+__PACKAGE__->add_columns(Bugzilla::FlagType->DB_COLUMN_NAMES);
+__PACKAGE__->set_primary_key(Bugzilla::FlagType->ID_FIELD);
+
+__PACKAGE__->has_many(flags => 'Bugzilla::Model::Result::Flag', 'type_id');
+
+1;
diff --git a/Bugzilla/Model/Result/Group.pm b/Bugzilla/Model/Result/Group.pm
new file mode 100644 (file)
index 0000000..39268cc
--- /dev/null
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Model::Result::Group;
+use Mojo::Base 'DBIx::Class::Core';
+
+__PACKAGE__->table(Bugzilla::Group->DB_TABLE);
+__PACKAGE__->add_columns(Bugzilla::Group->DB_COLUMN_NAMES);
+__PACKAGE__->set_primary_key(Bugzilla::Group->ID_FIELD);
+
+1;
diff --git a/Bugzilla/Model/Result/Keyword.pm b/Bugzilla/Model/Result/Keyword.pm
new file mode 100644 (file)
index 0000000..e38a97e
--- /dev/null
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Model::Result::Keyword;
+use Mojo::Base 'DBIx::Class::Core';
+
+__PACKAGE__->table(Bugzilla::Keyword->DB_TABLE);
+__PACKAGE__->add_columns(Bugzilla::Keyword->DB_COLUMN_NAMES);
+__PACKAGE__->set_primary_key(Bugzilla::Keyword->ID_FIELD);
+
+1;
diff --git a/Bugzilla/Model/Result/Product.pm b/Bugzilla/Model/Result/Product.pm
new file mode 100644 (file)
index 0000000..2539c68
--- /dev/null
@@ -0,0 +1,17 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Model::Result::Product;
+use Mojo::Base 'DBIx::Class::Core';
+
+__PACKAGE__->table(Bugzilla::Product->DB_TABLE);
+__PACKAGE__->add_columns(Bugzilla::Product->DB_COLUMN_NAMES);
+__PACKAGE__->set_primary_key(Bugzilla::Product->ID_FIELD);
+
+__PACKAGE__->has_many('components', 'Bugzilla::Model::Result::Component', 'product_id');
+
+1;
diff --git a/Bugzilla/Model/Result/User.pm b/Bugzilla/Model/Result/User.pm
new file mode 100644 (file)
index 0000000..d127c6b
--- /dev/null
@@ -0,0 +1,25 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Model::Result::User;
+use Mojo::Base 'DBIx::Class::Core';
+
+__PACKAGE__->table(Bugzilla::User->DB_TABLE);
+__PACKAGE__->add_columns(Bugzilla::User->DB_COLUMN_NAMES);
+__PACKAGE__->set_primary_key(Bugzilla::User->ID_FIELD);
+
+sub name {
+  my ($self) = @_;
+  $self->realname;
+}
+
+sub login {
+  my ($self) = @_;
+  $self->login_name;
+}
+
+1;
diff --git a/Bugzilla/Model/ResultSet.pm b/Bugzilla/Model/ResultSet.pm
new file mode 100644 (file)
index 0000000..2c84274
--- /dev/null
@@ -0,0 +1,13 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Model::ResultSet;
+use Mojo::Base 'DBIx::Class::ResultSet';
+
+__PACKAGE__->load_components('Helper::ResultSet');
+
+1;
index bd108495a0341f48b3d4fb0bb4b1e1a6bb79a7eb..f3059ad489b25a4c12657040ee445fd3fa23fa9b 100644 (file)
@@ -883,6 +883,22 @@ sub _insert_dep_field {
 # hooks to only run once per request instead of multiple times on each
 # page.
 
+sub DB_COLUMN_NAMES {
+  my ($class) = @_;
+  my $table = $class->DB_TABLE;
+  my $_error = sub { die "$class: cannot determine attribute name from $_[0]\n" };
+
+  my $column_re = qr{
+      ^(?:\Q$table.\E)?(?<name>\w+)$
+    | ^(?:\Q$table.\E)?(?<name>\w+)\s+AS\s+\w+$
+    | (?<name>\w+)\b.+AS\s+\g{name}$
+  }six;
+
+  return map { trim($_) =~ $column_re ? $+{name} : $_error->($_) }
+    $class->_get_db_columns;
+}
+
+
 sub _get_db_columns {
   my $invocant  = shift;
   my $class     = ref($invocant) || $invocant;
diff --git a/t/model.t b/t/model.t
new file mode 100644 (file)
index 0000000..551fb4c
--- /dev/null
+++ b/t/model.t
@@ -0,0 +1,54 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+use lib qw( . lib local/lib/perl5 );
+
+BEGIN {
+  unlink('data/db/model_test') if -f 'data/db/model_test';
+  $ENV{test_db_name} = 'model_test';
+}
+
+use Bugzilla::Test::MockDB;
+use Bugzilla::Test::MockParams (password_complexity => 'no_constraints');
+use Bugzilla::Test::Util qw(create_bug create_user);
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Hook;
+BEGIN { Bugzilla->extensions }
+use Test2::V0;
+use Test2::Tools::Mock qw(mock mock_accessor);
+use Test2::Tools::Exception qw(dies lives);
+use PerlX::Maybe qw(provided);
+
+Bugzilla->dbh->model->resultset('Keyword')
+  ->create({name => 'regression', description => 'the regression keyword'});
+
+my $user = create_user('reportuser@invalid.tld', '*');
+$user->{groups} = [Bugzilla::Group->get_all];
+Bugzilla->set_user($user);
+
+create_bug(
+  short_desc  => "test bug $_",
+  comment     => "Hello, world: $_",
+  provided $_ % 3 == 0, keywords => ['regression'],
+  assigned_to => 'reportuser@invalid.tld'
+) for (1..10);
+
+my $model = Bugzilla->dbh->model;
+my $bug3 = $model->resultset('Bug')->find(3);
+isa_ok($bug3, 'Bugzilla::Model::Result::Bug');
+
+is([map { $_->name } $bug3->keywords->all], ['regression']);
+
+is($bug3->reporter->login_name, Bugzilla::Bug->new($bug3->id)->reporter->login);
+
+
+done_testing;
+