← Back to team overview

maria-developers team mailing list archive

Patch: CREATE VIEW test program

 

Hi List.


Below is tests/view.pl for testing CREATE VIEW options.

It tests several thousand permutations and was used to find MDEV-6709

Thanks, James Briggs.
-- 
Available for MySQL/NoSQL DBA/Programming in San Jose area or remote.

tests/view.pl:

#!/usr/bin/perl

# Program: view.pl
# Licence: GPL version 2
# Author: James Briggs
# Date: 2014 09 07
# Env: Perl 5
# Note: requires CREATE [[NO] FORCE] option

#
# Testing of views.
#

use strict;
use warnings;

use DBI;
use Getopt::Long;

$|=1;

use vars qw($opt_help $opt_Information $opt_force $opt_debug
        $opt_verbose $opt_root_user $opt_root_password $opt_user $opt_password
   
     $opt_database $opt_host $opt_silent);

   $opt_help = $opt_Information = $opt_force = $opt_debug = $opt_verbose = $opt_silent = 0;
   $opt_host = "localhost",
   $opt_root_user = "root";
   $opt_root_password = "";
   $opt_user = "view_user";
   $opt_password = "view_user";
   $opt_database = "view_test";
   $opt_force = 1;

   my $version = "1.0";
   my $opt_table="table1";
   my $opt_user2  =  $opt_user . '2';

  

 
GetOptions("Information","help","server=s","root-user=s","root-password=s","user","password=s","database=s","force","host=s","debug","verbose","silent")
 || usage();

   usage() if ($opt_help || $opt_Information);

# magic constants

   use constant N_TEST_RECORDS => 3;

   use constant HAS_VIEW_GRANTS => 0;
   use constant NO_VIEW_GRANTS  => 1;

   my @cols = qw[id col1 col2 col3 col4];
   
   my $tmp_table="/tmp/mysql-view.test";
   unlink($tmp_table);

# %roles array offets

  
 use constant U_DBH        => 0;
   use constant U_PRIV_LEVEL => 1;
   use constant U_USER       => 2;
   use constant U_HOST       => 3;
   use constant U_PASSWORD   => 4;

# run tests with multiple user accounts from SUPER to very basic privs
   my %roles = (
#              [ U_DBH, U_PRIV_LEVEL,    U_USER,      U_HOST, U_PASSWORD ]

      root  => [ undef, HAS_VIEW_GRANTS, $opt_root_user, $opt_host, $opt_root_password ],
      power => [ undef, HAS_VIEW_GRANTS, $opt_user,      $opt_host, $opt_password ],
     
 crud  => [ undef, NO_VIEW_GRANTS,  $opt_user2,     $opt_host, 
$opt_password], # use the non-root user account again, this time with 
less privs
   );

   if (!$opt_force) {
      print_info();
   }

#
# setup test database
#

  
 my $dbh = user_connect($opt_root_user,$opt_root_password, 0, 'test'); #
 $opt_database may not exist yet, so connect with 'test'
   $roles{'root'}->[U_DBH] = $dbh;

   test_query('root', "drop database if exists $opt_database"); # drop database to quickly drop any tables and views
   test_query('root', "create database $opt_database");
   test_query('root', "use $opt_database");

