# Copyright (c) 2021-2025, PostgreSQL Global Development Group
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
# Check the initial state of the data generated. Tables for tellers and # branches use NULL for their filler attribute. The table accounts uses # a non-NULL filler. The history table should have no data.
sub check_data_state
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
my $node = shift;
my $type = shift;
my $sql_result = $node->safe_psql('postgres', 'SELECT count(*) AS null_count FROM pgbench_accounts WHERE filler IS NULL LIMIT 10;'
);
is($sql_result, '0', "$type: filler column of pgbench_accounts has no NULL data");
$sql_result = $node->safe_psql('postgres', 'SELECT count(*) AS null_count FROM pgbench_branches WHERE filler IS NULL;'
);
is($sql_result, '1', "$type: filler column of pgbench_branches has only NULL data");
$sql_result = $node->safe_psql('postgres', 'SELECT count(*) AS null_count FROM pgbench_tellers WHERE filler IS NULL;'
);
is($sql_result, '10', "$type: filler column of pgbench_tellers has only NULL data");
$sql_result = $node->safe_psql('postgres', 'SELECT count(*) AS data_count FROM pgbench_history;');
is($sql_result, '0', "$type: pgbench_history has no data");
}
# start a pgbench specific server
my $node = PostgreSQL::Test::Cluster->new('main'); # Set to untranslated messages, to be able to compare program output with # expected strings.
$node->init(extra => [ '--locale', 'C' ]);
$node->start;
# tablespace for testing, because partitioned tables cannot use pg_default # explicitly and we want to test that table creation with tablespace works # for partitioned tables.
my $ts = $node->basedir . '/regress_pgbench_tap_1_ts_dir';
mkdir $ts or die "cannot create directory $ts";
# the next commands will issue a syntax error if the path contains a "'"
$node->safe_psql('postgres', "CREATE TABLESPACE regress_pgbench_tap_1_ts LOCATION '$ts';");
# Test concurrent OID generation via pg_enum_oid_index. This indirectly # exercises LWLock and spinlock concurrency.
my $labels = join ',', map { "'l$_'" } 1 .. 1000;
$node->pgbench( '--no-vacuum --client=5 --protocol=prepared --transactions=25', 0,
[qr{processed: 125/125}],
[qr{^$}], 'concurrent OID generation',
{ '001_pgbench_concurrent_insert' => "CREATE TYPE pg_temp.e AS ENUM ($labels); DROP TYPE pg_temp.e;"
});
# Test inplace updates from VACUUM concurrent with heap_update from GRANT. # The PROC_IN_VACUUM environment can't finish MVCC table scans consistently, # so this fails rarely. To reproduce consistently, add a sleep after # GetCatalogSnapshot(non-catalog-rel).
Test::More->builder->todo_start('PROC_IN_VACUUM scan breakage');
$node->safe_psql('postgres', 'CREATE TABLE ddl_target ()');
$node->pgbench( '--no-vacuum --client=5 --protocol=prepared --transactions=50', 0,
[qr{processed: 250/250}],
[qr{^$}], 'concurrent GRANT/VACUUM',
{ '001_pgbench_grant@9' => q(
DO $$
BEGIN
PERFORM pg_advisory_xact_lock(42);
FOR i IN 1 .. 10 LOOP
GRANT SELECT ON ddl_target TO PUBLIC;
REVOKE SELECT ON ddl_target FROM PUBLIC;
END LOOP;
END
$$;
), '001_pgbench_vacuum_ddl_target@1' => "VACUUM ddl_target;",
});
Test::More->builder->todo_end;
# Trigger various connection errors
$node->pgbench( 'no-such-database', 1,
[qr{^$}],
[
qr{connection to server .* failed},
qr{FATAL: database "no-such-database" does not exist}
], 'no such database');
$node->pgbench( '-S -t 1', 1, [],
[qr{Perhaps you need to do initialization}], 'run without init');
# check if threads are supported
my $nthreads = 2;
{
my ($stderr);
run_log([ 'pgbench', '--jobs' => '2', '--bad-option' ], '2>' => \$stderr);
$nthreads = 1if $stderr =~ m/threads are not supported on this platform/;
}
# run custom scripts
$node->pgbench( "-t 100 -c 1 -j $nthreads -M prepared -n", 0,
[
qr{type: multiple scripts},
qr{mode: prepared},
qr{script 1: .*/001_pgbench_custom_script_1},
qr{weight: 2},
qr{script 2: .*/001_pgbench_custom_script_2},
qr{weight: 1},
qr{processed: 100/100}
],
[qr{^$}], 'pgbench custom scripts',
{ '001_pgbench_custom_script_1@1' => q{-- select only
\set aid random(1, :scale * 100000)
SELECT abalance::INTEGER AS balance
FROM pgbench_accounts
WHERE aid=:aid;
}, '001_pgbench_custom_script_2@2' => q{-- special variables
BEGIN;
\set foo 1
-- cast are needed for typing under -M prepared
SELECT :foo::INT + :scale::INT * :client_id::INT AS bla;
COMMIT;
}
});
$node->pgbench( '-n -t 10 -c 1 -M simple', 0,
[
qr{type: .*/001_pgbench_custom_script_3},
qr{processed: 10/10},
qr{mode: simple}
],
[qr{^$}], 'pgbench custom script',
{ '001_pgbench_custom_script_3' => q{-- select only variant
\set aid random(1, :scale * 100000)
BEGIN;
SELECT abalance::INTEGER AS balance
FROM pgbench_accounts
WHERE aid=:aid;
COMMIT;
}
});
$node->pgbench( '-n -t 10 -c 2 -M extended', 0,
[
qr{type: .*/001_pgbench_custom_script_4},
qr{processed: 20/20},
qr{mode: extended}
],
[qr{^$}], 'pgbench custom script',
{ '001_pgbench_custom_script_4' => q{-- select only variant
\set aid random(1, :scale * 100000)
BEGIN;
SELECT abalance::INTEGER AS balance
FROM pgbench_accounts
WHERE aid=:aid;
COMMIT;
}
});
# Verify server logging of query parameters. # (This doesn't really belong here, but pgbench is a convenient way # to issue commands using extended query mode with parameters.)
# 1. Logging neither with errors nor with statements
$node->append_conf('postgresql.conf', "log_min_duration_statement = 0\n"
. "log_parameter_max_length = 0\n"
. "log_parameter_max_length_on_error = 0");
$node->reload;
$node->pgbench( '-n -t1 -c1 -M prepared', 2,
[],
[
qr{ERROR: invalid input syntax for type json},
qr{(?!unnamed portal with parameters)}
], 'server parameter logging',
{ '001_param_1' => q[select '{ invalid ' as value \gset
select $$'Valame Dios!' dijo Sancho; 'no le dije yo a vuestra merced que mirase bien lo que hacia?'$$ as long \gset
select column1::jsonb from (values (:value), (:long)) as q;
]
});
my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile);
unlike(
$log,
qr[DETAIL: Parameters: \$1 = '\{ invalid ',], "no parameters logged");
$log = undef;
# 2. Logging truncated parameters on error, full with statements
$node->append_conf('postgresql.conf', "log_parameter_max_length = -1\n"
. "log_parameter_max_length_on_error = 64");
$node->reload;
$node->pgbench( '-n -t1 -c1 -M prepared', 2,
[],
[
qr{ERROR: division by zero},
qr{CONTEXT: unnamed portal with parameters: \$1 = '1', \$2 = NULL}
], 'server parameter logging',
{ '001_param_2' => q{select '1' as one \gset
SELECT 1 / (random() / 2)::int, :one::int, :two::int;
}
});
$node->pgbench( '-n -t1 -c1 -M prepared', 2,
[],
[
qr{ERROR: invalid input syntax for type json},
qr[CONTEXT: JSON data, line 1: \{ invalid\.\.\.[\r\n]+unnamed portal with parameters: \$1 = '\{ invalid ', \$2 = '''Valame Dios!'' dijo Sancho; ''no le dije yo a vuestra merced que \.\.\.']m
], 'server parameter logging',
{ '001_param_3' => q[select '{ invalid ' as value \gset
select $$'Valame Dios!' dijo Sancho; 'no le dije yo a vuestra merced que mirase bien lo que hacia?'$$ as long \gset
select column1::jsonb from (values (:value), (:long)) as q;
]
});
$log = PostgreSQL::Test::Utils::slurp_file($node->logfile);
like(
$log,
qr[DETAIL: Parameters: \$1 = '\{ invalid ', \$2 = '''Valame Dios!'' dijo Sancho; ''no le dije yo a vuestra merced que mirase bien lo que hacia\?'''], "parameter report does not truncate");
$log = undef;
# 3. Logging full parameters on error, truncated with statements
$node->append_conf('postgresql.conf', "log_min_duration_statement = -1\n"
. "log_parameter_max_length = 7\n"
. "log_parameter_max_length_on_error = -1");
$node->reload;
$node->pgbench( '-n -t1 -c1 -M prepared', 2,
[],
[
qr{ERROR: division by zero},
qr{CONTEXT: unnamed portal with parameters: \$1 = '1', \$2 = NULL}
], 'server parameter logging',
{ '001_param_4' => q{select '1' as one \gset
SELECT 1 / (random() / 2)::int, :one::int, :two::int;
}
});
$node->append_conf('postgresql.conf', "log_min_duration_statement = 0");
$node->reload;
$node->pgbench( '-n -t1 -c1 -M prepared', 2,
[],
[
qr{ERROR: invalid input syntax for type json},
qr[CONTEXT: JSON data, line 1: \{ invalid\.\.\.[\r\n]+unnamed portal with parameters: \$1 = '\{ invalid ', \$2 = '''Valame Dios!'' dijo Sancho; ''no le dije yo a vuestra merced que mirase bien lo que hacia\?']m
], 'server parameter logging',
{ '001_param_5' => q[select '{ invalid ' as value \gset
select $$'Valame Dios!' dijo Sancho; 'no le dije yo a vuestra merced que mirase bien lo que hacia?'$$ as long \gset
select column1::jsonb from (values (:value), (:long)) as q;
]
});
$log = PostgreSQL::Test::Utils::slurp_file($node->logfile);
like(
$log,
qr[DETAIL: Parameters: \$1 = '\{ inval\.\.\.', \$2 = '''Valame\.\.\.'], "parameter report truncates");
$log = undef;
# Check that bad parameters are reported during typinput phase of BIND
$node->pgbench( '-n -t1 -c1 -M prepared', 2,
[],
[
qr{ERROR: invalid input syntax for type smallint: "1a"},
qr{CONTEXT: unnamed portal parameter \$2 = '1a'}
], 'server parameter logging',
{ '001_param_6' => q{select 42 as value1, '1a' as value2 \gset
select :value1::smallint, :value2::smallint;
}
});
# random determinism when seeded
$node->safe_psql('postgres', 'CREATE UNLOGGED TABLE seeded_random(seed INT8 NOT NULL, rand TEXT NOT NULL, val INTEGER NOT NULL);'
);
# same value to check for determinism
my $seed = int(rand(1000000000)); for my $i (1, 2)
{
$node->pgbench( "--random-seed=$seed -t 1", 0,
[qr{processed: 1/1}],
[qr{setting random seed to $seed\b}], "random seeded with $seed",
{ "001_pgbench_random_seed_$i" => q{-- test random functions
\set ur random(1000, 1999)
\set er random_exponential(2000, 2999, 2.0)
\set gr random_gaussian(3000, 3999, 3.0)
\set zr random_zipfian(4000, 4999, 1.5)
INSERT INTO seeded_random(seed, rand, val) VALUES
(:random_seed, 'uniform', :ur),
(:random_seed, 'exponential', :er),
(:random_seed, 'gaussian', :gr),
(:random_seed, 'zipfian', :zr);
}
});
}
# check that all runs generated the same 4 values
my ($ret, $out, $err) = $node->psql('postgres', 'SELECT seed, rand, val, COUNT(*) FROM seeded_random GROUP BY seed, rand, val'
);
# backslash commands
$node->pgbench( '-t 1', 0,
[
qr{type: .*/001_pgbench_backslash_commands},
qr{processed: 1/1},
qr{shell-echo-output}
],
[qr{command=8.: int 1\b}], 'pgbench backslash commands',
{ '001_pgbench_backslash_commands' => q{-- run set
\set zero 0
\set one 1.0
-- sleep
\sleep :one ms
\sleep 100 us
\sleep 0 s
\sleep :zero
-- setshell and continuation
\setshell another_one\ echo \
:one
\set n debug(:another_one)
-- shell
\shell echo shell-echo-output
}
});
# working \gset
$node->pgbench( '-t 1', 0,
[ qr{type: .*/001_pgbench_gset}, qr{processed: 1/1} ],
[
qr{command=3.: int 0\b},
qr{command=5.: int 1\b},
qr{command=6.: int 2\b},
qr{command=8.: int 3\b},
qr{command=10.: int 4\b},
qr{command=12.: int 5\b}
], 'pgbench gset command',
{ '001_pgbench_gset' => q{-- test gset
-- no columns
SELECT \gset
-- one value
SELECT 0 AS i0 \gset
\set i debug(:i0)
-- two values
SELECT 1 AS i1, 2 AS i2 \gset
\set i debug(:i1)
\set i debug(:i2)
-- with prefix
SELECT 3 AS i3 \gset x_
\set i debug(:x_i3)
-- overwrite existing variable
SELECT 0 AS i4, 4 AS i4 \gset
\set i debug(:i4)
-- work on the last SQL command under \;
\; \; SELECT 0 AS i5 \; SELECT 5 AS i5 \; \; \gset
\set i debug(:i5)
}
}); # \gset cannot accept more than one row, causing command to fail.
$node->pgbench( '-t 1', 2,
[ qr{type: .*/001_pgbench_gset_two_rows}, qr{processed: 0/1} ],
[qr{expected one row, got 2\b}], 'pgbench gset command with two rows',
{ '001_pgbench_gset_two_rows' => q{
SELECT 5432 AS fail UNION SELECT 5433 ORDER BY 1 \gset
}
});
# working \aset # Valid cases.
$node->pgbench( '-t 1', 0,
[ qr{type: .*/001_pgbench_aset}, qr{processed: 1/1} ],
[ qr{command=3.: int 8\b}, qr{command=4.: int 7\b} ], 'pgbench aset command',
{ '001_pgbench_aset' => q{
-- test aset, which applies to a combined query
\; SELECT 6 AS i6 \; SELECT 7 AS i7 \; \aset
-- unless it returns more than one row, last is kept
SELECT 8 AS i6 UNION SELECT 9 ORDER BY 1 DESC \aset
\set i debug(:i6)
\set i debug(:i7)
}
}); # Empty result set with \aset, causing command to fail.
$node->pgbench( '-t 1', 2,
[ qr{type: .*/001_pgbench_aset_empty}, qr{processed: 0/1} ],
[
qr{undefined variable \"i8\"},
qr{evaluation of meta-command failed\b}
], 'pgbench aset command with empty result',
{ '001_pgbench_aset_empty' => q{
-- empty result
\; SELECT 5432 AS i8 WHERE FALSE \; \aset
\set i debug(:i8)
}
});
# Try to end a pipeline that hasn't started
$node->pgbench( '-t 1 -n -M extended', 2,
[],
[qr{not in pipeline mode}], 'error: \endpipeline with no start',
{ '001_pgbench_pipeline_3' => q{
-- pipeline not started
\endpipeline
}
});
# Try \gset in pipeline mode
$node->pgbench( '-t 1 -n -M extended', 2,
[],
[qr{gset is not allowed in pipeline mode}], 'error: \gset not allowed in pipeline mode',
{ '001_pgbench_pipeline_4' => q{
\startpipeline
select 1 \gset f
\endpipeline
}
});
# Try \startpipeline without \endpipeline in a single transaction
$node->pgbench( '-t 1 -n -M extended', 2,
[],
[qr{end of script reached with pipeline open}], 'error: call \startpipeline without \endpipeline in a single transaction',
{ '001_pgbench_pipeline_5' => q{
-- startpipeline only with single transaction
\startpipeline
}
});
# Try \startpipeline without \endpipeline
$node->pgbench( '-t 2 -n -M extended', 2,
[],
[qr{end of script reached with pipeline open}], 'error: call \startpipeline without \endpipeline',
{ '001_pgbench_pipeline_6' => q{
-- startpipeline only
\startpipeline
}
});
# Try \startpipeline with \syncpipeline without \endpipeline
$node->pgbench( '-t 2 -n -M extended', 2,
[],
[qr{end of script reached with pipeline open}], 'error: call \startpipeline and \syncpipeline without \endpipeline',
{ '001_pgbench_pipeline_7' => q{
-- startpipeline with \syncpipeline only
\startpipeline
\syncpipeline
}
});
# Try SET LOCAL as first pipeline command. This succeeds and the first # command is not executed inside an implicit transaction block, causing # a WARNING.
$node->pgbench( '-t 1 -n -M extended', 0,
[],
[qr{WARNING: SET LOCAL can only be used in transaction blocks}], 'SET LOCAL outside implicit transaction block of pipeline',
{ '001_pgbench_pipeline_set_local_1' => q{
\startpipeline
SET LOCAL statement_timeout='1h';
\endpipeline
}
});
# Try SET LOCAL as second pipeline command. This succeeds and the second # command does not cause a WARNING to be generated.
$node->pgbench( '-t 1 -n -M extended', 0,
[],
[qr{^$}], 'SET LOCAL inside implicit transaction block of pipeline',
{ '001_pgbench_pipeline_set_local_2' => q{
\startpipeline
SELECT 1;
SET LOCAL statement_timeout='1h';
\endpipeline
}
});
# Try SET LOCAL with \syncpipeline. This succeeds and the command # launched after the sync is outside the implicit transaction block # of the pipeline, causing a WARNING.
$node->pgbench( '-t 1 -n -M extended', 0,
[],
[qr{WARNING: SET LOCAL can only be used in transaction blocks}], 'SET LOCAL and \syncpipeline',
{ '001_pgbench_pipeline_set_local_3' => q{
\startpipeline
SELECT 1;
\syncpipeline
SET LOCAL statement_timeout='1h';
\endpipeline
}
});
# Try REINDEX CONCURRENTLY as first pipeline command. This succeeds # as the first command is outside the implicit transaction block of # a pipeline.
$node->pgbench( '-t 1 -n -M extended', 0,
[],
[], 'REINDEX CONCURRENTLY outside implicit transaction block of pipeline',
{ '001_pgbench_pipeline_reindex_1' => q{
\startpipeline
REINDEX TABLE CONCURRENTLY pgbench_accounts;
SELECT 1;
\endpipeline
}
});
# Try REINDEX CONCURRENTLY as second pipeline command. This fails # as the second command is inside an implicit transaction block.
$node->pgbench( '-t 1 -n -M extended', 2,
[],
[], 'error: REINDEX CONCURRENTLY inside implicit transaction block of pipeline',
{ '001_pgbench_pipeline_reindex_2' => q{
\startpipeline
SELECT 1;
REINDEX TABLE CONCURRENTLY pgbench_accounts;
\endpipeline
}
});
# Try VACUUM as first pipeline command. Like REINDEX CONCURRENTLY, this # succeeds as this is outside the implicit transaction block of a pipeline.
$node->pgbench( '-t 1 -n -M extended', 0,
[],
[], 'VACUUM outside implicit transaction block of pipeline',
{ '001_pgbench_pipeline_vacuum_1' => q{
\startpipeline
VACUUM pgbench_accounts;
\endpipeline
}
});
# Try VACUUM as second pipeline command. This fails, as the second command # of a pipeline is inside an implicit transaction block.
$node->pgbench( '-t 1 -n -M extended', 2,
[],
[], 'error: VACUUM inside implicit transaction block of pipeline',
{ '001_pgbench_pipeline_vacuum_2' => q{
\startpipeline
SELECT 1;
VACUUM pgbench_accounts;
\endpipeline
}
});
# Try subtransactions in a pipeline. These are forbidden in implicit # transaction blocks.
$node->pgbench( '-t 1 -n -M extended', 2,
[],
[], 'error: subtransactions not allowed in pipeline',
{ '001_pgbench_pipeline_subtrans' => q{
\startpipeline
SAVEPOINT a;
SELECT 1;
ROLLBACK TO SAVEPOINT a;
SELECT 2;
\endpipeline
}
});
# Try LOCK TABLE as first pipeline command. This fails as LOCK is outside # an implicit transaction block.
$node->pgbench( '-t 1 -n -M extended', 2,
[],
[], 'error: LOCK TABLE outside implicit transaction block of pipeline',
{ '001_pgbench_pipeline_lock_1' => q{
\startpipeline
LOCK pgbench_accounts;
SELECT 1;
\endpipeline
}
});
# Try LOCK TABLE as second pipeline command. This succeeds as LOCK is inside # an implicit transaction block.
$node->pgbench( '-t 1 -n -M extended', 0,
[],
[], 'LOCK TABLE inside implicit transaction block of pipeline',
{ '001_pgbench_pipeline_lock_2' => q{
\startpipeline
SELECT 1;
LOCK pgbench_accounts;
\endpipeline
}
});
# Working \startpipeline in prepared query mode with serializable
$node->pgbench( '-c4 -t 10 -n -M prepared', 0,
[
qr{type: .*/001_pgbench_pipeline_serializable},
qr{actually processed: (\d+)/\1}
],
[], 'working \startpipeline with serializable',
{ '001_pgbench_pipeline_serializable' => q{
-- test startpipeline with serializable
\startpipeline
BEGIN ISOLATION LEVEL SERIALIZABLE;
} . "select 1;\n" x 10 . q{
END;
\endpipeline
}
});
# trigger many expression errors
my @errors = (
# [ test name, expected status, expected stderr, script ] # SQL
[ 'sql syntax error', 2,
[
qr{ERROR: syntax error},
qr{prepared statement .* does not exist}
],
q{-- SQL syntax error
SELECT 1 + ;
}
],
[ 'sql too many args', 1,
[qr{statement has too many arguments.*\b255\b}],
q{-- MAX_ARGS=256for prepared
\set i 0
SELECT LEAST(} . join(', ', (':i') x 256) . q{)}
],
# SHELL
[ 'shell bad command', 2,
[qr{\(shell\) .* meta-command failed}], q{\shell no-such-command}
],
[ 'shell undefined variable', 2,
[qr{undefined variable ":nosuchvariable"}],
q{-- undefined variable in shell
\shell echo ::foo :nosuchvariable
}
],
[ 'shell missing command', 1, [qr{missing command }], q{\shell} ],
[ 'shell too many args', 1, [qr{too many arguments in command "shell"}],
q{-- 256 arguments to \shell
\shell echo } . join(' ', ('arg') x 255)
],
# SET
[ 'set syntax error', 1,
[qr{syntax error in command "set"}], q{\set i 1 +}
],
[ 'set no such function', 1,
[qr{unexpected function name}], q{\set i noSuchFunction()}
],
[ 'set invalid variable name', 2,
[qr{invalid variable name}], q{\set . 1}
],
[ 'set division by zero', 2, [qr{division by zero}], q{\set i 1/0} ],
[ 'set undefined variable', 2,
[qr{undefined variable "nosuchvariable"}],
q{\set i :nosuchvariable}
],
[ 'set unexpected char', 1, [qr{unexpected character .;.}], q{\set i ;} ],
[ 'set too many args', 2,
[qr{too many function arguments}],
q{\set i least(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16)}
],
[ 'set empty random range', 2,
[qr{empty range given to random}], q{\set i random(5,3)}
],
[ 'set random range too large', 2,
[qr{random range is too large}], q{\set i random(:minint, :maxint)}
],
[ 'set gaussian param too small', 2,
[qr{gaussian param.* at least 2}],
q{\set i random_gaussian(0, 10, 1.0)}
],
[ 'set exponential param greater 0', 2,
[qr{exponential parameter must be greater }],
q{\set i random_exponential(0, 10, 0.0)}
],
[ 'set zipfian param to 1', 2,
[qr{zipfian parameter must be in range \[1\.001, 1000\]}],
q{\set i random_zipfian(0, 10, 1)}
],
[ 'set zipfian param too large', 2,
[qr{zipfian parameter must be in range \[1\.001, 1000\]}],
q{\set i random_zipfian(0, 10, 1000000)}
],
[ 'set non numeric value', 2,
[qr{malformed variable "foo" value: "bla"}], q{\set i :foo + 1}
],
[ 'set no expression', 1, [qr{syntax error}], q{\set i} ],
[ 'set missing argument', 1, [qr{missing argument}i], q{\set} ],
[ 'set not a bool', 2,
[qr{cannot coerce double to boolean}], q{\set b NOT 0.0}
],
[ 'set not an int', 2,
[qr{cannot coerce boolean to int}], q{\set i TRUE + 2}
],
[ 'set not a double', 2,
[qr{cannot coerce boolean to double}], q{\set d ln(TRUE)}
],
[ 'set case error', 1,
[qr{syntax error in command "set"}],
q{\set i CASE TRUE THEN 1 ELSE 0 END}
],
[ 'set random error', 2,
[qr{cannot coerce boolean to int}], q{\set b random(FALSE, TRUE)}
],
[ 'set number of args mismatch', 1,
[qr{unexpected number of arguments}], q{\set d ln(1.0, 2.0))}
],
[ 'set at least one arg', 1,
[qr{at least one argument expected}], q{\set i greatest())}
],
# SET: ARITHMETIC OVERFLOW DETECTION
[ 'set double to int overflow', 2,
[qr{double to int overflow for100}], q{\set i int(1E32)}
],
[ 'set bigint add overflow', 2,
[qr{int add out}], q{\set i (1<<62) + (1<<62)}
],
[ 'set bigint sub overflow', 2, [qr{int sub out}], q{\set i 0 - (1<<62) - (1<<62) - (1<<62)}
],
[ 'set bigint mul overflow', 2,
[qr{int mul out}], q{\set i 2 * (1<<62)}
],
[ 'set bigint div out of range', 2,
[qr{bigint div out of range}], q{\set i :minint / -1}
],
# SETSHELL
[ 'setshell not an int', 2,
[qr{command must return an integer}], q{\setshell i echo -n one}
],
[ 'setshell missing arg', 1, [qr{missing argument }], q{\setshell var} ],
[ 'setshell no such command', 2,
[qr{could not read result }], q{\setshell var no-such-command}
],
# SLEEP
[ 'sleep undefined variable', 2,
[qr{sleep: undefined variable}], q{\sleep :nosuchvariable}
],
[ 'sleep too many args', 1,
[qr{too many arguments}], q{\sleep too many args}
],
[ 'sleep missing arg', 1,
[ qr{missing argument}, qr{\\sleep} ], q{\sleep}
],
[ 'sleep unknown unit', 1,
[qr{unrecognized time unit}], q{\sleep 1 week}
],
# MISC
[ 'misc invalid backslash command', 1,
[qr{invalid command .* "nosuchcommand"}], q{\nosuchcommand}
],
[ 'misc empty script', 1, [qr{empty command list for script}], q{} ],
[ 'bad boolean', 2,
[qr{malformed variable.*trueXXX}], q{\set b :badtrue or true}
],
[ 'invalid permute size', 2,
[qr{permute size parameter must be greater than zero}],
q{\set i permute(0, 0)}
],
# GSET
[ 'gset no row', 2,
[qr{expected one row, got 0\b}], q{SELECT WHERE FALSE \gset}
],
[ 'gset alone', 1, [qr{gset must follow an SQL command}], q{\gset} ],
[ 'gset no SQL', 1,
[qr{gset must follow an SQL command}], q{\set i +1
\gset}
],
[ 'gset too many arguments', 1,
[qr{too many arguments}], q{SELECT 1 \gset a b}
],
[ 'gset after gset', 1,
[qr{gset must follow an SQL command}], q{SELECT 1 AS i \gset
\gset}
],
[ 'gset non SELECT', 2,
[qr{expected one row, got 0}],
q{DROP TABLE IF EXISTS no_such_table \gset}
],
[ 'gset bad default name', 2,
[qr{error storing into variable \?column\?}], q{SELECT 1 \gset}
],
[ 'gset bad name', 2,
[qr{error storing into variable bad name!}],
q{SELECT 1 AS "bad name!" \gset}
],);
for my $e (@errors)
{
my ($name, $status, $re, $script, $no_prepare) = @$e;
$status != 0 or die "invalid expected status for test \"$name\"";
my $n = '001_pgbench_error_' . $name;
$n =~ s/ /_/g;
$node->pgbench( '-n -t 1 -Dfoo=bla -Dnull=null -Dtrue=true -Done=1 -Dzero=0.0 -Dbadtrue=trueXXX'
. ' -Dmaxint=9223372036854775807 -Dminint=-9223372036854775808'
. ($no_prepare ? '' : ' -M prepared'),
$status,
[ $status == 1 ? qr{^$} : qr{processed: 0/1} ],
$re, 'pgbench script error: ' . $name,
{ $n => $script });
}
# given the expected rate and the 2 ms tx duration, at most one is executed '-t 10 --rate=100000 --latency-limit=1 -n -r', 0,
[
qr{processed: [01]/10},
qr{type: .*/001_pgbench_sleep},
qr{above the 1.0 ms latency limit: [01]/}
],
[qr{^$}], 'pgbench late throttling',
{ '001_pgbench_sleep' => q{\sleep 2ms} });
# return a list of files from directory $dir matching regexpr $re # this works around glob portability and escaping issues
sub list_files
{
my ($dir, $re) = @_;
opendir my $dh, $dir or die "cannot opendir $dir: $!";
my @files = grep /$re/, readdir $dh;
closedir $dh or die "cannot closedir $dir: $!";
return map { $dir . '/' . $_ } @files;
}
# Check log contents and clean them up: # $dir: directory holding logs # $prefix: file prefix for per-thread logs # $nb: number of expected files # $min/$max: minimum and maximum number of lines in log files # $re: regular expression each log line has to match
sub check_pgbench_logs
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
my ($dir, $prefix, $nb, $min, $max, $re) = @_;
# $prefix is simple enough, thus does not need escaping
my @logs = list_files($dir, qr{^$prefix\..*$});
ok(@logs == $nb, "number of log files");
ok(grep(/\/$prefix\.\d+(\.\d+)?$/, @logs) == $nb, "file name format");
my $log_number = 0; for my $log (sort @logs)
{ # Check the contents of each log file.
my $contents_raw = slurp_file($log);
my @contents = split(/\n/, $contents_raw);
my $clen = @contents;
ok( $min <= $clen && $clen <= $max, "transaction count for $log ($clen)");
my $clen_match = grep(/$re/, @contents);
ok($clen_match == $clen, "transaction format for $prefix");
# Show more information if some logs don't match # to help with debugging. if ($clen_match != $clen)
{ foreach my $log (@contents)
{
print "# Log entry not matching: $log\n"
unless $log =~ /$re/;
}
}
}
return;
}
my $bdir = $node->basedir;
# Run with sampling rate, 2 clients with 50 transactions each.
$node->pgbench( "-n -S -t 50 -c 2 --log --sampling-rate=0.5", 0,
[ qr{select only}, qr{processed: 100/100} ], [qr{^$}], 'pgbench logs', undef, "--log-prefix=$bdir/001_pgbench_log_2"); # The IDs of the clients (1st field) in the logs should be either 0 or 1.
check_pgbench_logs($bdir, '001_pgbench_log_2', 1, 8, 92,
qr{^[01] \d{1,2} \d+ \d \d+ \d+$});
# Run with different read-only option pattern, 1 client with 10 transactions.
$node->pgbench( "-n -b select-only -t 10 -l", 0,
[ qr{select only}, qr{processed: 10/10} ], [qr{^$}], 'pgbench logs contents', undef, "--log-prefix=$bdir/001_pgbench_log_3"); # The ID of a single client (1st field) should match 0.
check_pgbench_logs($bdir, '001_pgbench_log_3', 1, 10, 10,
qr{^0 \d{1,2} \d+ \d \d+ \d+$});
# abortion of the client if the script contains an incomplete transaction block
$node->pgbench( '--no-vacuum', 2,
[qr{processed: 1/10}],
[
qr{client 0 aborted: end of script reached without completing the last transaction}
], 'incomplete transaction block',
{ '001_pgbench_incomplete_transaction_block' => q{BEGIN;SELECT 1;} });
# Test the concurrent update in the table row and deadlocks.
local $ENV{PGOPTIONS} = "-c default_transaction_isolation=repeatable\\ read";
# Check that we have a serialization error and the same random value of the # delta variable in the next try
my $err_pattern = "(client (0|1) sending UPDATE xy SET y = y \\+ -?\\d+\\b).*"
. "client \\2 got an error in command 3 \\(SQL\\) of script 0; "
. "ERROR: could not serialize access due to concurrent update\\b.*"
. "\\1";
$node->pgbench( "-n -c 2 -t 1 --debug --verbose-errors --max-tries 2", 0,
[
qr{processed: 2/2\b},
qr{number of transactions retried: 1\b},
qr{total number of retries: 1\b}
],
[qr/$err_pattern/s], 'concurrent update with retrying',
{ '001_pgbench_serialization' => q{
-- What's happening:
-- The first client starts the transaction with the isolation level Repeatable
-- Read:
--
-- BEGIN;
-- UPDATE xy SET y = ... WHERE x = 1;
--
-- The second client starts a similar transaction with the same isolation level:
--
-- BEGIN;
-- UPDATE xy SET y = ... WHERE x = 1;
-- <waiting for the first client>
--
-- The first client commits its transaction, and the second client gets a
-- serialization error.
\set delta random(-5000, 5000)
-- The second client will stop here
SELECT pg_advisory_lock(0);
-- Start transaction with concurrent update
BEGIN;
UPDATE xy SET y = y + :delta WHERE x = 1 AND pg_advisory_lock(1) IS NOT NULL;
-- Wait for the second client
DO $$
DECLARE
exists boolean;
waiters integer;
BEGIN
-- The second client always comes in second, and the number of rows in the
-- table first_client_table reflect this. Here the first client inserts a row,
-- so the second client will see a non-empty table when repeating the
-- transaction after the serialization error.
SELECT EXISTS (SELECT * FROM first_client_table) INTO STRICT exists;
IF NOT exists THEN
-- Let the second client begin
PERFORM pg_advisory_unlock(0);
-- And wait until the second client tries to get the same lock
LOOP
SELECT COUNT(*) INTO STRICT waiters FROM pg_locks WHERE
locktype = 'advisory' AND objsubid = 1 AND
((classid::bigint << 32) | objid::bigint = 1::bigint) AND NOT granted;
IF waiters = 1 THEN
INSERT INTO first_client_table VALUES (1);
-- Exit loop
EXIT;
END IF;
END LOOP;
END IF;
END$$;
COMMIT;
SELECT pg_advisory_unlock_all();
}
});
# Clean up
$node->safe_psql('postgres', 'DELETE FROM first_client_table;');
local $ENV{PGOPTIONS} = "-c default_transaction_isolation=read\\ committed";
# Deadlock error and retry
# Check that we have a deadlock error
$err_pattern = "client (0|1) got an error in command (3|5) \\(SQL\\) of script 0; "
. "ERROR: deadlock detected\\b";
$node->pgbench( "-n -c 2 -t 1 --max-tries 2 --verbose-errors", 0,
[
qr{processed: 2/2\b},
qr{number of transactions retried: 1\b},
qr{total number of retries: 1\b}
],
[qr{$err_pattern}], 'deadlock with retrying',
{ '001_pgbench_deadlock' => q{
-- What's happening:
-- The first client gets the lock 2.
-- The second client gets the lock 3 and tries to get the lock 2.
-- The first client tries to get the lock 3 and one of them gets a deadlock
-- error.
--
-- A client that does not get a deadlock error must hold a lock at the
-- transaction start. Thus in the end it releases all of its locks before the
-- client with the deadlock error starts a retry (we do not want any errors
-- again).
-- Since the client with the deadlock error has not released the blocking locks,
-- let's do this here.
SELECT pg_advisory_unlock_all();
-- The second client and the client with the deadlock error stop here
SELECT pg_advisory_lock(0);
SELECT pg_advisory_lock(1);
-- The second client and the client with the deadlock error always come after
-- the first and the number of rows in the table first_client_table reflects
-- this. Here the first client inserts a row, so in the future the table is
-- always non-empty.
DO $$
DECLARE
exists boolean;
BEGIN
SELECT EXISTS (SELECT * FROM first_client_table) INTO STRICT exists;
IF exists THEN
-- We are the second client or the client with the deadlock error
-- The first client will take care by itself of this lock (see below)
PERFORM pg_advisory_unlock(0);
PERFORM pg_advisory_lock(3);
-- The second client can get a deadlock here
PERFORM pg_advisory_lock(2);
ELSE
-- We are the first client
-- This code should not be used in a new transaction after an error
INSERT INTO first_client_table VALUES (1);
PERFORM pg_advisory_lock(2);
END IF;
END$$;
DO $$
DECLARE
num_rows integer;
waiters integer;
BEGIN
-- Check if we are the first client
SELECT COUNT(*) FROM first_client_table INTO STRICT num_rows;
IF num_rows = 1 THEN
-- This code should not be used in a new transaction after an error
INSERT INTO first_client_table VALUES (2);
-- Let the second client begin
PERFORM pg_advisory_unlock(0);
PERFORM pg_advisory_unlock(1);
-- Make sure the second client is ready for deadlock
LOOP
SELECT COUNT(*) INTO STRICT waiters FROM pg_locks WHERE
locktype = 'advisory' AND
objsubid = 1 AND
((classid::bigint << 32) | objid::bigint = 2::bigint) AND
NOT granted;
IF waiters = 1 THEN
-- Exit loop
EXIT;
END IF;
END LOOP;
PERFORM pg_advisory_lock(0);
-- And the second client took care by itself of the lock 1
END IF;
END$$;
-- The first client can get a deadlock here
SELECT pg_advisory_lock(3);
SELECT pg_advisory_unlock_all();
}
});
# Clean up
$node->safe_psql('postgres', 'DROP TABLE first_client_table, xy;');
# Test --exit-on-abort
$node->safe_psql('postgres', 'CREATE TABLE counter(i int); ' . 'INSERT INTO counter VALUES (0);');
$node->pgbench( '-t 10 -c 2 -j 2 --exit-on-abort', 2,
[],
[ qr{division by zero}, qr{Run was aborted due to an error in thread} ], 'test --exit-on-abort',
{ '001_exit_on_abort' => q{
update counter set i = i+1 returning i \gset
\if :i = 5
\set y 1/0
\endif
}
});
# Test copy in pgbench
$node->pgbench( '-t 10', 2,
[],
[ qr{COPY is not supported in pgbench, aborting} ], 'Test copy in script',
{ '001_copy' => q{ COPY pgbench_accounts FROM stdin }
});
# Clean up
$node->safe_psql('postgres', 'DROP TABLE counter;');
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.