/*
* Copyright (c) 2004, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/
package org.postgresql.test;
import org.postgresql.PGProperty;
import org.postgresql.core.ServerVersion;
import org.postgresql.core.Version;
import org.postgresql.jdbc.PgConnection;
import org.junit.Assert;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Utility class for JDBC tests
*/
public class TestUtil {
/*
* Returns the Test database JDBC URL
*/
public static String getURL() {
return getURL(getServer(), getPort());
}
public static String getURL(String server, int port) {
String logLevel = "";
if (getLogLevel() != null && !getLogLevel().equals("")) {
logLevel = "&loggerLevel=" + getLogLevel();
}
String logFile = "";
if (getLogFile() != null && !getLogFile().equals("")) {
logFile = "&loggerFile=" + getLogFile();
}
String protocolVersion = "";
if (getProtocolVersion() != 0) {
protocolVersion = "&protocolVersion=" + getProtocolVersion();
}
String binaryTransfer = "";
if (getBinaryTransfer() != null && !getBinaryTransfer().equals("")) {
binaryTransfer = "&binaryTransfer=" + getBinaryTransfer();
}
String receiveBufferSize = "";
if (getReceiveBufferSize() != -1) {
receiveBufferSize = "&receiveBufferSize=" + getReceiveBufferSize();
}
String sendBufferSize = "";
if (getSendBufferSize() != -1) {
sendBufferSize = "&sendBufferSize=" + getSendBufferSize();
}
String ssl = "";
if (getSSL() != null) {
ssl = "&ssl=" + getSSL();
}
return "jdbc:postgresql://"
+ server + ":"
+ port + "/"
+ getDatabase()
+ "?ApplicationName=Driver Tests"
+ logLevel
+ logFile
+ protocolVersion
+ binaryTransfer
+ receiveBufferSize
+ sendBufferSize
+ ssl;
}
/*
* Returns the Test server
*/
public static String getServer() {
return System.getProperty("server", "localhost");
}
/*
* Returns the Test port
*/
public static int getPort() {
return Integer.parseInt(System.getProperty("port", System.getProperty("def_pgport")));
}
/*
* Returns the server side prepared statement threshold.
*/
public static int getPrepareThreshold() {
return Integer.parseInt(System.getProperty("preparethreshold", "5"));
}
public static int getProtocolVersion() {
return Integer.parseInt(System.getProperty("protocolVersion", "0"));
}
/*
* Returns the Test database
*/
public static String getDatabase() {
return System.getProperty("database");
}
/*
* Returns the Postgresql username
*/
public static String getUser() {
return System.getProperty("username");
}
/*
* Returns the user's password
*/
public static String getPassword() {
return System.getProperty("password");
}
/*
* Returns the user for SSPI authentication tests
*/
public static String getSSPIUser() {
return System.getProperty("sspiusername");
}
/*
* postgres like user
*/
public static String getPrivilegedUser() {
return System.getProperty("privilegedUser");
}
public static String getPrivilegedPassword() {
return System.getProperty("privilegedPassword");
}
/*
* Returns the log level to use
*/
public static String getLogLevel() {
return System.getProperty("loggerLevel");
}
/*
* Returns the log file to use
*/
public static String getLogFile() {
return System.getProperty("loggerFile");
}
/*
* Returns the binary transfer mode to use
*/
public static String getBinaryTransfer() {
return System.getProperty("binaryTransfer");
}
public static int getSendBufferSize() {
return Integer.parseInt(System.getProperty("sendBufferSize", "-1"));
}
public static int getReceiveBufferSize() {
return Integer.parseInt(System.getProperty("receiveBufferSize", "-1"));
}
public static String getSSL() {
return System.getProperty("ssl");
}
static {
try {
initDriver();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("Unable to initialize driver", e);
}
}
private static boolean initialized = false;
public static Properties loadPropertyFiles(String... names) {
Properties p = new Properties();
for (String name : names) {
for (int i = 0; i < 2; i++) {
// load x.properties, then x.local.properties
if (i == 1 && name.endsWith(".properties") && !name.endsWith(".local.properties")) {
name = name.replaceAll("\\.properties$", ".local.properties");
}
File f = getFile(name);
if (!f.exists()) {
System.out.println("Configuration file " + f.getAbsolutePath()
+ " does not exist. Consider adding it to specify test db host and login");
continue;
}
try {
p.load(new FileInputStream(f));
} catch (IOException ex) {
// ignore
}
}
}
return p;
}
public static void initDriver() throws Exception {
synchronized (TestUtil.class) {
if (initialized) {
return;
}
Properties p = loadPropertyFiles("build.properties");
p.putAll(System.getProperties());
System.getProperties().putAll(p);
initialized = true;
}
}
/**
* Resolves file path with account of {@code build.properties.relative.path}. This is a bit tricky
* since during maven release, maven does a temporary checkout to {@code core/target/checkout}
* folder, so that script should somehow get {@code build.local.properties}
*
* @param name original name of the file, as if it was in the root pgjdbc folder
* @return actual location of the file
*/
public static File getFile(String name) {
if (name == null) {
throw new IllegalArgumentException("null file name is not expected");
}
if (name.startsWith("/")) {
return new File(name);
}
return new File(System.getProperty("build.properties.relative.path", "../"), name);
}
/**
* Get a connection using a priviliged user mostly for tests that the ability to load C functions
* now as of 4/14
*
* @return connection using a priviliged user mostly for tests that the ability to load C
* functions now as of 4/14
*/
public static java.sql.Connection openPrivilegedDB() throws Exception {
initDriver();
Properties properties = new Properties();
properties.setProperty("user", getPrivilegedUser());
properties.setProperty("password", getPrivilegedPassword());
return DriverManager.getConnection(getURL(), properties);
}
/**
* Helper - opens a connection.
*
* @return connection
*/
public static java.sql.Connection openDB() throws Exception {
return openDB(new Properties());
}
/*
* Helper - opens a connection with the allowance for passing additional parameters, like
* "compatible".
*/
public static java.sql.Connection openDB(Properties props) throws Exception {
initDriver();
// Allow properties to override the user name.
String user = props.getProperty("username");
if (user == null) {
user = getUser();
}
if (user == null) {
throw new IllegalArgumentException(
"user name is not specified. Please specify 'username' property via -D or build.properties");
}
props.setProperty("user", user);
String password = getPassword();
if (password == null) {
password = "";
}
props.setProperty("password", password);
if (!props.containsKey(PGProperty.PREPARE_THRESHOLD.getName())) {
PGProperty.PREPARE_THRESHOLD.set(props, getPrepareThreshold());
}
if (!props.containsKey(PGProperty.PREFER_QUERY_MODE.getName())) {
String value = System.getProperty(PGProperty.PREFER_QUERY_MODE.getName());
if (value != null) {
props.put(PGProperty.PREFER_QUERY_MODE.getName(), value);
}
}
return DriverManager.getConnection(getURL(), props);
}
/*
* Helper - closes an open connection.
*/
public static void closeDB(Connection con) throws SQLException {
if (con != null) {
con.close();
}
}
/*
* Helper - creates a test schema for use by a test
*/
public static void createSchema(Connection con, String schema) throws SQLException {
Statement st = con.createStatement();
try {
// Drop the schema
dropSchema(con, schema);
// Now create the schema
String sql = "CREATE SCHEMA " + schema;
st.executeUpdate(sql);
} finally {
closeQuietly(st);
}
}
/*
* Helper - drops a schema
*/
public static void dropSchema(Connection con, String schema) throws SQLException {
Statement stmt = con.createStatement();
try {
String sql = "DROP SCHEMA " + schema + " CASCADE ";
stmt.executeUpdate(sql);
} catch (SQLException ex) {
// Since every create schema issues a drop schema
// it's easy to get a schema doesn't exist error.
// we want to ignore these, but if we're in a
// transaction then we've got trouble
if (!con.getAutoCommit()) {
throw ex;
}
}
}
/*
* Helper - creates a test table for use by a test
*/
public static void createTable(Connection con, String table, String columns) throws SQLException {
// by default we don't request oids.
createTable(con, table, columns, false);
}
/*
* Helper - creates a test table for use by a test
*/
public static void createTable(Connection con, String table, String columns, boolean withOids)
throws SQLException {
Statement st = con.createStatement();
try {
// Drop the table
dropTable(con, table);
// Now create the table
String sql = "CREATE TABLE " + table + " (" + columns + ")";
if (withOids) {
sql += " WITH OIDS";
}
st.executeUpdate(sql);
} finally {
closeQuietly(st);
}
}
/**
* Helper creates a temporary table
*
* @param con Connection
* @param table String
* @param columns String
*/
public static void createTempTable(Connection con, String table, String columns)
throws SQLException {
Statement st = con.createStatement();
try {
// Drop the table
dropTable(con, table);
// Now create the table
st.executeUpdate("create temp table " + table + " (" + columns + ")");
} finally {
closeQuietly(st);
}
}
/**
* Helper creates an enum type
*
* @param con Connection
* @param name String
* @param values String
*/
public static void createEnumType(Connection con, String name, String values)
throws SQLException {
Statement st = con.createStatement();
try {
dropType(con, name);
// Now create the table
st.executeUpdate("create type " + name + " as enum (" + values + ")");
} finally {
closeQuietly(st);
}
}
/**
* Helper creates an composite type
*
* @param con Connection
* @param name String
* @param values String
*/
public static void createCompositeType(Connection con, String name, String values)
throws SQLException {
Statement st = con.createStatement();
try {
dropType(con, name);
// Now create the table
st.executeUpdate("create type " + name + " as (" + values + ")");
} finally {
closeQuietly(st);
}
}
/**
* Drops a domain
*
* @param con Connection
* @param name String
*/
public static void dropDomain(Connection con, String name)
throws SQLException {
Statement st = con.createStatement();
try {
st.executeUpdate("drop domain " + name + " cascade");
} catch (SQLException ex) {
if (!con.getAutoCommit()) {
throw ex;
}
} finally {
closeQuietly(st);
}
}
/**
* Helper creates a domain
*
* @param con Connection
* @param name String
* @param values String
*/
public static void createDomain(Connection con, String name, String values)
throws SQLException {
Statement st = con.createStatement();
try {
dropDomain(con, name);
// Now create the table
st.executeUpdate("create domain " + name + " as " + values);
} finally {
closeQuietly(st);
}
}
/*
* drop a sequence because older versions don't have dependency information for serials
*/
public static void dropSequence(Connection con, String sequence) throws SQLException {
Statement stmt = con.createStatement();
try {
String sql = "DROP SEQUENCE " + sequence;
stmt.executeUpdate(sql);
} catch (SQLException sqle) {
if (!con.getAutoCommit()) {
throw sqle;
}
}
}
/*
* Helper - drops a table
*/
public static void dropTable(Connection con, String table) throws SQLException {
Statement stmt = con.createStatement();
try {
String sql = "DROP TABLE " + table + " CASCADE ";
stmt.executeUpdate(sql);
} catch (SQLException ex) {
// Since every create table issues a drop table
// it's easy to get a table doesn't exist error.
// we want to ignore these, but if we're in a
// transaction then we've got trouble
if (!con.getAutoCommit()) {
throw ex;
}
}
}
/*
* Helper - drops a type
*/
public static void dropType(Connection con, String type) throws SQLException {
Statement stmt = con.createStatement();
try {
String sql = "DROP TYPE " + type + " CASCADE";
stmt.executeUpdate(sql);
} catch (SQLException ex) {
if (!con.getAutoCommit()) {
throw ex;
}
}
}
public static void assertNumberOfRows(Connection con, String tableName, int expectedRows, String message)
throws SQLException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
ps = con.prepareStatement("select count(*) from " + tableName + " as t");
rs = ps.executeQuery();
rs.next();
Assert.assertEquals(message, expectedRows, rs.getInt(1));
} finally {
closeQuietly(rs);
closeQuietly(ps);
}
}
/*
* Helper - generates INSERT SQL - very simple
*/
public static String insertSQL(String table, String values) {
return insertSQL(table, null, values);
}
public static String insertSQL(String table, String columns, String values) {
String s = "INSERT INTO " + table;
if (columns != null) {
s = s + " (" + columns + ")";
}
return s + " VALUES (" + values + ")";
}
/*
* Helper - generates SELECT SQL - very simple
*/
public static String selectSQL(String table, String columns) {
return selectSQL(table, columns, null, null);
}
public static String selectSQL(String table, String columns, String where) {
return selectSQL(table, columns, where, null);
}
public static String selectSQL(String table, String columns, String where, String other) {
String s = "SELECT " + columns + " FROM " + table;
if (where != null) {
s = s + " WHERE " + where;
}
if (other != null) {
s = s + " " + other;
}
return s;
}
/*
* Helper to prefix a number with leading zeros - ugly but it works...
*
* @param v value to prefix
*
* @param l number of digits (0-10)
*/
public static String fix(int v, int l) {
String s = "0000000000".substring(0, l) + Integer.toString(v);
return s.substring(s.length() - l);
}
public static String escapeString(Connection con, String value) throws SQLException {
if (con instanceof PgConnection) {
return ((PgConnection) con).escapeString(value);
}
return value;
}
public static boolean getStandardConformingStrings(Connection con) {
if (con instanceof PgConnection) {
return ((PgConnection) con).getStandardConformingStrings();
}
return false;
}
/**
* Determine if the given connection is connected to a server with a version of at least the given
* version. This is convenient because we are working with a java.sql.Connection, not an Postgres
* connection.
*/
public static boolean haveMinimumServerVersion(Connection con, int version) throws SQLException {
if (con instanceof PgConnection) {
return ((PgConnection) con).haveMinimumServerVersion(version);
}
return false;
}
public static boolean haveMinimumServerVersion(Connection con, Version version)
throws SQLException {
if (con instanceof PgConnection) {
return ((PgConnection) con).haveMinimumServerVersion(version);
}
return false;
}
public static boolean haveMinimumJVMVersion(String version) {
String jvm = java.lang.System.getProperty("java.version");
return (jvm.compareTo(version) >= 0);
}
public static boolean isProtocolVersion(Connection con, int version) {
if (con instanceof PgConnection) {
return (version == ((PgConnection) con).getProtocolVersion());
}
return false;
}
/**
* Print a ResultSet to System.out. This is useful for debugging tests.
*/
public static void printResultSet(ResultSet rs) throws SQLException {
ResultSetMetaData rsmd = rs.getMetaData();
for (int i = 1; i <= rsmd.getColumnCount(); i++) {
if (i != 1) {
System.out.print(", ");
}
System.out.print(rsmd.getColumnName(i));
}
System.out.println();
while (rs.next()) {
for (int i = 1; i <= rsmd.getColumnCount(); i++) {
if (i != 1) {
System.out.print(", ");
}
System.out.print(rs.getString(i));
}
System.out.println();
}
}
/*
* Find the column for the given label. Only SQLExceptions for system or set-up problems are
* thrown. The PSQLState.UNDEFINED_COLUMN type exception is consumed to allow cleanup. Relying on
* the caller to detect if the column lookup was successful.
*/
public static int findColumn(PreparedStatement query, String label) throws SQLException {
int returnValue = 0;
ResultSet rs = query.executeQuery();
if (rs.next()) {
try {
returnValue = rs.findColumn(label);
} catch (SQLException sqle) {
} // consume exception to allow cleanup of resource.
}
rs.close();
return returnValue;
}
/**
* Close a Connection and ignore any errors during closing.
*/
public static void closeQuietly(Connection conn) {
if (conn != null) {
try {
conn.close();
} catch (SQLException ignore) {
}
}
}
/**
* Close a Statement and ignore any errors during closing.
*/
public static void closeQuietly(Statement stmt) {
if (stmt != null) {
try {
stmt.close();
} catch (SQLException ignore) {
}
}
}
/**
* Close a ResultSet and ignore any errors during closing.
*/
public static void closeQuietly(ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException ignore) {
}
}
}
public static void recreateLogicalReplicationSlot(Connection connection, String slotName, String outputPlugin)
throws SQLException, InterruptedException, TimeoutException {
//drop previos slot
dropReplicationSlot(connection, slotName);
PreparedStatement stm = null;
try {
stm = connection.prepareStatement("SELECT * FROM pg_create_logical_replication_slot(?, ?)");
stm.setString(1, slotName);
stm.setString(2, outputPlugin);
stm.execute();
} finally {
closeQuietly(stm);
}
}
public static void recreatePhysicalReplicationSlot(Connection connection, String slotName)
throws SQLException, InterruptedException, TimeoutException {
//drop previos slot
dropReplicationSlot(connection, slotName);
PreparedStatement stm = null;
try {
stm = connection.prepareStatement("SELECT * FROM pg_create_physical_replication_slot(?)");
stm.setString(1, slotName);
stm.execute();
} finally {
closeQuietly(stm);
}
}
public static void dropReplicationSlot(Connection connection, String slotName)
throws SQLException, InterruptedException, TimeoutException {
if (haveMinimumServerVersion(connection, ServerVersion.v9_5)) {
PreparedStatement stm = null;
try {
stm = connection.prepareStatement(
"select pg_terminate_backend(active_pid) from pg_replication_slots "
+ "where active = true and slot_name = ?");
stm.setString(1, slotName);
stm.execute();
} finally {
closeQuietly(stm);
}
}
waitStopReplicationSlot(connection, slotName);
PreparedStatement stm = null;
try {
stm = connection.prepareStatement(
"select pg_drop_replication_slot(slot_name) "
+ "from pg_replication_slots where slot_name = ?");
stm.setString(1, slotName);
stm.execute();
} finally {
closeQuietly(stm);
}
}
public static boolean isReplicationSlotActive(Connection connection, String slotName)
throws SQLException {
PreparedStatement stm = null;
ResultSet rs = null;
try {
stm =
connection.prepareStatement("select active from pg_replication_slots where slot_name = ?");
stm.setString(1, slotName);
rs = stm.executeQuery();
return rs.next() && rs.getBoolean(1);
} finally {
closeQuietly(rs);
closeQuietly(stm);
}
}
private static void waitStopReplicationSlot(Connection connection, String slotName)
throws InterruptedException, TimeoutException, SQLException {
long startWaitTime = System.currentTimeMillis();
boolean stillActive;
long timeInWait = 0;
do {
stillActive = isReplicationSlotActive(connection, slotName);
if (stillActive) {
TimeUnit.MILLISECONDS.sleep(100L);
timeInWait = System.currentTimeMillis() - startWaitTime;
}
} while (stillActive && timeInWait <= 30000);
if (stillActive) {
throw new TimeoutException("Wait stop replication slot " + timeInWait + " timeout occurs");
}
}
}