#
# setup test table
#

   test_query('root', "create table $opt_table (id int primary key auto_increment, col1 int, col2 int, col3 int, col4 int)");

   my @c = @cols; # column names for test database
   shift @c; # remove first column (id)
   my $cols = join ',', @c; # squash array into a string

   for my $i (1..N_TEST_RECORDS) {
      test_query('root', "insert into $opt_table ($cols) values (2, 3, 4, 5)");
   }

  
 test_query('root', "grant select, insert, update, delete, create, drop,
 create view, show view on $opt_database.* to '$opt_user'\@'$opt_host' 
identified by '$opt_password'");
   my $dbh_power = user_connect($opt_user, $opt_password, 0);

  
 test_query('root', "grant select, insert, update, delete on 
$opt_database.* to '$opt_user2'\@'$opt_host' identified by 
'$opt_password'");
   my $dbh_crud = user_connect($opt_user2, $opt_password, 0);

   $roles{'power'}->[0] = $dbh_power;
   $roles{'crud'}->[0]  = $dbh_crud;

#
# test views
#

#  query array offsets

   use constant Q_QRY     => 0;
   use constant Q_HI_PRIV => 1;
   use constant Q_LO_PRIV => 2;
   use constant Q_OUTPUT  => 3;
   use constant Q_COMMENT => 4;

   my @t0 = (
#     [ query,                                   ignore_failure_hi_priv, ignore_failure_lo_priv, result, comment ]
#     [
 Q_QRY,                                                Q_HI_PRIV, Q_LO_PRIV, Q_OUTPUT, Q_COMMENT ]

      [ "create view $opt_table as select * from $opt_table",         1, 1, undef, "should fail - duplicate object name" ],
      [ "create view view1 as select * from ${opt_table}2",           1, 1, undef, "should fail - no base table found" ],
      [ "create view view1 as select * from $opt_table",              0,
 1, undef, "" ],
      [ "select count(*) from view1",                                 0, 1, N_TEST_RECORDS, "" ],
      [ "create definer = current_user() sql security invoker view v1 as select 1", 0, 1, undef, "" ],
      [ "drop view view1",                                            0, 1, undef, "" ],
      [ "drop view
 v1",                                               0, 1, undef, "" ],
   );

   test_driver('original create view commands', \%roles, \@t0);

#
# test new CREATE NO FORCE VIEW view options in 10.1.x
#

# CREATE FORCE VIEW should work the same as default (omitted)

   my @t1 = (
      [ "create no force view $opt_table as select * from $opt_table", 1, 1, undef, "should fail - duplicate object name" ],
      [ "create no force view view1 as select * from $opt_table",      0, 1, undef, "" ],
      [ "create no force view view1 as select * from $opt_table",      1, 1, undef, "should fail - duplicate object name" ],
      [ "select count(*) from view1",                                  0, 1, N_TEST_RECORDS, "" ],
      [ "drop view
 view1",                                             0, 1, undef, "" ],
   );

   test_driver('new CREATE NO FORCE VIEW options in 10.1.x', \%roles, \@t1);

#
# test new CREATE FORCE VIEW options in 10.1.x
#

# Similar to Oracle Enterprise, behavior of CREATE FORCE VIEW:
#
# - no base table needs exist at creation time
# - thus no table or column access rights need exist 
#
# - however, CREATE VIEW and SHOW VIEW should be
 enforced

   my @t2 = (
     
 [ "create force view $opt_table as select id, col1 from $opt_table",   
 1, 1, undef, "should fail - duplicate object name" ],
      [ "create force view view1 as select id, col1 from $opt_table",         0, 1, undef, "" ],
     
 [ "create force view view1 as select id, col1 from $opt_table",        
 1, 1, undef, "should fail - duplicate object name" ],
      [ "create force view view2 as select id, col1 from ${opt_table}2",      0, 1, undef, "" ],
# failed on "CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `view1` AS
 select `*` AS `*` from `table1`" # note two AS symbols
  #   [ "select count(*) from view1",                                 0, 1, N_TEST_RECORDS, "" ],

      [ "drop view view1, view2",                                             0, 1, undef, "" ],
   );

   test_driver('new CREATE VIEW FORCE view options in 10.1.x', \%roles, \@t2);

# test views by
 automatically generating thousands of permutation of CREATE VIEW

   gen_permutations();

#
# Clean up
#

   unlink($tmp_table);

   test_query('root', "revoke all privileges, grant option from '$opt_user'\@'%'");
   test_query('root', "revoke all privileges, grant option from '$opt_user2'\@'%'");
   test_query('root', "drop database if exists $opt_database");

   print "end of test\n";

   exit 0;

#
# do permuted tests
#
# Todo:
#
# - derived tables
# - I_S tables
# - views of views
# - invalid syntax

sub gen_permutations {

# CREATE
#   [OR REPLACE]
#   [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}]
#   [DEFINER = { user | CURRENT_USER }]
#   [SQL SECURITY { DEFINER | INVOKER }]
#   [[NO] FORCE]
#   VIEW view_name [(column_list)]
#   AS select_statement
#   [WITH [CASCADED | LOCAL] CHECK OPTION]

   use constant P_CREATE        => 0;
   use
 constant P_ALGORITHM     => 1;
   use constant P_DEFINER       => 2;
   use constant P_SQL_SECURITY  => 3;
   use constant P_FORCE         => 4;
   use constant P_WITH          => 5;

# insert a blank string to make the option skippable

   my @p = (
      [ 'create', 'create or replace'],
      [ '', 'undefined', 'merge', 'temptable' ],
      [ '', 'user', 'current_user'],
      [ '', 'definer', 'invoker'
 ],
      [ '', 'force', 'no force'],
      [ '', 'cascaded', 'local'],
   );

   my $permutations = 0;
   my $sql = '';
   my $quiet = 1;

   print "Trying create view permutations\n\n";
   
   for my $role (sort { $b cmp $a } keys %roles) {
       for my $create (@{$p[P_CREATE]}) {
           for my $algorithm (@{$p[P_ALGORITHM]}) {
               for my $definer (@{$p[P_DEFINER]}) {
                   for my $sql_security (@{$p[P_SQL_SECURITY]}) {
                       for my $force (@{$p[P_FORCE]}) {
                           for my $col_list (('', $cols)) {
                               for my $columns (('*', $cols)) {
                                   for my $with (@{$p[P_WITH]}) {

                                       next if ($columns eq '*' && $force eq 'force'); # ambiguous view definition

                                       $sql = $create . ' ';
                                       $sql .= "ALGORITHM = $algorithm " if $algorithm ne '';

                                       if ($definer ne '') {
                                          if ($definer eq 'user') {
                                             $sql .= "DEFINER = '$roles{$role}->[U_USER]'\@'$roles{$role}->[U_HOST]' ";
                                          }
                                          else {
                                             $sql .= "DEFINER = $definer ";
                                          }
                                       }
    
                                       $sql .= "$force " if $force ne '';
                                       $sql .= "VIEW ";
                                       $sql .= "view1 ";
                                      
 $sql .= "($col_list) " if $col_list ne '' and $col_list eq $columns; # 
if you use $col_list, the number of columns must match $columns
                                       $sql .= "AS SELECT $columns FROM $opt_table ";
                                       $sql .= "WITH $with CHECK OPTION" if $with ne
 '';
    
                                       print "$permutations:$role: $sql\n";
         
                                       my $will_error = 0;

                                       $will_error = 1 if
                                          ($role eq 'crud')                              # no grants
                                          || ($algorithm eq 'temptable' and $with ne '') # always an error to CHECK a TEMPTABLE
#                                         || ($definer eq 'user' and $role ne 'root')    # super priv needed for DEFINER user
                                       ;
         
                                       test_query('root', 'drop view if exists
 view1', 0, $quiet);
                                       test_query($role, $sql, $will_error, $quiet);
                                       test_query($role, 'select count(*) from view1', $will_error, $quiet);

                                       if (!$will_error &&
 $roles{$role}->[U_PRIV_LEVEL] == HAS_VIEW_GRANTS) {
                                         
 if (db_cmp_count($roles{'root'}->[U_DBH], 'select count(*) from 
view1', N_TEST_RECORDS )) {
                                             die "error: wrong row count for '$sql'";
                                          }
                                       }

                                       $permutations++;
                                  }
                             }
                         }
                      }
                   }
               }
           }
       }
   }

   print "total permutations = $permutations\n";

   return 0;
}

sub test_driver {
   my ($heading, $r_roles, $r_qry) = @_;

   print $heading, "\n\n";

   for my $role (sort { $b cmp $a } keys %$r_roles) {
      test_query('root', "drop view if exists view1");

      for my $q (@$r_qry) {
         my
 $flag_fail = ($r_roles->{$role}->[U_PRIV_LEVEL] == HAS_VIEW_GRANTS) ? $q->[Q_HI_PRIV] : $q->[Q_LO_PRIV];
         print "$role '$q->[Q_QRY]'", ($flag_fail ? ' should fail' : ' should pass'), "\n";
         test_query($role, $q->[Q_QRY], $flag_fail, 0);

         if (defined $q->[Q_OUTPUT]) {
            my $ret = db_cmp_count($r_roles->{'root'}->[U_DBH], $q->[Q_QRY], ($flag_fail ? undef : $q->[Q_OUTPUT]));
            die if $ret;
         }
      }
   }

   print "\n\n";
}
   
sub usage {
   print <<EOF;
$0  Ver $version

This program tests that the VIEW commands works by creating a temporary
database ($opt_database) and users ($opt_user, $opt_user2).

Options:

--database (Default $opt_database)
  In which database the test tables are created.

--force
  Don''t ask any question before starting this test.

--host='host name' (Default $opt_host)
  Host name where the database server is
 located.

--Information
--help
  Print this help

--root-password
  Password for root-user.

--user  (Default $opt_user)
  A non-existing user on which we will test view commands

--password
  Password for non-root-user.

--verbose
  Write all queries when we are execute them.

--root-user='user name' (Default $opt_root_user)
  superuser for creating tables and grants
EOF
  exit(0);
}

sub print_info {
  my $tmp;

  print <<EOF;
This test will do view statements against the $opt_database database !
the $opt_database database and $opt_user user will be created and deleted !

EOF

  while (1) {
    print "Start test (yes/no) ? ";
    $tmp=<STDIN>; chomp($tmp); $tmp=lc($tmp);
    last if ($tmp =~ /^yes$/i);
    exit 1 if ($tmp =~ /^n/i);
    print "\n";
  }
}

sub user_connect {
  my ($user, $password, $ignore_error, $db) = @_;

  $db = $opt_database if not defined $db or $db eq '';

  print "Connecting $user\n" if ($opt_verbose);

  my $dbh =DBI->connect("DBI:mysql:$db:$opt_host",$user, $password, { PrintError => 0});
  if (!$dbh)
  {
    if ($opt_verbose || !$ignore_error)
    {
      print "error on connect: $DBI::errstr\n";
    }
    if (!$ignore_error)
    {
      die "The above should not have failed!";
    }
  }
  elsif ($ignore_error)
  {
    die "Connect succeeded when it shouldn't have !\n";
  }
  else {
    return $dbh;
  }
}

sub test_query {
  my ($role, $query, $ignore_error, $quiet) = @_;

  my ($package, $filename, $line) = caller;

  my $dbh = $roles{$role}->[U_DBH];

  if (defined $dbh && !$dbh->ping) {
     $roles{$role}->[U_DBH] = user_connect($roles{$role}->[U_USER], $roles{$role}->[U_PASSWORD], $ignore_error);
     $dbh = $roles{$role}->[U_DBH];
  }
  elsif (not defined $dbh) {
     $roles{$role}->[U_DBH] = user_connect($roles{$role}->[U_USER], $roles{$role}->[U_PASSWORD], $ignore_error);
     $dbh = $roles{$role}->[U_DBH];
  }

  if (do_query($dbh, $query, $ignore_error, $quiet)) {
    if (!defined($ignore_error))
    {
      print "error:$line: This query should not have failed: '$query', do SHOW CREATE VIEW VIEW_TEST.VIEW1 to troubleshoot.\n";
      exit 1;
    }
  }
  elsif (defined($ignore_error) && $ignore_error == 1)
  {
    print "error:$line: This query should not have succeeded: '$query'\n";
    exit 1;
  }
}

sub do_query {
  my ($my_dbh, $query, $ignore_error, $quiet) = @_;

  my ($sth, $row, $fatal_error);

  print "$query\n" if ($opt_debug || $opt_verbose);
  if (!($sth= $my_dbh->prepare($query)))
  {
    print "error in prepare: $DBI::errstr\n";
    return 1;
  }
  if (!$sth->execute)
  {
    $fatal_error= ($DBI::errstr =~ /parse error/);
    if (!$ignore_error || ($opt_verbose && $ignore_error != 3) || $fatal_error)
    {
      print "error in execute: $DBI::errstr\n";

      {
         my $cmd = "show create view $opt_database.view1";
         my $out = `mysql -h $opt_host -u root -p$opt_root_password -e '$cmd'`;
         print "$cmd: $out\n";
         print '"*** You have the SQL parser bug: AS select `*` AS `*` ***' . "\n" if $out =~ / AS SELECT .* AS /i;
      }
    }
    die if ($fatal_error);
    $sth->finish;
    return 1;
  }

  if (!$opt_silent and !$quiet) {
     my $found = 0;
    while (($row=$sth->fetchrow_arrayref)) {
      $found = 1;
      my $tab = '';

      for my $col (@$row) {
    print $tab;
    print defined($col) ? $col : "NULL";
    $tab="\t";
      }
      print "\n";
    }
    print "\n" if $found;
  }
  $sth->finish;

  return 0;
}

# Note: cmp_tmp_table is not currently used, but available for future use again

sub cmp_tmp_table {
   my ($s) = @_;

    if (not defined $s) {
       if (-e $tmp_table) {
          return 1;
       }
       else {
          return 0;
       }
   }

   $s =~ s/\n+$//g; # remove trailing blanks

   open X, "<", $tmp_table or return 2;
   local($/)='';
   my $t = <X>;
   close X;

   unlink($tmp_table) or warn "debug: cannot unlink tmp table";

   $t =~ s/\n+$//g;

   if ($s ne $t) {
      return 1;
   }

   return 0;
}

sub db_cmp_count {
  
 my ($dbh, $q, $s) = @_;

   my $sth = $dbh->prepare($q);

   my $ret = $sth->execute() || do {
      return 0 if not defined $s;
      return 2;
   };

   my $out = '';

   if (my (@row) = $sth->fetchrow_array()) {
       $out = $row[0];
   }

   $sth->finish;

   return 1 if $out != $s;

   return 0;
}

# The End.