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.