package net.sourceforge.mayfly; import net.sourceforge.mayfly.datastore.DataStore; import net.sourceforge.mayfly.evaluation.command.Command; import net.sourceforge.mayfly.evaluation.command.UpdateStore; import net.sourceforge.mayfly.evaluation.select.Evaluator; import net.sourceforge.mayfly.evaluation.select.StoreEvaluator; import net.sourceforge.mayfly.jdbc.JdbcConnection; import net.sourceforge.mayfly.parser.Parser; import java.io.IOException; import java.io.Reader; import java.sql.Connection; import java.sql.ResultSet; import java.util.Iterator; import java.util.List; import java.util.Set; /** * A Database contains a set of tables, but can be managed as easily as * any other object. * * <p>Here's an example of what your test might look like:</p> * <pre> * private Database database; * public void setUp() throws Exception { *     database = new Database(); * } * * public void tearDown() throws Exception { *     // Nulling variables can hurt performance in general, but *     // might be helpful here because JUnit3 keeps test case *     // objects around until the end of a test run. *     database = null; * } * * public void testData() throws Exception { *     connection = database.openConnection(); *     . . . *     connection.close(); * } * </pre> * * <p>In the above example, we first call {@link #openConnection()} * and then perform all operations via that connection. There are * also a variety of methods to operate on the database directly * (for example, {@link #execute(String)}. * These are basically convenience methods; most/all of these operations * can also be done via connections. These methods take per-connection settings * (for example the current schema as set by * SET SCHEMA, or the auto-commit flag) from a <i>default connection</i>; * that is they are shared with other calls which use the default connection, * but not with connections opened explicitly (for example * via {@link #openConnection()}). */ public class Database { private DataStore dataStore; private final MayflyConnection defaultConnection; private Options options = new Options(); /** * Create an empty database (one with no tables). */ public Database() { this(new DataStore()); } /** * Create a database starting with the contents of a {@link net.sourceforge.mayfly.datastore.DataStore}, * which you'd normally get from the {@link #dataStore()} method of * another {@link Database} object. */ public Database(DataStore store) { setDataStore(store); defaultConnection = new MayflyConnection(this); } /** * Execute an SQL command which does not return results. * This is similar to the JDBC java.sql.Statement#executeUpdate(java.lang.String) * but is more convenient if you have a Database instance around. * * @return Number of rows changed. */ public int execute(String sql) throws MayflyException { return defaultConnection.execute(sql); } /** * Execute a series of SQL commands separated by semicolons. * This method closes the reader when done. */ public void executeScript(Reader script) throws MayflyException { try { List commands = new Parser(script, options).parseCommands(); for (Iterator iter = commands.iterator(); iter.hasNext();) { Command command = (Command) iter.next(); defaultConnection.executeUpdate(command); } } finally { try { script.close(); } catch (IOException e) { // Location should probably be where we stopped // reading, which I guess is always end of file. throw new MayflyException(e); } } } /** * @internal * Only intended for use within Mayfly. */ public UpdateStore executeUpdate(Command command, String currentSchema) { Evaluator evaluator = new StoreEvaluator(dataStore, currentSchema); UpdateStore result = command.update(evaluator); dataStore = result.store(); return result; } /** * Execute an SQL command which returns results. * * This is similar to the JDBC java.sql.Statement#executeQuery(java.lang.String) * but is more convenient if you have a Database instance around. */ public ResultSet query(String sql) throws MayflyException { return defaultConnection.query(sql); } /** * List your tables, as names. * * <p>The returned list only includes tables * which you have explicitly created; there are no tables here * which are for Mayfly's own use. If you have more than one * schema, only tables in the current * schema (as set by SET SCHEMA) are returned.</p> * * <p>If a future version of Mayfly implements this functionality in * java.sql.DatabaseMetaData, this method is likely to remain, as * being more convenient than DatabaseMetaData.</p> */ public Set tables() { return defaultConnection.tables(); } /** * Return the schema names for this Database. * * <p>This returned list only includes schemas * which you have explicitly created. The anonymous schema is not * included in the returned list, but will always exist.</p> * * <p>If a future version of Mayfly implements this functionality in * java.sql.DatabaseMetaData, this method is likely to remain, as * being more convenient than DatabaseMetaData.</p> */ public Set schemas() { return dataStore.schemas(); } /** * Return the column names in the given table. * * <p>If a future version of Mayfly implements this functionality in * java.sql.DatabaseMetaData, this method may go away or become * some kind of convenience method.</p> */ public List columnNames(String tableName) { return defaultConnection.columnNames(tableName); } /** * Return the index names for indexes in the given table. * * <p>If you have more than one * schema, only tables in the current * schema (as set by SET SCHEMA) are consulted.</p> * * <p>If a future version of Mayfly implements this functionality in * java.sql.DatabaseMetaData, this method is likely to remain, as * being more convenient than DatabaseMetaData.</p> */ public List<String> indexes(String tableName) { return defaultConnection.indexes(tableName); } /** * Number of rows in given table. * * This is a convenience method. Your production code will almost * surely be counting rows (if it needs to at all) via * java.sql.ResultSet (or the SQL COUNT expression). * But this method may be convenient in tests. */ public int rowCount(String tableName) { return defaultConnection.rowCount(tableName); } /** * Open a JDBC connection. * * This is similar to the JDBC java.sql.DriverManager#getConnection(java.lang.String) * but is based on this Database, rather than the static Database used in the JDBC case. */ public Connection openConnection() { return new JdbcConnection(this); } /** * Take a snapshot of this database. * * Specifically, return the data store, which is * an immutable object containing all the data, and table definitions, for this * database. Because the data store is immutable, one might store it in a constant * and use it from multiple tests. Here's an example: * * <pre> static final DataStore standardSetup = makeData(); private static DataStore makeData() {     try {         Database original = new Database();         original.execute("create table foo (a integer)");         original.execute("insert into foo(a) values(6)");         return original.dataStore();     } catch (SQLException e) {         throw new RuntimeException(e);     } } Database database; public void setUp() {     database = new Database(standardSetup); } </pre> @see {@link JdbcDriver#create(DataStore)} */ public DataStore dataStore() { return dataStore; } /** * @internal * Only intended for use within Mayfly. * The idea is that public callers call * {@link #Database(DataStore)}. * * The idea of setting the datastore is a * dangerous one - what if someone else has changed * the database in the meantime? Are we supposed * to detect a conflict? Or merge the changes? * * So, yes, {@link #Database(DataStore)} is a * more sensible interface. */ public void setDataStore(DataStore store) { if (store == null) { throw new NullPointerException("Attempt to set data store to null"); } dataStore = store; } public Options options() { return options; } /** * Set whether table names are case sensitive. * The default is false (which is what is specified by the SQL standard). * * This option exists if you want to be able to write SQL code with * Mayfly, and check that it will run on MySQL * (in which table names are case sensitive, but only if file names * are, which usually means Unix vs. Windows). Unless you have this * situation, or perhaps a coding standard about the case of table * names, you'll want to stick with the SQL standard behavior. * * Note that setting this to true does not enable the creation of * several tables whose names differ only in case. It merely makes it * an error to refer to a table via a case other than the one * with which it was created. */ public void tableNamesCaseSensitive(boolean caseSensitive) { options = options.tableNamesCaseSensitive(caseSensitive); } }