cleaning the code, move sqlserver, table name normalization in flavor, get rid of properties/file handling in DatabaseProvider

main
Jörg Prante 2 years ago
parent ca784dc6cf
commit 7378b91c28

@ -1,4 +1,12 @@
This work is based upon
The work in jdbc-connection-pool is base upon
https://github.com/brettwooldridge/HikariCP
as of 28 Dec 2021
License: Apache 2.0
The work in jdbc-query is based upon
https://github.com/susom/database

@ -20,9 +20,9 @@ import org.xbib.jdbc.connection.pool.PoolConfig;
import org.xbib.jdbc.connection.pool.PoolDataSource;
@ExtendWith(PoolTestExtension.class)
public class SaturatedPoolTest830 {
public class SaturatedPoolTest {
private static final Logger logger = Logger.getLogger(SaturatedPoolTest830.class.getName());
private static final Logger logger = Logger.getLogger(SaturatedPoolTest.class.getName());
private static final int MAX_POOL_SIZE = 10;

@ -4,6 +4,7 @@ import org.xbib.jdbc.query.Flavor;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Locale;
public class MariaDB implements Flavor {
@ -26,8 +27,16 @@ public class MariaDB implements Flavor {
}
@Override
public boolean isNormalizedUpperCase() {
return false;
public String normalizeTableName(String tableName) {
if (tableName == null) {
return tableName;
}
if (tableName.length() > 2) {
if (tableName.startsWith("\"") && tableName.endsWith("\"")) {
return tableName.substring(1, tableName.length() - 1);
}
}
return tableName.toLowerCase(Locale.ROOT);
}
@Override
@ -52,7 +61,7 @@ public class MariaDB implements Flavor {
@Override
public void setFloat(PreparedStatement preparedStatement, int i, Float floatValue) throws SQLException {
preparedStatement.setFloat(i, floatValue);
preparedStatement.setDouble(i, floatValue);
}
@Override
@ -98,6 +107,11 @@ public class MariaDB implements Flavor {
return "datetime(3)";
}
@Override
public String columnTypeLocalDateTime() {
return "DATETIME";
}
@Override
public String typeLocalDate() {
return "date";

@ -1,6 +1,7 @@
package org.xbib.jdbc.mariadb.test;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@ -9,13 +10,24 @@ import org.xbib.jdbc.query.Config;
import org.xbib.jdbc.query.ConfigSupplier;
import org.xbib.jdbc.query.DatabaseProvider;
import org.xbib.jdbc.query.OptionsOverride;
import org.xbib.jdbc.query.Schema;
import org.xbib.jdbc.query.Sql;
import org.xbib.jdbc.query.SqlArgs;
import org.xbib.jdbc.test.CommonTest;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class MariaDBTest extends CommonTest {
static MariaDBContainer<?> mariaDBContainer;
static {
// mariadb 10.3.6
mariaDBContainer = new MariaDBContainer<>("mariadb")
.withDatabaseName("testDB")
.withUsername("testUser")
@ -88,21 +100,156 @@ public class MariaDBTest extends CommonTest {
super.intervals();
}
@Disabled("MariaDB temporarily disabled")
@Override
public void metadataColumnNames() {
super.intervals();
}
@Disabled("MariaDB temporarily disabled")
@Override
public void metadataColumnTypes() {
super.metadataColumnTypes();
}
@Disabled("MariaDB temporarily disabled")
/**
* MariaDB seems to have different behavior in that is does not convert
* column names.
* I haven't figured out how to smooth over this difference, since all databases
* seem to respect the provided case when it is inside quotes, but don't provide
* a way to tell whether a particular parameter was quoted.
*/
@Override
@Test
public void metadataColumnNames() {
db.dropTableQuietly("dbtest");
new Schema().addTable("dbtest").addColumn("pk").primaryKey().schema().execute(db);
db.toSelect("select Pk, Pk as Foo, Pk as \"Foo\" from dbtest")
.query(rs -> {
Assertions.assertArrayEquals(new String[]{"Pk", "Foo", "Foo"}, rs.getColumnLabels());
return null;
});
}
/**
* This one is adjusted in that the float values are passed as double, because
* the database stores them both as double and there doesn't appear to be a way
* to tell that one was actually declared as a float.
*
* Also, CLOBs are stored as strings ("mediumtext" is just an alias).
*/
@Test
public void saveResultAsTable() {
super.saveResultAsTable();
new Schema().addTable("dbtest")
.addColumn("nbr_integer").asInteger().primaryKey().table()
.addColumn("nbr_long").asLong().table()
.addColumn("nbr_float").asFloat().table()
.addColumn("nbr_double").asDouble().table()
.addColumn("nbr_big_decimal").asBigDecimal(19, 9).table()
.addColumn("str_varchar").asString(80).table()
.addColumn("str_fixed").asStringFixed(1).table()
.addColumn("str_lob").asClob().table()
.addColumn("bin_blob").asBlob().table()
.addColumn("boolean_flag").asBoolean().table()
.addColumn("date_millis").asLocalDateTime().table()
.addColumn("local_date").asLocalDate().schema().execute(db);
db.toInsert("insert into dbtest (nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar,"
+ " str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date) values (?,?,?,?,?,?,?,?,?,?,?,?)")
.argInteger(Integer.MAX_VALUE)
.argLong(Long.MAX_VALUE)
.argDouble((double) Float.MAX_VALUE)
.argDouble(Double.MAX_VALUE)
.argBigDecimal(new BigDecimal("123.456"))
.argString("hello")
.argString("Z")
.argClobString("hello again")
.argBlobBytes(new byte[]{'1', '2'})
.argBoolean(true)
.argLocalDateTime(now)
.argLocalDate(localDateNow)
.insert(1);
db.toInsert("insert into dbtest (nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar,"
+ " str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date) values (?,?,?,?,?,?,?,?,?,?,?,?)")
.argInteger(Integer.MIN_VALUE)
.argLong(Long.MIN_VALUE)
.argDouble((double) Float.MIN_VALUE)
.argDouble(Double.MIN_VALUE)
.argBigDecimal(new BigDecimal("-123.456"))
.argString("goodbye")
.argString("A")
.argClobString("bye again")
.argBlobBytes(new byte[]{'3', '4'})
.argBoolean(false)
.argLocalDateTime(now)
.argLocalDate(localDateNow)
.insert(1);
String expectedSchema = new Schema().addTable("dbtest2")
.addColumn("nbr_integer").asInteger().table()
.addColumn("nbr_long").asLong().table()
.addColumn("nbr_float").asFloat().table()
.addColumn("nbr_double").asDouble().table()
.addColumn("nbr_big_decimal").asBigDecimal(19, 9).table()
.addColumn("str_varchar").asString(80).table()
.addColumn("str_fixed").asStringFixed(1).table()
.addColumn("str_lob").asClob().table()
.addColumn("bin_blob").asBlob().table()
.addColumn("boolean_flag").asBoolean().table()
.addColumn("date_millis").asLocalDateTime().table()
.addColumn("local_date").asLocalDate()
.schema()
.print(db.flavor());
List<SqlArgs> args = db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal,"
+ " str_varchar, str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date from dbtest")
.query(rs -> {
List<SqlArgs> result = new ArrayList<>();
while (rs.next()) {
if (result.size() == 0) {
db.dropTableQuietly("dbtest2");
Schema schema = new Schema().addTableFromRow("dbtest2", rs).schema();
assertEquals(expectedSchema, schema.print(db.flavor()));
schema.execute(db);
}
result.add(SqlArgs.readRow(rs));
}
return result;
});
db.toInsert(Sql.insert("dbtest2", args)).insertBatch();
assertEquals(2, db.toSelect("select count(*) from dbtest2").queryIntegerOrZero());
assertEquals(db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal,"
+ " str_varchar, str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date from dbtest order by 1")
.queryMany(SqlArgs::readRow),
db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal,"
+ " str_varchar, str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date from dbtest2 order by 1")
.queryMany(SqlArgs::readRow));
assertEquals(Arrays.asList(
new SqlArgs()
.argInteger("nbr_integer", Integer.MIN_VALUE)
.argLong("nbr_long", Long.MIN_VALUE)
.argDouble("nbr_float", (double) Float.MIN_VALUE)
.argDouble("nbr_double", Double.MIN_VALUE)
.argBigDecimal("nbr_big_decimal", new BigDecimal("-123.456"))
.argString("str_varchar", "goodbye")
.argString("str_fixed", "A")
.argString("str_lob", "bye again")
.argBlobBytes("bin_blob", new byte[]{'3', '4'})
.argString("boolean_flag", "N")//.argBoolean("boolean_flag", false)
.argLocalDateTime("date_millis", now)
.argLocalDate("local_date", localDateNow),
new SqlArgs()
.argInteger("nbr_integer", Integer.MAX_VALUE)
.argLong("nbr_long", Long.MAX_VALUE)
.argDouble("nbr_float", (double) Float.MAX_VALUE)
.argDouble("nbr_double", Double.MAX_VALUE)
.argBigDecimal("nbr_big_decimal", new BigDecimal("123.456"))
.argString("str_varchar", "hello")
.argString("str_fixed", "Z")
.argString("str_lob", "hello again")
.argBlobBytes("bin_blob", new byte[]{'1', '2'})
.argString("boolean_flag", "Y")//.argBoolean("boolean_flag", true)
.argLocalDateTime("date_millis", now)
.argLocalDate("local_date", localDateNow)),
db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal,"
+ " str_varchar, str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date from dbtest2 order by 1")
.queryMany(SqlArgs::readRow));
}
}

@ -28,8 +28,16 @@ public class Oracle implements Flavor {
}
@Override
public boolean isNormalizedUpperCase() {
return true;
public String normalizeTableName(String tableName) {
if (tableName == null) {
return tableName;
}
if (tableName.length() > 2) {
if (tableName.startsWith("\"") && tableName.endsWith("\"")) {
return tableName.substring(1, tableName.length() - 1);
}
}
return tableName.toUpperCase();
}
@Override
@ -87,6 +95,11 @@ public class Oracle implements Flavor {
return "timestamp(3)";
}
@Override
public String columnTypeLocalDateTime() {
return "TIMESTAMP";
}
@Override
public String typeLocalDate() {
return "date";

@ -4,6 +4,7 @@ import org.xbib.jdbc.query.Flavor;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Locale;
public class Postgresql implements Flavor {
@ -26,8 +27,16 @@ public class Postgresql implements Flavor {
}
@Override
public boolean isNormalizedUpperCase() {
return false;
public String normalizeTableName(String tableName) {
if (tableName == null) {
return tableName;
}
if (tableName.length() > 2) {
if (tableName.startsWith("\"") && tableName.endsWith("\"")) {
return tableName.substring(1, tableName.length() - 1);
}
}
return tableName.toLowerCase(Locale.ROOT);
}
@Override
@ -95,6 +104,11 @@ public class Postgresql implements Flavor {
return "timestamp(3)";
}
@Override
public String columnTypeLocalDateTime() {
return "timestamp";
}
@Override
public String typeLocalDate() {
return "date";

@ -20,6 +20,7 @@ public class PostgresqlTest extends CommonTest {
static PostgreSQLContainer<?> postgreSQLContainer;
static {
// postgresql 9.6.12
postgreSQLContainer = new PostgreSQLContainer<>("postgres")
.withDatabaseName("testDB")
.withUsername("testUser")

@ -8,3 +8,4 @@ java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.FileHandler.pattern=build/database.log
jdk.event.security.level=INFO
javax.management.level=INFO
org.postgresql.level=INFO

@ -1,7 +1,13 @@
dependencies {
api project(":jdbc-connection-pool")
api project(':jdbc-connection-pool')
testImplementation project(':jdbc-test')
testImplementation libs.derby
testImplementation libs.hsqldb
testImplementation libs.h2
testImplementation libs.testcontainers
testImplementation libs.testcontainers.junit.jupiter
}
test {
systemProperty 'user.timezone', 'GMT'
}

@ -1,12 +1,12 @@
import org.xbib.jdbc.query.Flavor;
import org.xbib.jdbc.query.flavor.Derby;
import org.xbib.jdbc.query.flavor.H2;
import org.xbib.jdbc.query.flavor.Hsql;
import org.xbib.jdbc.query.flavor.SqlServer;
module org.xbib.jdbc.query {
uses Flavor;
requires org.xbib.jdbc.connection.pool;
requires java.sql;
exports org.xbib.jdbc.query;
provides Flavor with Derby, Hsql, SqlServer;
provides Flavor with Derby, Hsql, H2;
}

@ -11,6 +11,11 @@ import java.util.function.Supplier;
* Primary class for accessing a relational (SQL) database.
*/
public interface Database extends Supplier<Database> {
int jdbcMajorVersion();
int jdbcMinorVersion();
/**
* Create a SQL "insert" statement for further manipulation and execution.
* Note this call does not actually execute the SQL.
@ -181,18 +186,6 @@ public interface Database extends Supplier<Database> {
*/
Map<String, Integer> getColumnSizesOfTable(String tableName);
/**
* Return the DB table name in the normalized form in which it is stored.
* Databases like Oracle, Derby, HSQL store their tables in upper case.
* Databases like postgres and sqlserver use lower case unless configured otherwise.
* If the caller passes in a quoted string, we will leave the name as is, removing
* the quotes.
*
* @param tableName this should be a name, not a pattern
* @return table name in appropriate format for DB lookup - original case, uppercase, or lowercase
*/
String normalizeTableName(String tableName);
/**
* Check the JVM time (and timezone) against the database and log a warning
* or throw an error if they are too far apart. It is a good idea to do this

@ -131,16 +131,15 @@ public class DatabaseImpl implements Database {
throw new DatabaseException("Unable to rollback transaction", e);
}
}
@Override
public Connection underlyingConnection() {
if (!options.allowConnectionAccess()) {
throw new DatabaseException("Calls to underlyingConnection() are not allowed");
}
return connection;
}
@Override
public Options options() {
return options;
@ -151,7 +150,24 @@ public class DatabaseImpl implements Database {
return options.flavor();
}
@Override
public int jdbcMajorVersion() {
try {
return connection.getMetaData().getJDBCMajorVersion();
} catch (SQLException e) {
throw new DatabaseException(e);
}
}
@Override
public int jdbcMinorVersion() {
try {
return connection.getMetaData().getJDBCMinorVersion();
} catch (SQLException e) {
throw new DatabaseException(e);
}
}
@Override
public When when() {
return new When(options.flavor());
@ -181,7 +197,7 @@ public class DatabaseImpl implements Database {
if (tableName != null && connection != null) {
try {
DatabaseMetaData metadata = connection.getMetaData();
String normalizedTable = normalizeTableName(tableName);
String normalizedTable = flavor().normalizeTableName(tableName);
ResultSet resultSet = metadata.getTables(connection.getCatalog(), schemaName, normalizedTable, new String[]{"TABLE", "VIEW"});
while (resultSet.next()) {
if (normalizedTable.equals(resultSet.getString("TABLE_NAME"))) {
@ -203,7 +219,7 @@ public class DatabaseImpl implements Database {
if (tableName != null && connection != null) {
try {
DatabaseMetaData metaData = connection.getMetaData();
String normalizedTable = normalizeTableName(tableName);
String normalizedTable = flavor().normalizeTableName(tableName);
ResultSet resultSet = metaData.getColumns(null, null, normalizedTable, "%");
ResultSetMetaData rsmd = resultSet.getMetaData();
int cols = rsmd.getColumnCount();
@ -229,24 +245,6 @@ public class DatabaseImpl implements Database {
return map;
}
@Override
public String normalizeTableName(String tableName) {
if (tableName == null) {
return tableName;
}
// If user gave us a quoted string, leave it alone for look up
if (tableName.length() > 2) {
if (tableName.startsWith("\"") && tableName.endsWith("\"")) {
// Remove quotes and return as is.
return tableName.substring(1, tableName.length() - 1);
}
}
if (flavor().isNormalizedUpperCase()) {
return tableName.toUpperCase();
}
return tableName.toLowerCase();
}
@Override
public void assertTimeSynchronized(long millisToWarn, long millisToError) {
toSelect("select ?" + flavor().fromAny())

@ -6,12 +6,8 @@ import org.xbib.jdbc.query.util.Metric;
import javax.sql.DataSource;
import java.io.Closeable;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
@ -30,7 +26,7 @@ import java.util.logging.Logger;
*/
public final class DatabaseProvider implements Supplier<Database>, Closeable {
private static final Logger log = Logger.getLogger(DatabaseProvider.class.getName());
private static final Logger logger = Logger.getLogger(DatabaseProvider.class.getName());
private static final AtomicInteger poolNameCounter = new AtomicInteger(1);
@ -63,7 +59,8 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
*
* <p>A database pool will be created using jdbc-connection-pool.</p>
*/
public static DatabaseProviderBuilder builder(Config config) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
public static DatabaseProviderBuilder builder(Config config)
throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
return builder(createDataSource(config), getFlavor(config));
}
@ -85,67 +82,12 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
* the JDBC standard DriverManager method. The url parameter will be inspected
* to determine the Flavor for this database.
*/
public static DatabaseProviderBuilder builder(String url) {
return builder(url, Flavor.fromJdbcUrl(url), null, null, null);
public static DatabaseProviderBuilder builder(ClassLoader classLoader, String url) {
return builder(classLoader, url, Flavor.fromJdbcUrl(url), null, null, null);
}
/**
* Builder method to create and initialize an instance of this class using
* the JDBC standard DriverManager method.
*
* @param flavor use this flavor rather than guessing based on the url
*/
public static DatabaseProviderBuilder builder(String url, Flavor flavor) {
return builder(url, flavor, null, null, null);
}
/**
* Builder method to create and initialize an instance of this class using
* the JDBC standard DriverManager method. The url parameter will be inspected
* to determine the Flavor for this database.
*/
public static DatabaseProviderBuilder builder(String url, Properties info) {
return builder(url, Flavor.fromJdbcUrl(url), info, null, null);
}
/**
* Builder method to create and initialize an instance of this class using
* the JDBC standard DriverManager method.
*
* @param flavor use this flavor rather than guessing based on the url
*/
public static DatabaseProviderBuilder builder(String url, Flavor flavor, Properties info) {
return builder(url, flavor, info, null, null);
}
/**
* Builder method to create and initialize an instance of this class using
* the JDBC standard DriverManager method. The url parameter will be inspected
* to determine the Flavor for this database.
*/
public static DatabaseProviderBuilder builder(String url, String user, String password) {
return builder(url, Flavor.fromJdbcUrl(url), null, user, password);
}
/**
* Builder method to create and initialize an instance of this class using
* the JDBC standard DriverManager method.
*
* @param flavor use this flavor rather than guessing based on the url
*/
public static DatabaseProviderBuilder builder(String url,
Flavor flavor,
String user,
String password) {
return builder(url, flavor, null, user, password);
}
private static DatabaseProviderBuilder builder(String url,
private static DatabaseProviderBuilder builder(ClassLoader classLoader,
String url,
Flavor flavor,
Properties info,
String user,
@ -156,7 +98,7 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
} catch (SQLException e) {
if (flavor.driverClass() != null) {
try {
Class.forName(flavor.driverClass());
Class.forName(flavor.driverClass(), true, classLoader);
} catch (ClassNotFoundException e1) {
throw new DatabaseException("couldn't locate JDBC driver", e1);
}
@ -176,417 +118,6 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
}, options);
}
/**
* Configure the database from up to five properties read from a file:
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
* <p>This will use the JVM default character encoding to read the property file.</p>
*
* @param propertyFileName path to the properties file we will attempt to read
* @throws DatabaseException if the property file could not be read for any reason
*/
public static DatabaseProviderBuilder fromPropertyFile(String propertyFileName) {
return fromPropertyFile(propertyFileName, Charset.defaultCharset().newDecoder());
}
/**
* Configure the database from up to five properties read from a file:
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
*
* @param propertyFileName path to the properties file we will attempt to read
* @param decoder character encoding to use when reading the property file
* @throws DatabaseException if the property file could not be read for any reason
*/
public static DatabaseProviderBuilder fromPropertyFile(String propertyFileName, CharsetDecoder decoder) {
Properties properties = new Properties();
if (propertyFileName != null && propertyFileName.length() > 0) {
try (
FileInputStream fis = new FileInputStream(propertyFileName);
InputStreamReader reader = new InputStreamReader(fis, decoder)
) {
properties.load(reader);
} catch (Exception e) {
throw new DatabaseException("Unable to read properties file: " + propertyFileName, e);
}
}
return fromProperties(properties, "", true);
}
/**
* Configure the database from up to five properties read from a file:
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
* <p>This will use the JVM default character encoding to read the property file.</p>
*
* @param filename path to the properties file we will attempt to read
* @param propertyPrefix if this is null or empty the properties above will be read;
* if a value is provided it will be prefixed to each property
* (exactly, so if you want to use "my.database.url" you must
* pass "my." as the prefix)
* @throws DatabaseException if the property file could not be read for any reason
*/
public static DatabaseProviderBuilder fromPropertyFile(String filename, String propertyPrefix) {
return fromPropertyFile(filename, propertyPrefix, Charset.defaultCharset().newDecoder());
}
/**
* Configure the database from up to five properties read from a file:
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
*
* @param filename path to the properties file we will attempt to read
* @param propertyPrefix if this is null or empty the properties above will be read;
* if a value is provided it will be prefixed to each property
* (exactly, so if you want to use "my.database.url" you must
* pass "my." as the prefix)
* @param decoder character encoding to use when reading the property file
* @throws DatabaseException if the property file could not be read for any reason
*/
public static DatabaseProviderBuilder fromPropertyFile(String filename, String propertyPrefix, CharsetDecoder decoder) {
Properties properties = new Properties();
if (filename != null && filename.length() > 0) {
try (
FileInputStream fis = new FileInputStream(filename);
InputStreamReader reader = new InputStreamReader(fis, decoder)
) {
properties.load(reader);
} catch (Exception e) {
throw new DatabaseException("Unable to read properties file: " + filename, e);
}
}
return fromProperties(properties, propertyPrefix, true);
}
/**
* Configure the database from up to five properties read from the provided properties:
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
*
* @param properties properties will be read from here
* @throws DatabaseException if the property file could not be read for any reason
*/
public static DatabaseProviderBuilder fromProperties(Properties properties) {
return fromProperties(properties, "", false);
}
/**
* Configure the database from up to five properties read from the provided properties:
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
*
* @param properties properties will be read from here
* @param propertyPrefix if this is null or empty the properties above will be read;
* if a value is provided it will be prefixed to each property
* (exactly, so if you want to use "my.database.url" you must
* pass "my." as the prefix)
* @throws DatabaseException if the property file could not be read for any reason
*/
public static DatabaseProviderBuilder fromProperties(Properties properties, String propertyPrefix) {
return fromProperties(properties, propertyPrefix, false);
}
/**
* Configure the database from up to five properties read from the specified
* properties file, or from the system properties (system properties will take
* precedence over the file):
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
* <p>This will use the JVM default character encoding to read the property file.</p>
*
* @param filename path to the properties file we will attempt to read; if the file
* cannot be read for any reason (e.g. does not exist) a debug level
* log entry will be entered, but it will attempt to proceed using
* solely the system properties
*/
public static DatabaseProviderBuilder fromPropertyFileOrSystemProperties(String filename) {
return fromPropertyFileOrSystemProperties(filename, Charset.defaultCharset().newDecoder());
}
/**
* Configure the database from up to five properties read from the specified
* properties file, or from the system properties (system properties will take
* precedence over the file):
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
*
* @param filename path to the properties file we will attempt to read; if the file
* cannot be read for any reason (e.g. does not exist) a debug level
* log entry will be entered, but it will attempt to proceed using
* solely the system properties
* @param decoder character encoding to use when reading the property file
*/
public static DatabaseProviderBuilder fromPropertyFileOrSystemProperties(String filename, CharsetDecoder decoder) {
Properties properties = new Properties();
if (filename != null && filename.length() > 0) {
try (
FileInputStream fis = new FileInputStream(filename);
InputStreamReader reader = new InputStreamReader(fis, decoder)
) {
properties.load(reader);
} catch (Exception e) {
log.fine("Trying system properties - unable to read properties file: " + filename);
}
}
return fromProperties(properties, "", true);
}
/**
* Configure the database from up to five properties read from the specified
* properties file, or from the system properties (system properties will take
* precedence over the file):
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
* <p>This will use the JVM default character encoding to read the property file.</p>
*
* @param filename path to the properties file we will attempt to read; if the file
* cannot be read for any reason (e.g. does not exist) a debug level
* log entry will be entered, but it will attempt to proceed using
* solely the system properties
* @param propertyPrefix if this is null or empty the properties above will be read;
* if a value is provided it will be prefixed to each property
* (exactly, so if you want to use "my.database.url" you must
* pass "my." as the prefix)
*/
public static DatabaseProviderBuilder fromPropertyFileOrSystemProperties(String filename, String propertyPrefix) {
return fromPropertyFileOrSystemProperties(filename, propertyPrefix, Charset.defaultCharset().newDecoder());
}
/**
* Configure the database from up to five properties read from the specified
* properties file, or from the system properties (system properties will take
* precedence over the file):
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
*
* @param filename path to the properties file we will attempt to read; if the file
* cannot be read for any reason (e.g. does not exist) a debug level
* log entry will be entered, but it will attempt to proceed using
* solely the system properties
* @param propertyPrefix if this is null or empty the properties above will be read;
* if a value is provided it will be prefixed to each property
* (exactly, so if you want to use "my.database.url" you must
* pass "my." as the prefix)
* @param decoder character encoding to use when reading the property file
*/
public static DatabaseProviderBuilder fromPropertyFileOrSystemProperties(String filename, String propertyPrefix,
CharsetDecoder decoder) {
Properties properties = new Properties();
if (filename != null && filename.length() > 0) {
try (
FileInputStream fis = new FileInputStream(filename);
InputStreamReader reader = new InputStreamReader(fis, decoder)
) {
properties.load(reader);
} catch (Exception e) {
log.fine("Trying system properties - unable to read properties file: " + filename);
}
}
return fromProperties(properties, propertyPrefix, true);
}
/**
* Configure the database from up to five system properties:
* <pre>
* -Ddatabase.url=... Database connect string (required)
* -Ddatabase.user=... Authenticate as this user (optional if provided in url)
* -Ddatabase.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* -Ddatabase.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* -Ddatabase.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
*/
public static DatabaseProviderBuilder fromSystemProperties() {
return fromProperties(null, "", true);
}
/**
* Configure the database from up to five system properties:
* <pre>
* -D{prefix}database.url=... Database connect string (required)
* -D{prefix}database.user=... Authenticate as this user (optional if provided in url)
* -D{prefix}database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* -D{prefix}database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* -D{prefix}database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
*
* @param propertyPrefix a prefix to attach to each system property - be sure to include the
* dot if desired (e.g. "mydb." for properties like -Dmydb.database.url)
*/
public static DatabaseProviderBuilder fromSystemProperties(String propertyPrefix) {
return fromProperties(null, propertyPrefix, true);
}
private static DatabaseProviderBuilder fromProperties(Properties properties, String propertyPrefix, boolean useSystemProperties) {
if (propertyPrefix == null) {
propertyPrefix = "";
}
String driver;
String flavorStr;
String url;
String user;
String password;
if (useSystemProperties) {
if (properties == null) {
properties = new Properties();
}
driver = System.getProperty(propertyPrefix + "database.driver",
properties.getProperty(propertyPrefix + "database.driver"));
flavorStr = System.getProperty(propertyPrefix + "database.flavor",
properties.getProperty(propertyPrefix + "database.flavor"));
url = System.getProperty(propertyPrefix + "database.url",
properties.getProperty(propertyPrefix + "database.url"));
user = System.getProperty(propertyPrefix + "database.user",
properties.getProperty(propertyPrefix + "database.user"));
password = System.getProperty(propertyPrefix + "database.password",
properties.getProperty(propertyPrefix + "database.password"));
} else {
if (properties == null) {
throw new DatabaseException("No properties were provided");
}
driver = properties.getProperty(propertyPrefix + "database.driver");
flavorStr = properties.getProperty(propertyPrefix + "database.flavor");
url = properties.getProperty(propertyPrefix + "database.url");
user = properties.getProperty(propertyPrefix + "database.user");
password = properties.getProperty(propertyPrefix + "database.password");
}
if (url == null) {
throw new DatabaseException("You must use -D" + propertyPrefix + "database.url=...");
}
if (user != null && password == null) {
System.out.println("Enter database password for user " + user + ":");
byte[] input = new byte[256];
try {
int bytesRead = System.in.read(input);
password = new String(input, 0, bytesRead - 1, Charset.defaultCharset());
} catch (IOException e) {
throw new DatabaseException("Error reading password from standard input", e);
}
}
Flavor flavor;
if (flavorStr != null) {
flavor = Flavor.valueOf(flavorStr);
} else {
flavor = Flavor.fromJdbcUrl(url);
}
if (driver != null) {
try {
Class.forName(driver).getDeclaredConstructor().newInstance();
} catch (Exception e) {
driver = flavor.driverClass();
try {
Class.forName(driver).getDeclaredConstructor().newInstance();
} catch (Exception ee) {
throw new DatabaseException("Unable to load JDBC driver: " + driver, ee);
}
}
}
if (user == null) {
return builder(url, flavor);
} else {
return builder(url, flavor, user, password);
}
}
private static DataSource createDataSource(Config config)
throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException,
IllegalAccessException {
@ -749,7 +280,7 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
if (builder.isClosed()) {
throw new DatabaseException("Called get() on a DatabaseProvider after close()");
}
Metric metric = new Metric(log.isLoggable(Level.FINE));
Metric metric = new Metric(logger.isLoggable(Level.FINE));
try {
connection = builder.connectionProvider.get();
metric.checkpoint("getConn");
@ -770,10 +301,10 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
throw e;
} finally {
metric.done();
if (log.isLoggable(Level.FINE)) {
if (logger.isLoggable(Level.FINE)) {
StringBuilder buf = new StringBuilder("Get ").append(builder.options.flavor()).append(" database: ");
metric.printMessage(buf);
log.fine(buf.toString());
logger.fine(buf.toString());
}
}
return database;
@ -811,7 +342,7 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
connection.rollback();
}
} catch (Exception e) {
log.log(Level.SEVERE, "Unable to rollback the transaction", e);
logger.log(Level.SEVERE, "Unable to rollback the transaction", e);
}
}
}
@ -825,7 +356,7 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
}
close();
} catch (Exception e) {
log.log(Level.SEVERE, "Unable to rollback the transaction", e);
logger.log(Level.SEVERE, "Unable to rollback the transaction", e);
}
}
@ -835,7 +366,7 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
try {
connection.close();
} catch (Exception e) {
log.log(Level.SEVERE, "Unable to close the database connection", e);
logger.log(Level.SEVERE, "Unable to close the database connection", e);
}
}
connection = null;
@ -843,129 +374,6 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
builder.close();
}
/**
* This builder is immutable, so setting various options does not affect
* the previous instance. This is intended to make it safe to pass builders
* around without risk someone will reconfigure it.
*/
public interface DatabaseProviderBuilder {
DatabaseProviderBuilder withOptions(OptionsOverride options);
/**
* Enable logging of parameter values along with the SQL.
*/
DatabaseProviderBuilder withSqlParameterLogging();
/**
* Include SQL in exception messages. This will also include parameters in the
* exception messages if SQL parameter logging is enabled. This is handy for
* development, but be careful as this is an information disclosure risk,
* dependent on how the exception are caught and handled.
*/
DatabaseProviderBuilder withSqlInExceptionMessages();
/**
* Wherever argDateNowPerDb() is specified, use argDateNowPerApp() instead. This is
* useful for testing purposes as you can use OptionsOverride to provide your
* own system clock that will be used for time travel.
*/
DatabaseProviderBuilder withDatePerAppOnly();
/**
* Allow provided Database instances to explicitly control transactions using the
* commitNow() and rollbackNow() methods. Otherwise calling those methods would
* throw an exception.
*/
DatabaseProviderBuilder withTransactionControl();
/**
* This can be useful when testing code, as it can pretend to use transactions,
* while giving you control over whether it actually commits or rolls back.
*/
DatabaseProviderBuilder withTransactionControlSilentlyIgnored();
/**
* Allow direct access to the underlying database connection. Normally this is
* not allowed, and is a bad idea, but it can be helpful when migrating from
* legacy code that works with raw JDBC.
*/
DatabaseProviderBuilder withConnectionAccess();
/**
* WARNING: You should try to avoid using this method. If you use it more
* that once or twice in your entire codebase you are probably doing
* something wrong.
*
* <p>If you use this method you are responsible for managing
* the transaction and commit/rollback/close.</p>
*/
DatabaseProvider build();
/**
* This is a convenience method to eliminate the need for explicitly
* managing the resources (and error handling) for this class. After
* the run block is complete the transaction will commit unless the
* {@link DbCode#run(Supplier) run(Supplier)} method threw a {@link Throwable}.
*
* <p>Here is a typical usage:
* <pre>
* dbp.transact(dbs -&gt; {
* List<String> r = dbs.get().toSelect("select a from b where c=?").argInteger(1).queryStrings();
* ... do something with the results ...
* });
* </pre>
* </p>
*
* @param code the code you want to run as a transaction with a Database
* @see #transact(DbCodeTx)
*/
void transact(DbCode code);
/**
* This method is the same as {@link #transact(DbCode)} but allows a return value.
*
* <p>Here is a typical usage:
* <pre>
* List<String> r = dbp.transact(dbs -&gt; {
* return dbs.get().toSelect("select a from b where c=?").argInteger(1).queryStrings();
* });
* </pre>
* </p>
*/
<T> T transactReturning(DbCodeTyped<T> code);
/**
* This is a convenience method to eliminate the need for explicitly
* managing the resources (and error handling) for this class. After
* the run block is complete commit() will be called unless either the
* {@link DbCodeTx#run(Supplier, Transaction)} method threw a {@link Throwable}
* while {@link Transaction#isRollbackOnError()} returns true, or
* {@link Transaction#isRollbackOnly()} returns a true value.
*
* <p>Here is a typical usage:
* <pre>
* dbp.transact((dbs, tx) -&gt; {
* tx.setRollbackOnError(false);
* dbs.get().toInsert("...").argInteger(1).insert(1);
* ...some stuff that might fail...
* });
* </pre>
* </p>
*
* @param code the code you want to run as a transaction with a Database
*/
void transact(DbCodeTx code);
}
private static class DatabaseProviderBuilderImpl implements DatabaseProviderBuilder, Closeable {
private DataSource dataSource;

@ -0,0 +1,126 @@
package org.xbib.jdbc.query;
import java.util.function.Supplier;
/**
* This builder is immutable, so setting various options does not affect
* the previous instance. This is intended to make it safe to pass builders
* around without risk someone will reconfigure it.
*/
public interface DatabaseProviderBuilder {
DatabaseProviderBuilder withOptions(OptionsOverride options);
/**
* Enable logging of parameter values along with the SQL.
*/
DatabaseProviderBuilder withSqlParameterLogging();
/**
* Include SQL in exception messages. This will also include parameters in the
* exception messages if SQL parameter logging is enabled. This is handy for
* development, but be careful as this is an information disclosure risk,
* dependent on how the exception are caught and handled.
*/
DatabaseProviderBuilder withSqlInExceptionMessages();
/**
* Wherever argDateNowPerDb() is specified, use argDateNowPerApp() instead. This is
* useful for testing purposes as you can use OptionsOverride to provide your
* own system clock that will be used for time travel.
*/
DatabaseProviderBuilder withDatePerAppOnly();
/**
* Allow provided Database instances to explicitly control transactions using the
* commitNow() and rollbackNow() methods. Otherwise calling those methods would
* throw an exception.
*/
DatabaseProviderBuilder withTransactionControl();
/**
* This can be useful when testing code, as it can pretend to use transactions,
* while giving you control over whether it actually commits or rolls back.
*/
DatabaseProviderBuilder withTransactionControlSilentlyIgnored();
/**
* Allow direct access to the underlying database connection. Normally this is
* not allowed, and is a bad idea, but it can be helpful when migrating from
* legacy code that works with raw JDBC.
*/
DatabaseProviderBuilder withConnectionAccess();
/**
* WARNING: You should try to avoid using this method. If you use it more
* that once or twice in your entire codebase you are probably doing
* something wrong.
*
* <p>If you use this method you are responsible for managing
* the transaction and commit/rollback/close.</p>
*/
DatabaseProvider build();
/**
* This is a convenience method to eliminate the need for explicitly
* managing the resources (and error handling) for this class. After
* the run block is complete the transaction will commit unless the
* {@link DbCode#run(Supplier) run(Supplier)} method threw a {@link Throwable}.
*
* <p>Here is a typical usage:
* <pre>
* dbp.transact(dbs -&gt; {
* List<String> r = dbs.get().toSelect("select a from b where c=?").argInteger(1).queryStrings();
* ... do something with the results ...
* });
* </pre>
* </p>
*
* @param code the code you want to run as a transaction with a Database
* @see #transact(DbCodeTx)
*/
void transact(DbCode code);
/**
* This method is the same as {@link #transact(DbCode)} but allows a return value.
*
* <p>Here is a typical usage:
* <pre>
* List<String> r = dbp.transact(dbs -&gt; {
* return dbs.get().toSelect("select a from b where c=?").argInteger(1).queryStrings();
* });
* </pre>
* </p>
*/
<T> T transactReturning(DbCodeTyped<T> code);
/**
* This is a convenience method to eliminate the need for explicitly
* managing the resources (and error handling) for this class. After
* the run block is complete commit() will be called unless either the
* {@link DbCodeTx#run(Supplier, Transaction)} method threw a {@link Throwable}
* while {@link Transaction#isRollbackOnError()} returns true, or
* {@link Transaction#isRollbackOnly()} returns a true value.
*
* <p>Here is a typical usage:
* <pre>
* dbp.transact((dbs, tx) -&gt; {
* tx.setRollbackOnError(false);
* dbs.get().toInsert("...").argInteger(1).insert(1);
* ...some stuff that might fail...
* });
* </pre>
* </p>
*
* @param code the code you want to run as a transaction with a Database
*/
void transact(DbCodeTx code);
}

@ -12,11 +12,7 @@ public interface Flavor {
String driverClass();
/**
* Returns true if DB normalizes to upper case names for ids like tables and columns
* See <a href="https://github.com/ontop/ontop/wiki/Case-sensitivity-for-SQL-identifiers>SQL case sensitivity</a>
*/
boolean isNormalizedUpperCase();
String normalizeTableName(String tableName);
String typeInteger();
@ -44,6 +40,8 @@ public interface Flavor {
String typeLocalDateTime();
String columnTypeLocalDateTime();
String typeLocalDate();
boolean useStringForClob();

@ -21,15 +21,19 @@ import java.util.function.Supplier;
*/
public class Schema {
private final List<Table> tables = new ArrayList<>();
private final List<Table> tables;
private final List<Sequence> sequences = new ArrayList<>();
private final List<Sequence> sequences;
private boolean indexForeignKeys = true;
private String userTableName = "user_principal";
private String userTableName;
public Schema() {
tables = new ArrayList<>();
sequences = new ArrayList<>();
indexForeignKeys = true;
userTableName = "user_principal";
}
public Sequence addSequence(String name) {
@ -70,10 +74,10 @@ public class Schema {
return table;
}
public Table addTableFromRow(String tableName, Row r) {
public Table addTableFromRow(String tableName, Row row) {
Table table = addTable(tableName);
try {
ResultSetMetaData metadata = r.getMetadata();
ResultSetMetaData metadata = row.getMetadata();
int columnCount = metadata.getColumnCount();
String[] names = new String[columnCount];
for (int i = 0; i < columnCount; i++) {
@ -464,51 +468,70 @@ public class Schema {
}
public class Table {
private final String name;
private String comment;
private final List<Column> columns = new ArrayList<>();
private PrimaryKey primaryKey;
private final List<ForeignKey> foreignKeys = new ArrayList<>();
private final List<Index> indexes = new ArrayList<>();
private final List<Check> checks = new ArrayList<>();
private final List<Unique> uniques = new ArrayList<>();
private final Map<Flavor, String> customClauses = new HashMap<>();
private final List<Column> columns;
private final List<ForeignKey> foreignKeys;
private final List<Index> indexes;
private final List<Check> checks;
private final List<Unique> uniques;
private final Map<Flavor, String> customClauses;
private boolean createTracking;
private String createTrackingFkName;
private String createTrackingFkTable;
private boolean updateTracking;
private String updateTrackingFkName;
private String updateTrackingFkTable;
private boolean updateSequence;
private boolean historyTable;
public Table(String name) {
this.name = toName(name);
if (this.name.length() > 27) {
throw new RuntimeException("Table name should be 27 characters or less");
throw new IllegalArgumentException("Table name should be 27 characters or less");
}
columns = new ArrayList<>();
foreignKeys = new ArrayList<>();
indexes = new ArrayList<>();
checks = new ArrayList<>();
uniques = new ArrayList<>();
customClauses = new HashMap<>();
}
public void validate() {
if (columns.size() < 1) {
throw new RuntimeException("Table " + name + " needs at least one column");
throw new IllegalArgumentException("Table " + name + " needs at least one column");
}
for (Column c : columns) {
c.validate();
}
if (primaryKey != null) {
primaryKey.validate();
}
for (ForeignKey fk : foreignKeys) {
fk.validate();
}
for (Check c : checks) {
c.validate();
}
for (Index i : indexes) {
i.validate();
}
@ -629,12 +652,12 @@ public class Schema {
public PrimaryKey addPrimaryKey(String name, String... columnNames) {
if (primaryKey != null) {
throw new RuntimeException("Only one primary key is allowed. For composite keys use"
throw new IllegalArgumentException("Only one primary key is allowed. For composite keys use"
+ " addPrimaryKey(name, c1, c2, ...).");
}
for (Column c : columns) {
if (c.name.equalsIgnoreCase(name)) {
throw new RuntimeException("For table: " + this.name + " primary key name should not be a column name: " + name);
throw new IllegalArgumentException("For table: " + this.name + " primary key name should not be a column name: " + name);
}
}
primaryKey = new PrimaryKey(name, columnNames);
@ -671,18 +694,20 @@ public class Schema {
}
public class PrimaryKey {
private final String name;
private final List<String> columnNames = new ArrayList<>();
private final List<String> columnNames;
public PrimaryKey(String name, String[] columnNames) {
this.name = toName(name);
this.columnNames = new ArrayList<>();
for (String s : columnNames) {
this.columnNames.add(toName(s));
}
}
public void validate() {
}
public Table table() {
@ -692,18 +717,20 @@ public class Schema {
}
public class Unique {
private final String name;
private final List<String> columnNames = new ArrayList<>();
private final List<String> columnNames;
public Unique(String name, String[] columnNames) {
this.name = toName(name);
this.columnNames = new ArrayList<>();
for (String s : columnNames) {
this.columnNames.add(toName(s));
}
}
public void validate() {
}
public Table table() {
@ -713,13 +740,18 @@ public class Schema {
}
public class ForeignKey {
private final String name;
private final List<String> columnNames = new ArrayList<>();
public String foreignTable;
private boolean onDeleteCascade = false;
private final List<String> columnNames;
private String foreignTable;
private boolean onDeleteCascade;
public ForeignKey(String name, String[] columnNames) {
this.name = toName(name);
this.columnNames = new ArrayList<>();
for (String s : columnNames) {
this.columnNames.add(toName(s));
}
@ -737,7 +769,7 @@ public class Schema {
private void validate() {
if (foreignTable == null) {
throw new RuntimeException("Foreign key " + name + " must reference a table");
throw new IllegalArgumentException("Foreign key " + name + " must reference a table");
}
}
@ -748,7 +780,9 @@ public class Schema {
}
public class Check {
private final String name;
private final String expression;
public Check(String name, String expression) {
@ -758,7 +792,7 @@ public class Schema {
private void validate() {
if (expression == null) {
throw new RuntimeException("Expression needed for check constraint " + name + " on table " + Table.this.name);
throw new IllegalArgumentException("Expression needed for check constraint " + name + " on table " + Table.this.name);
}
}
@ -769,12 +803,16 @@ public class Schema {
}
public class Index {
private final String name;
private final List<String> columnNames = new ArrayList<>();
private final List<String> columnNames;
private boolean unique;
public Index(String name, String[] columnNames) {
this.name = toName(name);
this.columnNames = new ArrayList<>();
for (String s : columnNames) {
this.columnNames.add(toName(s));
}
@ -787,7 +825,7 @@ public class Schema {
private void validate() {
if (columnNames.size() < 1) {
throw new RuntimeException("Index " + name + " needs at least one column");
throw new IllegalArgumentException("Index " + name + " needs at least one column");
}
}
@ -798,11 +836,17 @@ public class Schema {
}
public class Column {
private final String name;
private ColumnType type;
private int scale;
private int precision;
private boolean notNull;
private String comment;
public Column(String name) {
@ -886,7 +930,7 @@ public class Schema {
private void validate() {
if (type == null) {
throw new RuntimeException("Call as*() on column " + name + " table " + Table.this.name);
throw new IllegalArgumentException("Call as*() on column " + name + " table " + Table.this.name);
}
}

@ -1,8 +1,5 @@
package org.xbib.jdbc.query;
import org.xbib.jdbc.query.flavor.Derby;
import org.xbib.jdbc.query.flavor.SqlServer;
import java.util.Objects;
/**
@ -17,21 +14,42 @@ public class When {
public When(Flavor actualFlavor) {
this.actualFlavor = actualFlavor;
}
public When oracle(String sql) {
if ("oracle".equals(actualFlavor.getName())) {
public When derby(String sql) {
if ("derby".equals(actualFlavor.getName())) {
chosen = sql;
}
return this;
}
public When derby(String sql) {
if (actualFlavor instanceof Derby) {
public When h2(String sql) {
if ("h2".equals(actualFlavor.getName())) {
chosen = sql;
}
return this;
}
public When hsqldb(String sql) {
if ("hsqldb".equals(actualFlavor.getName())) {
chosen = sql;
}
return this;
}
public When mariadb(String sql) {
if ("mariadb".equals(actualFlavor.getName())) {
chosen = sql;
}
return this;
}
public When oracle(String sql) {
if ("oracle".equals(actualFlavor.getName())) {
chosen = sql;
}
return this;
}
public When postgres(String sql) {
if ("postgresql".equals(actualFlavor.getName())) {
chosen = sql;
@ -40,12 +58,11 @@ public class When {
}
public When sqlserver(String sql) {
if (actualFlavor instanceof SqlServer) {
if ("sqlserver".equals(actualFlavor.getName())) {
chosen = sql;
}
return this;
}
public String other(String sql) {
if (chosen == null) {

@ -23,8 +23,16 @@ public class Derby implements Flavor {
}
@Override
public boolean isNormalizedUpperCase() {
return true;
public String normalizeTableName(String tableName) {
if (tableName == null) {
return tableName;
}
if (tableName.length() > 2) {
if (tableName.startsWith("\"") && tableName.endsWith("\"")) {
return tableName.substring(1, tableName.length() - 1);
}
}
return tableName.toUpperCase();
}
@Override
@ -92,6 +100,11 @@ public class Derby implements Flavor {
return "timestamp";
}
@Override
public String columnTypeLocalDateTime() {
return "timestamp";
}
@Override
public String typeLocalDate() {
return "date";

@ -23,8 +23,16 @@ public class H2 implements Flavor {
}
@Override
public boolean isNormalizedUpperCase() {
return true;
public String normalizeTableName(String tableName) {
if (tableName == null) {
return tableName;
}
if (tableName.length() > 2) {
if (tableName.startsWith("\"") && tableName.endsWith("\"")) {
return tableName.substring(1, tableName.length() - 1);
}
}
return tableName.toUpperCase();
}
@Override
@ -92,6 +100,11 @@ public class H2 implements Flavor {
return "timestamp(3)";
}
@Override
public String columnTypeLocalDateTime() {
return "timestamp";
}
@Override
public String typeLocalDate() {
return "date";

@ -23,8 +23,16 @@ public class Hsql implements Flavor {
}
@Override
public boolean isNormalizedUpperCase() {
return true;
public String normalizeTableName(String tableName) {
if (tableName == null) {
return tableName;
}
if (tableName.length() > 2) {
if (tableName.startsWith("\"") && tableName.endsWith("\"")) {
return tableName.substring(1, tableName.length() - 1);
}
}
return tableName.toUpperCase();
}
@Override
@ -92,6 +100,11 @@ public class Hsql implements Flavor {
return "timestamp with time zone";
}
@Override
public String columnTypeLocalDateTime() {
return "TIMESTAMP WITH TIME ZONE";
}
@Override
public String typeLocalDate() {
return "date";

@ -1,4 +1,3 @@
org.xbib.jdbc.query.flavor.Derby
org.xbib.jdbc.query.flavor.Hsql
org.xbib.jdbc.query.flavor.H2
org.xbib.jdbc.query.flavor.SqlServer
org.xbib.jdbc.query.flavor.H2

@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test;
import org.xbib.jdbc.query.DatabaseProvider;
import org.xbib.jdbc.query.OptionsOverride;
import org.xbib.jdbc.query.Schema;
import org.xbib.jdbc.test.CommonTest;
import java.math.BigDecimal;
import java.util.logging.Logger;
@ -15,7 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Exercise Database functionality with a real database (Derby).
*/
@Disabled
@Disabled("Derby 10.16.1.1 does not conform to JDBC 4.2 with getObject(... LocalDatetime.class) https://chariotsolutions.com/blog/post/using-java-time-resultset-preparedstatement/")
public class DerbyTest extends CommonTest {
private static final Logger logger = Logger.getLogger(DerbyTest.class.getName());
@ -26,12 +27,10 @@ public class DerbyTest extends CommonTest {
@Override
protected DatabaseProvider createDatabaseProvider(OptionsOverride options) {
return DatabaseProvider.builder("jdbc:derby:build/testdb;create=true")
return DatabaseProvider.builder(getClass().getClassLoader(), "jdbc:derby:build/testdb;create=true")
.withSqlParameterLogging().withSqlInExceptionMessages().withOptions(options).build();
}
// TODO fix this test
@Disabled("Not sure why this fails on the build servers right now...")
@Test
public void clockSync() {
super.clockSync();
@ -79,7 +78,6 @@ public class DerbyTest extends CommonTest {
super.intervals();
}
@Disabled("Derby limits out at precision 31")
@Test
public void argBigDecimal38Precision0() {

@ -10,6 +10,7 @@ import org.xbib.jdbc.query.OptionsOverride;
import org.xbib.jdbc.query.Schema;
import org.xbib.jdbc.query.Sql;
import org.xbib.jdbc.query.SqlArgs;
import org.xbib.jdbc.test.CommonTest;
import java.math.BigDecimal;
import java.util.ArrayList;
@ -38,25 +39,6 @@ public class HsqldbTest extends CommonTest {
.build();
}
@Test
public void noDatabaseAccess() throws Exception {
DatabaseProvider provider = createDatabaseProvider(new OptionsOverride());
provider.transact(dbp -> {
// Do nothing, just making sure no exception is thrown
});
provider.transact((dbp, tx) -> {
// Do nothing, just making sure no exception is thrown
});
provider.transact((dbp, tx) -> {
tx.setRollbackOnError(true);
// Do nothing, just making sure no exception is thrown
});
provider.transact((dbp, tx) -> {
tx.setRollbackOnly(true);
// Do nothing, just making sure no exception is thrown
});
}
@Disabled("LocalDate implementations should be TimeZone agnostic, but HSQLDB implementation has a bug.")
@Test
public void argLocalDateTimeZones() {
@ -85,7 +67,6 @@ public class HsqldbTest extends CommonTest {
.addColumn("boolean_flag").asBoolean().table()
.addColumn("date_millis").asLocalDateTime().table()
.addColumn("local_date").asLocalDate().schema().execute(db);
db.toInsert("insert into dbtest (nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar,"
+ " str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date) values (?,?,?,?,?,?,?,?,?,?,?,?)")
.argInteger(Integer.MAX_VALUE)

@ -2,6 +2,7 @@ package org.xbib.jdbc.query.test.example;
import org.xbib.jdbc.query.Database;
import org.xbib.jdbc.query.DatabaseProvider;
import org.xbib.jdbc.query.DatabaseProviderBuilder;
/**
* Demo of using some com.github.susom.database classes with Derby.
@ -12,7 +13,7 @@ public abstract class DerbyExample {
// For subclasses to override
}
void example(DatabaseProvider.DatabaseProviderBuilder dbb, final String[] args) {
void example(DatabaseProviderBuilder dbb, final String[] args) {
dbb.transact(db -> {
example(db.get(), args);
});
@ -26,7 +27,7 @@ public abstract class DerbyExample {
try {
System.setProperty("derby.stream.error.file", "java.lang.System.err");
String url = "jdbc:derby:target/testdb;create=true";
example(DatabaseProvider.builder(url), args);
example(DatabaseProvider.builder(getClass().getClassLoader(), url), args);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);

@ -1,47 +0,0 @@
package org.xbib.jdbc.query.test.example;
import org.xbib.jdbc.query.Database;
import org.xbib.jdbc.query.DatabaseProvider;
/**
* Example with database info provided from command line. To use this, set properties like this:
* <br/>
* <pre>
* -Ddatabase.url=... Database connect string (required)
* -Ddatabase.user=... Authenticate as this user (optional if provided in url)
* -Ddatabase.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* -Ddatabase.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* -Ddatabase.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
*/
public class HelloAny {
public static void main(final String[] args) {
try {
new HelloAny().run();
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
public void run() {
DatabaseProvider.fromSystemProperties().transact(dbp -> {
Database db = dbp.get();
db.dropTableQuietly("t");
db.ddl("create table t (a numeric)").execute();
db.toInsert("insert into t (a) values (?)")
.argInteger(32)
.insert(1);
db.toUpdate("update t set a=:val")
.argInteger("val", 23)
.update(1);
Long rows = db.toSelect("select count(1) from t ").queryLongOrNull();
System.out.println("Rows: " + rows);
});
}
}

@ -5,9 +5,6 @@ import org.xbib.jdbc.query.DatabaseProvider;
import java.io.File;
/**
* Demo of using some com.github.susom.database classes with Derby.
*/
public class HelloDerby {
public static void main(final String[] args) {
try {
@ -19,14 +16,12 @@ public class HelloDerby {
}
public void run() {
// Put all Derby related files inside ./build to keep our working copy clean
File directory = new File("target").getAbsoluteFile();
File directory = new File("build").getAbsoluteFile();
if (directory.exists() || directory.mkdirs()) {
System.setProperty("derby.stream.error.file", new File(directory, "derby.log").getAbsolutePath());
}
String url = "jdbc:derby:target/testdb;create=true";
DatabaseProvider.builder(url).transact(dbp -> {
DatabaseProvider.builder(getClass().getClassLoader(), url).transact(dbp -> {
Database db = dbp.get();
db.ddl("drop table t").executeQuietly();
db.ddl("create table t (a numeric)").execute();

@ -0,0 +1,10 @@
dependencies {
api project(':jdbc-query')
testImplementation project(':jdbc-test')
testImplementation libs.testcontainers
testImplementation libs.testcontainers.junit.jupiter
}
test {
systemProperty 'user.timezone', 'GMT'
}

@ -0,0 +1,10 @@
import org.xbib.jdbc.query.Flavor;
import org.xbib.jdbc.sqlserver.SqlServer;
module org.xbib.jdbc.sqlserver {
requires org.xbib.jdbc.query;
requires java.sql;
uses Flavor;
exports org.xbib.jdbc.sqlserver;
provides Flavor with SqlServer;
}

@ -1,4 +1,4 @@
package org.xbib.jdbc.query.flavor;
package org.xbib.jdbc.sqlserver;
import org.xbib.jdbc.query.Flavor;
@ -7,6 +7,9 @@ import java.sql.SQLException;
public class SqlServer implements Flavor {
public SqlServer() {
}
@Override
public String getName() {
return "sqlServer";
@ -23,8 +26,16 @@ public class SqlServer implements Flavor {
}
@Override
public boolean isNormalizedUpperCase() {
return false;
public String normalizeTableName(String tableName) {
if (tableName == null) {
return tableName;
}
if (tableName.length() > 2) {
if (tableName.startsWith("\"") && tableName.endsWith("\"")) {
return tableName.substring(1, tableName.length() - 1);
}
}
return tableName;
}
@Override
@ -72,6 +83,11 @@ public class SqlServer implements Flavor {
return "datetime2(3)";
}
@Override
public String columnTypeLocalDateTime() {
return "datetime2";
}
@Override
public String typeLocalDate() {
return "date";

@ -1,4 +1,4 @@
package org.xbib.jdbc.query.test;
package org.xbib.jdbc.sqlserver.test;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@ -7,6 +7,7 @@ import org.xbib.jdbc.query.ConfigSupplier;
import org.xbib.jdbc.query.DatabaseProvider;
import org.xbib.jdbc.query.OptionsOverride;
import org.xbib.jdbc.query.Schema;
import org.xbib.jdbc.test.CommonTest;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
@ -77,9 +78,7 @@ public class SqlServerTest extends CommonTest {
@Test
public void metadataColumnNames() {
db.dropTableQuietly("dbtest");
new Schema().addTable("dbtest").addColumn("pk").primaryKey().schema().execute(db);
db.toSelect("select Pk, Pk as Foo, Pk as \"Foo\" from dbtest").query(rs -> {
assertArrayEquals(new String[]{"Pk", "Foo", "Foo"}, rs.getColumnLabels());
return null;

@ -0,0 +1,10 @@
handlers=java.util.logging.FileHandler, java.util.logging.ConsoleHandler
.level=ALL
java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %5$s %6$s%n
java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.FileHandler.level=ALL
java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.FileHandler.pattern=build/database.log
jdk.event.security.level=INFO
javax.management.level=INFO

@ -36,6 +36,7 @@ import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -86,6 +87,13 @@ public abstract class CommonTest {
}
}
@Test
public void testJdbcVersion() {
// we want JDBC >= 4.2
assertEquals(4, db.jdbcMajorVersion());
assertTrue(db.jdbcMinorVersion() >= 2);
}
@Test
public void tableExists() {
// Verify dbtest table does not exist
@ -111,22 +119,13 @@ public abstract class CommonTest {
@Test
public void normalizeTableName() {
// Verify that null and empty cases are handled gracefully
assertNull(db.normalizeTableName(null));
assertEquals("", db.normalizeTableName(""));
// Verify a quoted table name is returned in exactly the same case, with quotes removed.
assertNull(db.flavor().normalizeTableName(null));
assertEquals("", db.flavor().normalizeTableName(""));
String camelCaseTableName = "\"DbTest\"";
assertEquals(camelCaseTableName.substring(1, camelCaseTableName.length() - 1),
db.normalizeTableName(camelCaseTableName));
// Verify that the database flavor gets the expected normalized case
boolean isUpperCase = db.flavor().isNormalizedUpperCase();
if (isUpperCase) {
assertEquals(TEST_TABLE_NAME.toUpperCase(), db.normalizeTableName(TEST_TABLE_NAME));
} else {
assertEquals(TEST_TABLE_NAME.toLowerCase(), db.normalizeTableName(TEST_TABLE_NAME));
}
db.flavor().normalizeTableName(camelCaseTableName));
assertEquals(TEST_TABLE_NAME.toUpperCase(Locale.ENGLISH),
db.flavor().normalizeTableName(TEST_TABLE_NAME).toUpperCase(Locale.ENGLISH));
}
@Test
@ -144,12 +143,10 @@ public abstract class CommonTest {
.addColumn("bin_blob").asBlob().table()
.addColumn("date_millis").asLocalDateTime().table()
.addColumn("local_date").asLocalDate().table().schema().execute(db);
BigDecimal bigDecimal = new BigDecimal("5.3");
db.toInsert("insert into dbtest values (?,?,?,?,?,?,?,?,?,?,?)").argInteger(1).argLong(2L).argFloat(3.2f).argDouble(4.2)
.argBigDecimal(bigDecimal).argString("Hello").argString("T").argClobString("World")
.argBlobBytes("More".getBytes()).argLocalDateTime(now).argLocalDate(localDateNow).insert(1);
db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar, str_fixed, str_lob, "
+ "bin_blob, date_millis, local_date from dbtest")
.query((RowsHandler<Void>) rs -> {
@ -594,20 +591,15 @@ public abstract class CommonTest {
new Schema()
.addTable("dbtest")
.addColumn(timestampColumnName).asLocalDateTime().table()
.addColumn(dateColumnName).asLocalDate().table().schema().execute(db);
.addColumn(dateColumnName).asLocalDate().table()
.schema().execute(db);
db.toSelect("select * from dbtest").query((RowsHandler<Void>) rs -> {
ResultSetMetaData metadata = rs.getMetadata();
for (int i = 1; i <= metadata.getColumnCount(); i++) {
String columnName = metadata.getColumnName(i);
String columnType = metadata.getColumnTypeName(i);
if (columnName.equalsIgnoreCase(timestampColumnName)) {
if ("sqlserver".equals(db.flavor().getName())) {
assertEquals("DATETIME2", columnType.toUpperCase());
} else if ("hsqldb".equals(db.flavor().getName())) {
assertEquals("TIMESTAMP WITH TIME ZONE", columnType.toUpperCase());
} else {
assertEquals("TIMESTAMP", columnType.toUpperCase());
}
assertEquals(db.flavor().columnTypeLocalDateTime(), columnType);
} else if (columnName.equalsIgnoreCase(dateColumnName)) {
assertEquals("DATE", columnType.toUpperCase());
} else {

@ -31,3 +31,4 @@ include 'jdbc-test'
include 'jdbc-mariadb'
include 'jdbc-oracle'
include 'jdbc-postgresql'
include 'jdbc-sqlserver'

Loading…
Cancel
Save