Java tutorial
/* // Licensed to Julian Hyde under one or more contributor license // agreements. See the NOTICE file distributed with this work for // additional information regarding copyright ownership. // // Julian Hyde licenses this file to you under the Apache License, // Version 2.0 (the "License"); you may not use this file except in // compliance with the License. You may obtain a copy of the License at: // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. */ package net.hydromatic.optiq.test; import net.hydromatic.linq4j.function.Function1; import net.hydromatic.optiq.DataContext; import net.hydromatic.optiq.SchemaPlus; import net.hydromatic.optiq.impl.clone.CloneSchema; import net.hydromatic.optiq.impl.java.ReflectiveSchema; import net.hydromatic.optiq.impl.jdbc.JdbcSchema; import net.hydromatic.optiq.jdbc.MetaImpl; import net.hydromatic.optiq.jdbc.OptiqConnection; import net.hydromatic.optiq.runtime.Hook; import org.eigenbase.sql.SqlDialect; import org.eigenbase.util.*; import com.google.common.collect.ImmutableMultiset; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.*; import java.sql.*; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; import javax.sql.DataSource; import static org.junit.Assert.*; /** * Fluid DSL for testing Optiq connections and queries. */ public class OptiqAssert { private OptiqAssert() { } /** Which database to use for tests that require a JDBC data source. By * default the test suite runs against the embedded hsqldb database. * * <p>We recommend that casual users use hsqldb, and frequent Optiq developers * use MySQL. The test suite runs faster against the MySQL database (mainly * because of the 0.1s versus 6s startup time). You have to populate MySQL * manually with the foodmart data set, otherwise there will be test failures. * To run against MySQL, specify '-Doptiq.test.db=mysql' on the java command * line.</p> */ public static final ConnectionSpec CONNECTION_SPEC = Util.first(System.getProperty("optiq.test.db"), "hsqldb") .equals("mysql") ? ConnectionSpec.MYSQL : ConnectionSpec.HSQLDB; private static final DateFormat UTC_DATE_FORMAT; private static final DateFormat UTC_TIME_FORMAT; private static final DateFormat UTC_TIMESTAMP_FORMAT; static { final TimeZone utc = TimeZone.getTimeZone("UTC"); UTC_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); UTC_DATE_FORMAT.setTimeZone(utc); UTC_TIME_FORMAT = new SimpleDateFormat("HH:mm:ss"); UTC_TIME_FORMAT.setTimeZone(utc); UTC_TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); UTC_TIMESTAMP_FORMAT.setTimeZone(utc); } /** Implementation of {@link AssertThat} that does nothing. */ private static final AssertThat DISABLED = new AssertThat((Config) null) { @Override public AssertThat with(Config config) { return this; } @Override public AssertThat with(ConnectionFactory connectionFactory) { return this; } @Override public AssertThat with(Map<String, String> map) { return this; } @Override public AssertThat with(String name, Object schema) { return this; } @Override public AssertThat withModel(String model) { return this; } @Override public AssertQuery query(String sql) { return NopAssertQuery.of(sql); } @Override public void connectThrows(String message) { // nothing } @Override public void connectThrows(Function1<Throwable, Void> exceptionChecker) { // nothing } @Override public <T> AssertThat doWithConnection(Function1<OptiqConnection, T> fn) throws Exception { return this; } @Override public AssertThat withSchema(String schema) { return this; } @Override public AssertThat enable(boolean enabled) { return this; } }; /** Creates an instance of {@code OptiqAssert} with the regular * configuration. */ public static AssertThat that() { return new AssertThat(Config.REGULAR); } static Function1<Throwable, Void> checkException(final String expected) { return new Function1<Throwable, Void>() { public Void apply(Throwable p0) { assertNotNull("expected exception but none was thrown", p0); StringWriter stringWriter = new StringWriter(); PrintWriter printWriter = new PrintWriter(stringWriter); p0.printStackTrace(printWriter); printWriter.flush(); String stack = stringWriter.toString(); assertTrue(stack, stack.contains(expected)); return null; } }; } static Function1<ResultSet, Void> checkResult(final String expected) { return new Function1<ResultSet, Void>() { public Void apply(ResultSet resultSet) { try { final String resultString = OptiqAssert.toString(resultSet); assertEquals(expected, resultString); return null; } catch (SQLException e) { throw new RuntimeException(e); } } }; } static Function1<ResultSet, Void> checkResultCount(final int expected) { return new Function1<ResultSet, Void>() { public Void apply(ResultSet resultSet) { try { final int count = OptiqAssert.countRows(resultSet); assertEquals(expected, count); return null; } catch (SQLException e) { throw new RuntimeException(e); } } }; } /** Checks that the result of the second and subsequent executions is the same * as the first. * * @param ordered Whether order should be the same both times */ static Function1<ResultSet, Void> consistentResult(final boolean ordered) { return new Function1<ResultSet, Void>() { int executeCount = 0; Collection expected; public Void apply(ResultSet resultSet) { ++executeCount; try { final Collection result = OptiqAssert.toStringList(resultSet, ordered ? new ArrayList<String>() : new TreeSet<String>()); if (executeCount == 1) { expected = result; } else { if (!expected.equals(result)) { // compare strings to get better error message assertEquals(newlineList(expected), newlineList(result)); fail("oops"); } } return null; } catch (SQLException e) { throw new RuntimeException(e); } } }; } static String newlineList(Collection collection) { final StringBuilder buf = new StringBuilder(); for (Object o : collection) { buf.append(o).append('\n'); } return buf.toString(); } static Function1<ResultSet, Void> checkResultUnordered(final String... lines) { return new Function1<ResultSet, Void>() { public Void apply(ResultSet resultSet) { try { final Collection<String> actualSet = new TreeSet<String>(); OptiqAssert.toStringList(resultSet, actualSet); final TreeSet<String> expectedSet = new TreeSet<String>(Arrays.asList(lines)); assertEquals(expectedSet, actualSet); return null; } catch (SQLException e) { throw new RuntimeException(e); } } }; } public static Function1<ResultSet, Void> checkResultContains(final String expected) { return new Function1<ResultSet, Void>() { public Void apply(ResultSet s) { try { final String actual = OptiqAssert.toString(s); if (!actual.contains(expected)) { assertEquals("contains", expected, actual); } return null; } catch (SQLException e) { throw new RuntimeException(e); } } }; } public static Function1<ResultSet, Void> checkResultType(final String expected) { return new Function1<ResultSet, Void>() { public Void apply(ResultSet s) { try { final String actual = typeString(s.getMetaData()); assertEquals(expected, actual); return null; } catch (SQLException e) { throw new RuntimeException(e); } } }; } private static String typeString(ResultSetMetaData metaData) throws SQLException { final List<String> list = new ArrayList<String>(); for (int i = 0; i < metaData.getColumnCount(); i++) { list.add(metaData.getColumnName(i + 1) + " " + metaData.getColumnTypeName(i + 1) + (metaData.isNullable(i + 1) == ResultSetMetaData.columnNoNulls ? " NOT NULL" : "")); } return list.toString(); } static void assertQuery(Connection connection, String sql, int limit, boolean materializationsEnabled, Function1<ResultSet, Void> resultChecker, Function1<Throwable, Void> exceptionChecker) throws Exception { final String message = "With materializationsEnabled=" + materializationsEnabled + ", limit=" + limit; try { ((OptiqConnection) connection).getProperties().setProperty("materializationsEnabled", Boolean.toString(materializationsEnabled)); Statement statement = connection.createStatement(); statement.setMaxRows(limit <= 0 ? limit : Math.max(limit, 1)); ResultSet resultSet; try { resultSet = statement.executeQuery(sql); if (exceptionChecker != null) { exceptionChecker.apply(null); return; } } catch (Exception e) { if (exceptionChecker != null) { exceptionChecker.apply(e); return; } throw e; } catch (Error e) { if (exceptionChecker != null) { exceptionChecker.apply(e); return; } throw e; } if (resultChecker != null) { resultChecker.apply(resultSet); } resultSet.close(); statement.close(); connection.close(); } catch (Throwable e) { throw new RuntimeException(message, e); } } static String toString(ResultSet resultSet) throws SQLException { final StringBuilder buf = new StringBuilder(); final ResultSetMetaData metaData = resultSet.getMetaData(); while (resultSet.next()) { int n = metaData.getColumnCount(); if (n > 0) { for (int i = 1;; i++) { buf.append(metaData.getColumnLabel(i)).append("=").append(resultSet.getString(i)); if (i == n) { break; } buf.append("; "); } } buf.append("\n"); } return buf.toString(); } static int countRows(ResultSet resultSet) throws SQLException { int n = 0; while (resultSet.next()) { ++n; } return n; } static Collection<String> toStringList(ResultSet resultSet, Collection<String> list) throws SQLException { final StringBuilder buf = new StringBuilder(); while (resultSet.next()) { int n = resultSet.getMetaData().getColumnCount(); if (n > 0) { for (int i = 1;; i++) { buf.append(resultSet.getMetaData().getColumnLabel(i)).append("=") .append(resultSet.getString(i)); if (i == n) { break; } buf.append("; "); } } list.add(buf.toString()); buf.setLength(0); } return list; } static ImmutableMultiset<String> toSet(ResultSet resultSet) throws SQLException { return ImmutableMultiset.copyOf(toStringList(resultSet, new ArrayList<String>())); } /** Calls a non-static method via reflection. Useful for testing methods that * don't exist in certain versions of the JDK. */ static Object call(Object o, String methodName, Object... args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { return method(o, methodName, args).invoke(o, args); } /** Finds a non-static method based on its target, name and arguments. * Throws if not found. */ static Method method(Object o, String methodName, Object[] args) { for (Class<?> aClass = o.getClass();;) { loop: for (Method method1 : aClass.getMethods()) { if (method1.getName().equals(methodName) && method1.getParameterTypes().length == args.length && Modifier.isPublic(method1.getDeclaringClass().getModifiers())) { for (Pair<Object, Class<?>> pair : Pair.zip(args, method1.getParameterTypes())) { if (!pair.right.isInstance(pair.left)) { continue loop; } } return method1; } } if (aClass.getSuperclass() != null && aClass.getSuperclass() != Object.class) { aClass = aClass.getSuperclass(); } else { final Class<?>[] interfaces = aClass.getInterfaces(); if (interfaces.length > 0) { aClass = interfaces[0]; } else { break; } } } throw new AssertionError("method " + methodName + " not found"); } static OptiqConnection getConnection(String... schema) throws ClassNotFoundException, SQLException { final List<String> schemaList = Arrays.asList(schema); Class.forName("net.hydromatic.optiq.jdbc.Driver"); String suffix = schemaList.contains("spark") ? "spark=true" : ""; Connection connection = DriverManager.getConnection("jdbc:optiq:" + suffix); OptiqConnection optiqConnection = connection.unwrap(OptiqConnection.class); SchemaPlus rootSchema = optiqConnection.getRootSchema(); if (schemaList.contains("hr")) { rootSchema.add(new ReflectiveSchema(rootSchema, "hr", new JdbcTest.HrSchema())); } if (schemaList.contains("foodmart")) { rootSchema.add(new ReflectiveSchema(rootSchema, "foodmart", new JdbcTest.FoodmartSchema())); } if (schemaList.contains("lingual")) { rootSchema.add(new ReflectiveSchema(rootSchema, "SALES", new JdbcTest.LingualSchema())); } if (schemaList.contains("metadata")) { // always present Util.discard(0); } return optiqConnection; } /** * Creates a connection with a given query provider. If provider is null, * uses the connection as its own provider. The connection contains a * schema called "foodmart" backed by a JDBC connection to MySQL. * * @param withClone Whether to create a "foodmart2" schema as in-memory * clone * @return Connection * @throws ClassNotFoundException * @throws java.sql.SQLException */ static OptiqConnection getConnection(boolean withClone) throws ClassNotFoundException, SQLException { Class.forName("net.hydromatic.optiq.jdbc.Driver"); Connection connection = DriverManager.getConnection("jdbc:optiq:"); OptiqConnection optiqConnection = connection.unwrap(OptiqConnection.class); final SchemaPlus rootSchema = optiqConnection.getRootSchema(); final DataSource dataSource = JdbcSchema.dataSource(CONNECTION_SPEC.url, CONNECTION_SPEC.driver, CONNECTION_SPEC.username, CONNECTION_SPEC.password); final SqlDialect dialect = JdbcSchema.createDialect(dataSource); final SchemaPlus foodmart = rootSchema .add(new JdbcSchema(rootSchema, "foodmart", dataSource, dialect, null, "foodmart")); if (withClone) { CloneSchema schema = new CloneSchema(rootSchema, "foodmart2", foodmart); rootSchema.add(schema); } optiqConnection.setSchema("foodmart2"); return optiqConnection; } /** * Result of calling {@link OptiqAssert#that}. */ public static class AssertThat { private final ConnectionFactory connectionFactory; private AssertThat(Config config) { this(new ConfigConnectionFactory(config)); } private AssertThat(ConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; } public AssertThat with(Config config) { return new AssertThat(config); } public AssertThat with(ConnectionFactory connectionFactory) { return new AssertThat(connectionFactory); } public AssertThat with(final Map<String, String> map) { return new AssertThat(new ConnectionFactory() { public OptiqConnection createConnection() throws Exception { Class.forName("net.hydromatic.optiq.jdbc.Driver"); final Properties info = new Properties(); for (Map.Entry<String, String> entry : map.entrySet()) { info.setProperty(entry.getKey(), entry.getValue()); } return (OptiqConnection) DriverManager.getConnection("jdbc:optiq:", info); } }); } /** Sets the default schema to a reflective schema based on a given * object. */ public AssertThat with(final String name, final Object schema) { return with(new OptiqAssert.ConnectionFactory() { public OptiqConnection createConnection() throws Exception { Class.forName("net.hydromatic.optiq.jdbc.Driver"); Connection connection = DriverManager.getConnection("jdbc:optiq:"); OptiqConnection optiqConnection = connection.unwrap(OptiqConnection.class); SchemaPlus rootSchema = optiqConnection.getRootSchema(); rootSchema.add(new ReflectiveSchema(rootSchema, name, schema)); optiqConnection.setSchema(name); return optiqConnection; } }); } public AssertThat withModel(final String model) { return new AssertThat(new OptiqAssert.ConnectionFactory() { public OptiqConnection createConnection() throws Exception { Class.forName("net.hydromatic.optiq.jdbc.Driver"); final Properties info = new Properties(); info.setProperty("model", "inline:" + model); return (OptiqConnection) DriverManager.getConnection("jdbc:optiq:", info); } }); } /** Adds materializations to the schema. */ public AssertThat withMaterializations(String model, String... materializations) { assert materializations.length % 2 == 0; final JsonBuilder builder = new JsonBuilder(); final List<Object> list = builder.list(); for (int i = 0; i < materializations.length; i++) { String table = materializations[i++]; final Map<String, Object> map = builder.map(); map.put("table", table); map.put("view", table + "v"); String sql = materializations[i]; final String sql2 = sql.replaceAll("`", "\""); map.put("sql", sql2); list.add(map); } final String buf = "materializations: " + builder.toJsonString(list); final String model2; if (model.contains("defaultSchema: 'foodmart'")) { model2 = model.replace("]", ", { name: 'mat', " + buf + "}\n" + "]"); } else if (model.contains("type: ")) { model2 = model.replace("type: ", buf + ",\n" + "type: "); } else { throw new AssertionError("do not know where to splice"); } return withModel(model2); } public AssertQuery query(String sql) { return new AssertQuery(connectionFactory, sql); } /** Asserts that there is an exception with the given message while * creating a connection. */ public void connectThrows(String message) { connectThrows(checkException(message)); } /** Asserts that there is an exception that matches the given predicate * while creating a connection. */ public void connectThrows(Function1<Throwable, Void> exceptionChecker) { Throwable throwable; try { Connection x = connectionFactory.createConnection(); try { x.close(); } catch (SQLException e) { // ignore } throwable = null; } catch (Throwable e) { throwable = e; } exceptionChecker.apply(throwable); } /** Creates a {@link OptiqConnection} and executes a callback. */ public <T> AssertThat doWithConnection(Function1<OptiqConnection, T> fn) throws Exception { Connection connection = connectionFactory.createConnection(); try { T t = fn.apply((OptiqConnection) connection); Util.discard(t); return AssertThat.this; } finally { connection.close(); } } /** Creates a {@link DataContext} and executes a callback. */ public <T> AssertThat doWithDataContext(Function1<DataContext, T> fn) throws Exception { OptiqConnection connection = connectionFactory.createConnection(); final DataContext dataContext = MetaImpl.createDataContext(connection); try { T t = fn.apply(dataContext); Util.discard(t); return AssertThat.this; } finally { connection.close(); } } public AssertThat withSchema(String schema) { return new AssertThat(new SchemaConnectionFactory(connectionFactory, schema)); } public AssertThat enable(boolean enabled) { return enabled ? this : DISABLED; } } public interface ConnectionFactory { OptiqConnection createConnection() throws Exception; } private static class ConfigConnectionFactory implements ConnectionFactory { private final Config config; public ConfigConnectionFactory(Config config) { this.config = config; } public OptiqConnection createConnection() throws Exception { switch (config) { case REGULAR: return getConnection("hr", "foodmart"); case REGULAR_PLUS_METADATA: return getConnection("hr", "foodmart", "metadata"); case LINGUAL: return getConnection("lingual"); case JDBC_FOODMART: return getConnection(false); case FOODMART_CLONE: return getConnection(true); case SPARK: return getConnection("spark"); default: throw Util.unexpected(config); } } } private static class DelegatingConnectionFactory implements ConnectionFactory { private final ConnectionFactory factory; public DelegatingConnectionFactory(ConnectionFactory factory) { this.factory = factory; } public OptiqConnection createConnection() throws Exception { return factory.createConnection(); } } private static class SchemaConnectionFactory extends DelegatingConnectionFactory { private final String schema; public SchemaConnectionFactory(ConnectionFactory factory, String schema) { super(factory); this.schema = schema; } @Override public OptiqConnection createConnection() throws Exception { OptiqConnection connection = super.createConnection(); connection.setSchema(schema); return connection; } } public static class AssertQuery { private final String sql; private ConnectionFactory connectionFactory; private String plan; private int limit; private boolean materializationsEnabled = false; private AssertQuery(ConnectionFactory connectionFactory, String sql) { this.sql = sql; this.connectionFactory = connectionFactory; } protected Connection createConnection() throws Exception { return connectionFactory.createConnection(); } public AssertQuery enable(boolean enabled) { return enabled ? this : NopAssertQuery.of(sql); } public AssertQuery returns(String expected) { return returns(checkResult(expected)); } public AssertQuery returnsCount(int expectedCount) { return returns(checkResultCount(expectedCount)); } public AssertQuery returns(Function1<ResultSet, Void> checker) { try { assertQuery(createConnection(), sql, limit, materializationsEnabled, checker, null); return this; } catch (Exception e) { throw new RuntimeException("exception while executing [" + sql + "]", e); } } public AssertQuery returnsUnordered(String... lines) { return returns(checkResultUnordered(lines)); } public AssertQuery throws_(String message) { try { assertQuery(createConnection(), sql, limit, materializationsEnabled, null, checkException(message)); return this; } catch (Exception e) { throw new RuntimeException("exception while executing [" + sql + "]", e); } } public AssertQuery runs() { try { assertQuery(createConnection(), sql, limit, materializationsEnabled, null, null); return this; } catch (Exception e) { throw new RuntimeException("exception while executing [" + sql + "]", e); } } public AssertQuery typeIs(String expected) { try { assertQuery(createConnection(), sql, limit, false, checkResultType(expected), null); return this; } catch (Exception e) { throw new RuntimeException("exception while executing [" + sql + "]", e); } } public AssertQuery explainContains(String expected) { String explainSql = "explain plan for " + sql; try { assertQuery(createConnection(), explainSql, limit, materializationsEnabled, checkResultContains(expected), null); return this; } catch (Exception e) { throw new RuntimeException("exception while executing [" + explainSql + "]", e); } } public AssertQuery planContains(String expected) { ensurePlan(); assertTrue("Plan [" + plan + "] contains [" + expected + "]", plan.contains(expected)); return this; } public AssertQuery planHasSql(String expected) { return planContains("getDataSource(), \"" + expected.replace("\\", "\\\\").replace("\"", "\\\"").replaceAll("\n", "\\\\n") + "\""); } private void ensurePlan() { if (plan != null) { return; } final Hook.Closeable hook = Hook.JAVA_PLAN.addThread(new Function1<Object, Object>() { public Object apply(Object a0) { plan = (String) a0; return null; } }); try { assertQuery(createConnection(), sql, limit, materializationsEnabled, null, null); assertNotNull(plan); } catch (Exception e) { throw new RuntimeException("exception while executing [" + sql + "]", e); } finally { hook.close(); } } /** Runs the query and applies a checker to the generated third-party * queries. The checker should throw to fail the test if it does not see * what it wants. This method can be used to check whether a particular * MongoDB or SQL query is generated, for instance. */ public AssertQuery queryContains(Function1<List, Void> predicate1) { final List<Object> list = new ArrayList<Object>(); final Hook.Closeable hook = Hook.QUERY_PLAN.addThread(new Function1<Object, Object>() { public Object apply(Object a0) { list.add(a0); return null; } }); try { assertQuery(createConnection(), sql, limit, materializationsEnabled, null, null); predicate1.apply(list); return this; } catch (Exception e) { throw new RuntimeException("exception while executing [" + sql + "]", e); } finally { hook.close(); } } /** Sets a limit on the number of rows returned. -1 means no limit. */ public AssertQuery limit(int limit) { this.limit = limit; return this; } public void sameResultWithMaterializationsDisabled() { boolean save = materializationsEnabled; try { materializationsEnabled = false; final boolean ordered = sql.toUpperCase().contains("ORDER BY"); final Function1<ResultSet, Void> checker = consistentResult(ordered); returns(checker); materializationsEnabled = true; returns(checker); } finally { materializationsEnabled = save; } } public AssertQuery enableMaterializations(boolean enable) { this.materializationsEnabled = enable; return this; } } public enum Config { /** * Configuration that creates a connection with two in-memory data sets: * {@link net.hydromatic.optiq.test.JdbcTest.HrSchema} and * {@link net.hydromatic.optiq.test.JdbcTest.FoodmartSchema}. */ REGULAR, /** * Configuration that creates a connection with an in-memory data set * similar to the smoke test in Cascading Lingual. */ LINGUAL, /** * Configuration that creates a connection to a MySQL server. Tables * such as "customer" and "sales_fact_1997" are available. Queries * are processed by generating Java that calls linq4j operators * such as * {@link net.hydromatic.linq4j.Enumerable#where(net.hydromatic.linq4j.function.Predicate1)}. */ JDBC_FOODMART, /** Configuration that contains an in-memory clone of the FoodMart * database. */ FOODMART_CLONE, /** Configuration that includes the metadata schema. */ REGULAR_PLUS_METADATA, /** Configuration that loads Spark. */ SPARK, } /** Implementation of {@link AssertQuery} that does nothing. */ private static class NopAssertQuery extends AssertQuery { private NopAssertQuery(String sql) { super(null, sql); } /** Returns an implementation of {@link AssertQuery} that does nothing. */ static AssertQuery of(final String sql) { return new NopAssertQuery(sql); } @Override protected Connection createConnection() throws Exception { throw new AssertionError("disabled"); } @Override public AssertQuery returns(String expected) { return this; } @Override public AssertQuery returns(Function1<ResultSet, Void> checker) { return this; } @Override public AssertQuery throws_(String message) { return this; } @Override public AssertQuery runs() { return this; } @Override public AssertQuery explainContains(String expected) { return this; } @Override public AssertQuery planContains(String expected) { return this; } @Override public AssertQuery planHasSql(String expected) { return this; } } /** Information necessary to create a JDBC connection. Specify one to run * tests against a different database. (hsqldb is the default.) */ public enum ConnectionSpec { HSQLDB("jdbc:hsqldb:res:foodmart", "FOODMART", "FOODMART", "org.hsqldb.jdbcDriver"), MYSQL( "jdbc:mysql://localhost/foodmart", "foodmart", "foodmart", "com.mysql.jdbc.Driver"); public final String url; public final String username; public final String password; public final String driver; ConnectionSpec(String url, String username, String password, String driver) { this.url = url; this.username = username; this.password = password; this.driver = driver; } } } // End OptiqAssert.java