package tuffy.db;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.OutputStreamWriter;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.LinkedHashMap; // formerly java.util.concurrent.ConcurrentHashMap

import org.postgresql.PGConnection;

import tuffy.mln.Predicate;
import tuffy.util.Config;
import tuffy.util.ExceptionMan;
import tuffy.util.FileMan;
import tuffy.util.StringMan;
import tuffy.util.Timer;
import tuffy.util.UIMan;

/**
 * Interface with the RDBMS. Currently only supports PostgreSQL (8.4 or later).
 */
public class RDB {
	private int lastUpdateRowCount = -1;
	private boolean savedAutoCommit = false;

	public static final long constantIdBase = 1 << 29;
	
	static ArrayList<RDB> allRDBs = new ArrayList<RDB>();
	static int currentDBCounter = 0;

	public Connection con = null;

	private Statement currentlyRunningQuery = null;

	public String db;
	public String user;
	public String password;
	public String schema = null;

	public static LinkedHashSet<RDB> historyInstances = new LinkedHashSet<RDB>();
	
	public static void resetStaticVars() {
		allRDBs = new ArrayList<RDB>();
		historyInstances = new LinkedHashSet<RDB>();
		currentDBCounter = 0;
	}

	/**
	 *  Disable auto-commit so that JDBC won't fetch all query results at once. 
	 *  Call this before retrieving data from a huge table.
	 *  After the big query is done, call {@link RDB#restoreAutoCommitState()} to
	 *  restore the initial auto-commit state.
	 *  
	 *  @see RDB#restoreAutoCommitState()
	 *  @see <a href='http://jdbc.postgresql.org/documentation/84/query.html#query-with-cursor'>
	 *  PostgreSQL's JDBC doc</a>
	 */
	public void disableAutoCommitForNow(){
		try {
			savedAutoCommit = con.getAutoCommit();
			con.setAutoCommit(false);
		} catch (SQLException e) {
			ExceptionMan.handle(e);
		}
	}

	/**
	 * Register a stored procedure to explain SQL queries.
	 * @param pname name of the stored procedure
	 */
	public void regExplainProc(String pname){
		String sql = "create or replace function " + pname +
		"(q text) returns setof text as $$\r\n" + 
		"declare r record;\r\n" + 
		"begin\r\n" + 
		"  for r in execute 'explain ' || q loop\r\n" + 
		"    return next r.\"QUERY PLAN\";\r\n" + 
		"  end loop;\r\n" + 
		"end$$ language plpgsql";

		update(sql);
	}


	public void estimateQuery(String sql, boolean analyze){
		RDB db = this;
		db.estimateCost(sql);
		UIMan.verbose(2, "ESTIMATED cost = " + db.estimatedCost + " ; rows = " + db.estimatedRows);
		if(analyze){
			Timer.start("cqmat");
			db.update(sql);
			double rtime = Timer.elapsedMilliSeconds("cqmat");
			UIMan.verbose(2, Timer.elapsed("cqmat"));
			UIMan.verbose(2, "COST-RATIO = " + (db.estimatedCost/rtime) + " ; ROW-RATIO = " + 
					((double)db.estimatedRows/db.getLastUpdateRowCount()));
		}
	}

	public double estimatedCost = 0;
	public double estimatedRows = 0;
	public String estimateCost(String sql){
		String plan = explain(sql);
		if(plan == null){
			estimatedCost = Double.MAX_VALUE;
			estimatedRows = Double.MAX_VALUE;
			return null;
		}
		String rep = plan.split("\n")[0];
		String[] parts = rep.split(" ");
		for(String p : parts){
			if(p.startsWith("(cost=")){
				int i = p.indexOf("..") + 2;
				estimatedCost = Double.parseDouble(p.substring(i));
			}else if(p.startsWith("rows=")){
				estimatedRows = Double.parseDouble(p.substring(5));
			}
		}
		return rep;
	}

	/**
	 * Explain a SQL query with an execution plan.
	 * @param sql
	 */
	public String explain(String sql){

		try {

			//this.execute("EXPLAIN " + sql);
			//this.regExplainProc("expl");

			PreparedStatement ps = getPrepareStatement(
			"SELECT * FROM expl(cast(? as text))");
			ps.setString(1, sql);
			//System.out.println(sql);
			ResultSet rs = ps.executeQuery();

			StringBuilder sb = new StringBuilder();
			while(rs.next()){
				sb.append(rs.getString(1) + "\n");
			}
			return sb.toString();
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			//e.printStackTrace();

			// dirty

			try {
				this.con.close();
				this.con = DriverManager.getConnection(db, user, password);
				if(this.schema != null){
					this.execute("SET SEARCH_PATH TO " + schema);
				}
				//return this.explain(sql);
				return null;
			} catch (SQLException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}

			return null;
		}

	}


	public void createTempTableIntList(String rel, Collection<Integer> vals){
		dropTable(rel);
		String sql = "CREATE TABLE " + rel + "(id INT)";
		update(sql);
		try {
			
			String loadingFile = Config.dir_working + "/createTempTableIntList";
			
			BufferedWriter bw = new BufferedWriter(new FileWriter(
					Config.dir_working + "/createTempTableIntList"));
			
			for(int pid : vals){
				bw.write(pid + "\n");
			}
			bw.close();
		
			ArrayList<String> cols = new ArrayList<String>();
			cols.add("id");
			FileInputStream in = new FileInputStream(loadingFile);
			PGConnection con = (PGConnection) this.getConnection();
			sql = "COPY " + rel + 
			StringMan.commaListParen(cols) + " FROM STDIN CSV";
			con.getCopyAPI().copyIn(sql, in);
			in.close();
			this.analyze(rel);
			FileMan.removeFile(loadingFile);
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}


	/**
	 * Restore the auto-commit state saved by {@link RDB#disableAutoCommitForNow()}.
	 * 
	 * @see RDB#disableAutoCommitForNow()
	 */
	public void restoreAutoCommitState(){
		try {
			con.setAutoCommit(savedAutoCommit);
		} catch (SQLException e) {
			ExceptionMan.handle(e);
		}
	}

	/**
	 * Return the number of affected tuples from last update.
	 */
	public int getLastUpdateRowCount() {
		return lastUpdateRowCount;
	}

	/**
	 * Return the database connection.
	 */
	public Connection getConnection(){
		return con;
	}

	public Statement createStatementWithTimeout() throws SQLException  {
		Statement stm = con.createStatement();
		if (Config.timeout > 0 && !Config.mcsatTimedOut && !Config.exiting_mode) {
			int secondsLeft;
			if (Config.inGroundingPhase) {
				secondsLeft = Timer.secondsToGroundingTimeOut();
				if (secondsLeft <= 0) {
					ExceptionMan.die("Timeout during grounding");
				}
			} else {
				secondsLeft = Timer.secondsToTimeOut();
			}
			if (secondsLeft <= 0) {
				ExceptionMan.die("Trying to set query timeout to value <= 0");
			}
//			UIMan.verbose(3, "Creating statement with " + secondsLeft + " second timeout...");
			stm.setQueryTimeout(secondsLeft);
		}
		return stm;
	}
	
	public Statement createStatementWithTimeout(int resultSetType, int resultSetConcurrency) throws SQLException  {
		Statement stm = con.createStatement(resultSetType, resultSetConcurrency);
		if (Config.timeout > 0 && !Config.mcsatTimedOut && !Config.exiting_mode) {
			int secondsLeft;
			if (Config.inGroundingPhase) {
				secondsLeft = Timer.secondsToGroundingTimeOut();
				if (secondsLeft <= 0) {
					ExceptionMan.die("Timeout during grounding");
				}
			} else {
				secondsLeft = Timer.secondsToTimeOut();
			}			
			if (secondsLeft <= 0) {
				ExceptionMan.die("Trying to set query timeout to value <= 0");
			}
			stm.setQueryTimeout(secondsLeft);
		}
		return stm;
	}

	/**
	 * Dump a MAP world produced by MAP inference.
	 * 
	 * @param fout path of output file
	 */
	public void dumpTableToFile(Predicate p, String fout) {
		LinkedHashMap<Long,String> cmap = this.loadIdSymbolMapFromTable();
		try {
			BufferedWriter bufferedWriter = null;
			bufferedWriter = new BufferedWriter(new OutputStreamWriter
					(new FileOutputStream(fout),"UTF8"));
			String sql = "SELECT * FROM " + p.getRelName() +
			" WHERE truth OR itruth " +
			" ORDER BY " + StringMan.commaList(p.getArgs());
			ResultSet rs = this.query(sql);
			while(rs.next()) {
				String line = p.getName() + "(";
				ArrayList<String> cs = new ArrayList<String>();
				for(String a : p.getArgs()) {
					long c = rs.getLong(a);
					cs.add("\"" + StringMan.escapeJavaString(cmap.get(c)) + "\"");
				}
				line += StringMan.commaList(cs) + ")";
				bufferedWriter.append(line + "\n");
			}
			rs.close();
			bufferedWriter.close();
		} catch (Exception e) {
			ExceptionMan.handle(e);
		}
	}



	/**
	 * Attempt to establish the connection as specified in the 
	 * (deault) configuration.
	 */
	public static RDB getRDBbyConfig() {

		//TODO: why need so large
		//int nConnections = 1;

		//TODO: change nCores to # connect
		//if(allRDBs.size() < nConnections){
		RDB tmp  = new RDB(Config.db_url,
				Config.db_username, Config.db_password);

		tmp.db = Config.db_url;
		tmp.user = Config.db_username;
		tmp.password = Config.db_password;

		historyInstances.add(tmp);


		//	allRDBs.add(tmp);
		//	currentDBCounter = allRDBs.size() - 1;
		//}else{
		//	currentDBCounter = (currentDBCounter+1) % nConnections;
		//}

		//return allRDBs.get(currentDBCounter);
		return tmp;

	}

	public static RDB getRDBbyConfig(String schema) {

		RDB tmp  = new RDB(Config.db_url,
				Config.db_username, Config.db_password);

		tmp.db = Config.db_url;
		tmp.user = Config.db_username;
		tmp.password = Config.db_password;
		tmp.schema = schema;

		tmp.execute("SET search_path = " + schema);

		historyInstances.add(tmp);

		return tmp;

	}

	/**
	 * Register the JDBC driver.
	 */
	private void registerDrivers(){
		/*
		try {
			Class.forName("org.postgresql.Driver");
		} catch (ClassNotFoundException e) {
			System.err.println(e.getMessage());
			System.err.println("Failed to load PostgreSQL JDBC driver.");
		}
		 */
	}

	private void dumpSQL(String sql){
		UIMan.println("-----BEGIN:SQL-----");
		UIMan.println(sql);
		UIMan.println("-----END:SQL-----");
	}

	/**
	 * Execute an update SQL statement.
	 * 
	 * @return the number of tuples affected
	 */
	public int update(String sql){
		if(Config.exiting_mode) ExceptionMan.die("");
		try (Statement stmt = createStatementWithTimeout()) {
//			Statement stmt = createStatementWithTimeout();
			currentlyRunningQuery = stmt;
			lastUpdateRowCount = stmt.executeUpdate(sql);
			stmt.close();
			currentlyRunningQuery = null;
		} catch (SQLException e) {
			UIMan.error(sql);
			ExceptionMan.handle(e);
			return 0;
		}
		return lastUpdateRowCount;
	}

	/**
	 * Execute a SQL statement (query/update).
	 */
	public void execute(String sql) {		
		//if(sql.contains("DELETE") || sql.contains("delete")) System.out.println(sql);
		if(Config.exiting_mode) ExceptionMan.die("");
		try (Statement stmt = createStatementWithTimeout()) {
//			Statement stmt = createStatementWithTimeout();
			currentlyRunningQuery = stmt;
			stmt.execute(sql);
			stmt.close();
			currentlyRunningQuery = null;
		} catch (SQLException e) {
			dumpSQL(sql);
			e.printStackTrace();

			ExceptionMan.handle(e);
		}
	}

	public void executeWhatever(String sql) {
		try (Statement stmt = createStatementWithTimeout()) {
//			Statement stmt = createStatementWithTimeout();
			stmt.execute(sql);
			stmt.close();
		} catch (SQLException e) {
			dumpSQL(sql);
		}
	}

	private void executeRaw(String sql) throws SQLException{
		Statement stmt = createStatementWithTimeout();
		stmt.execute(sql);
		stmt.close();
	}

	private void updateRaw(String sql) throws SQLException{
		this.commit();
		this.setAutoCommit(true);
		Statement stmt = createStatementWithTimeout();
		currentlyRunningQuery = stmt;
		stmt.executeUpdate(sql);
		stmt.close();
		currentlyRunningQuery = null;
	}

	/**
	 * Execute a set of update SQL statements as a batch.
	 * 
	 * @return true on success
	 */
	public boolean updateBatch(ArrayList<String> sqls) {
		try (Statement st = createStatementWithTimeout()) {
//			Statement st = createStatementWithTimeout();
			currentlyRunningQuery = st;
			for(String s : sqls) {
				st.addBatch(s);
			}
			st.executeBatch();
			st.close();
			currentlyRunningQuery = null;
			return true;
		} catch (SQLException e) {
			ExceptionMan.handle(e);
		}
		return false;
	}

	/**
	 * Execute a SQL query.
	 * 
	 * @param sql the SQL statement
	 * @return the result set. remembe to close it afterwards.
	 */
	public ResultSet query(String sql){
		if(Config.exiting_mode) ExceptionMan.die("");
		try {
			Statement stmt = createStatementWithTimeout(ResultSet.HOLD_CURSORS_OVER_COMMIT, 1);
			currentlyRunningQuery = stmt;
			stmt.setFetchSize(100000);
			ResultSet rs = stmt.executeQuery(sql);
			currentlyRunningQuery = null;
			return rs;
		} catch (SQLException e) {
			UIMan.error(sql);
			ExceptionMan.handle(e);
			return null;
		}
	}

	/**
	 * Load the symbol table into a hash table mapping
	 * symbols to their IDs.
	 * 
	 * @see Config#relConstants
	 */
	public LinkedHashMap<String, Integer> loadSymbolIdMapFromTable() {
		LinkedHashMap<String, Integer> map =
			new LinkedHashMap<String, Integer>();
		String rel = Config.relConstants;
		String sql = "SELECT * FROM " + rel;
		ResultSet rs = query(sql);
		try {
			while(rs.next()) {
				String word = rs.getString("string");
				int id = rs.getInt("id");
				map.put(word, id);
			}
			rs.close();
		} catch (SQLException e) {
			ExceptionMan.handle(e);
		}
		return map;
	}

	/**
	 * Load the symbol table into a hash table mapping
	 * symbol IDs to the original symbols.
	 * 
	 * @see Config#relConstants
	 */
	public LinkedHashMap<Long,String> loadIdSymbolMapFromTable() {
		LinkedHashMap<Long,String> map =
			new LinkedHashMap<Long,String>();
		String sql = "SELECT * FROM " + Config.relConstants;
		ResultSet rs = query(sql);
		try {
			while(rs.next()) {
				String word = rs.getString("string");
				long id = rs.getLong("id");
				map.put(id, word);
			}
			rs.close();
		} catch (SQLException e) {
			ExceptionMan.handle(e);
		}
		return map;
	}

	/**
	 * Store the symbol-ID mapping into a symbol table.
	 * 
	 * @param mapConstantID the symbol-ID mapping
	 * @see Config#relConstants
	 */
	public void createConstantTable(Map<String, Integer> mapConstantID, String rel) {
		dropTable(rel);
		String sql = "CREATE TABLE " + rel +
		"(id bigint, string TEXT)";
		
		if (rel.equals(Config.relConstants)) {
			sql = "CREATE TABLE " + rel +
			"(id bigint PRIMARY KEY, string TEXT)";
		}
		
		//if(Config.using_greenplum){
		//	sql += " DISTRIBUTED BY (string)";
		//}
		
		update(sql);

		BufferedWriter writer = null;
		File loadingFile = new File(Config.getLoadingDir(), "loading_symbols_");
		try {
			writer = new BufferedWriter(new OutputStreamWriter
					(new FileOutputStream(loadingFile),"UTF8"));
		} catch (Exception e) {
			ExceptionMan.handle(e);
		}
		try {
			for(Map.Entry<String, Integer> pair : mapConstantID.entrySet()) {
				writer.append(pair.getValue().toString());
				writer.append("\t"); 
				writer.append(StringMan.escapeJavaString(pair.getKey()));
				writer.append("\n");
			}
			writer.close();
			FileInputStream in = new FileInputStream(loadingFile);
			PGConnection con = (PGConnection)this.getConnection();
			sql = "COPY " + rel + " FROM STDIN ";
			con.getCopyAPI().copyIn(sql, in);
			in.close();

			//sql = "CREATE INDEX " + rel + "_constants_index_id ON " + rel + "(id)";
			//this.execute(sql);

		}catch(Exception e) {
			ExceptionMan.handle(e);
		}

	}

	public void insertConstantTable(Map<String, Integer> mapConstantID) {
		String rel = Config.relConstants;
		String sql;
		BufferedWriter writer = null;
		File loadingFile = new File(Config.getLoadingDir(), "loading_symbols_");
		try {
			writer = new BufferedWriter(new OutputStreamWriter
					(new FileOutputStream(loadingFile),"UTF8"));
		} catch (Exception e) {
			ExceptionMan.handle(e);
		}
		try {
			for(Map.Entry<String, Integer> pair : mapConstantID.entrySet()) {
				writer.append(pair.getValue() + "\t" + 
						StringMan.escapeJavaString(pair.getKey()) + "\n");
			}
			writer.close();
			FileInputStream in = new FileInputStream(loadingFile);
			PGConnection con = (PGConnection)this.getConnection();
			sql = "COPY " + rel + " FROM STDIN ";
			con.getCopyAPI().copyIn(sql, in);
			in.close();

			//sql = "CREATE INDEX constants_index_id ON " + rel + "(id)";
			//this.execute(sql);

		}catch(Exception e) {
			ExceptionMan.handle(e);
		}
	}

	/**
	 * Create a table to store a set of integers
	 * @param rel the name of the table
	 * @param set the set of integers
	 */
	public void createSetTable(String rel, LinkedHashSet<Integer> set){
		dropTable(rel);
		String sql = "CREATE TEMPORARY TABLE " + rel +
		"(id INT)";
		update(sql);
		PreparedStatement ps = getPrepareStatement(
				"INSERT INTO " + rel + " VALUES(?)");
		try {
			for(int pid : set){
				ps.setInt(1, pid);
				ps.addBatch();
			}
			ps.executeBatch();
			ps.close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Try to drop a table; remain silent if the specified
	 * table doesn't exist.
	 */
	public void dropTable(String rel){
		// System.out.println("####### drop " + rel);
		dropStuff("TABLE", rel);
	}

	/**
	 * Try to drop a schema; remain silent if the specified
	 * schema doesn't exist.
	 */
	public boolean dropSchema(String sch){
		return dropStuff("SCHEMA", sch + "");
	}

	/**
	 * Try to drop a sequence; remain silent if the specified
	 * sequence doesn't exist.
	 */
	public void dropSequence(String seq){
		dropStuff("SEQUENCE", seq);
	}

	public void dropView(String view){
		dropStuff("VIEW", view);
	}

	private boolean dropStuff(String type, String obj){
		String sql = "DROP " + type + " IF EXISTS " + obj + " CASCADE";
		String sql2 = "DROP " + type + " IF EXISTS " + obj + "";
		try {
			updateRaw(sql);
			return true;
		} catch (SQLException e) {

			// the target was not found; do nothing
			try{
				updateRaw(sql2);
				return true; 
			}catch(Exception e2){
				return false;
			}

		}
	}


	/**
	 * Return a prepared statement of the given SQL statement.
	 * A SQL statement with or without parameters can be pre-compiled 
	 * and stored in a PreparedStatement object. This object can then 
	 * be used to efficiently execute this statement multiple times. 
	 */
	public PreparedStatement getPrepareStatement(String sql) {
		PreparedStatement ps = null;
		try {
			ps = con.prepareStatement(sql,ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
			ps.setFetchSize(100000);
		} catch (SQLException e) {
			ExceptionMan.handle(e);
		}
		return ps;
	}

	public boolean schemaExists(String name){

		ResultSet rs = this.query("SELECT * FROM information_schema.schemata WHERE schema_name = '" + name.toLowerCase() + "'");
		try {
			if(rs.next()){
				return true;
			}else{
				return false;
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return false;
	}

	public boolean tableExists(String tableName) {
		String sql = "SELECT * FROM " + tableName + " LIMIT 1";
		try {
			executeRaw(sql);
			return true;
		} catch (SQLException e){
			return false;
		}
	}
	
	public boolean tableExists(String schemaName, String tableName){
		
		String sql = "SELECT * FROM " + schemaName + "." + tableName + " LIMIT 1";
		try {
			executeRaw(sql);
			return true;
		} catch (SQLException e){
			return false;
		}
	}

    /**
     * Reset the database schema that serves as Tuffy's workspace.
     * 
     * @see Config#db_schema
     */
    public void resetSchema(String schema) {
    	if (Config.pgSeed > 0) {
        	UIMan.println("### Setting Postgres seed to " + Config.pgSeed);
        	execute("SELECT setseed(" + Config.pgSeed + ");");
    	}
        UIMan.verbose(3, "### Checking existence of " + schema);
        if (Config.reuseSchema && countTuples("(SELECT schema_name FROM information_schema.schemata WHERE schema_name = '" + schema + "') SC") > 0) {
            try {
                updateRaw("SET search_path = " + schema);
                UIMan.verbose(3, "### Reusing schema " + schema);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        } else {
            dropSchema(schema);
            UIMan.verbose(3, "### Creating schema " + schema);
            String sql = "CREATE SCHEMA " + schema + " AUTHORIZATION " + Config.db_username;
            update(sql);
            sql = "GRANT ALL ON SCHEMA " + schema + " TO " + Config.db_username;
            update(sql);
            execute("SET search_path = " + schema);
            execute("DROP TYPE IF EXISTS typeOfIntArray CASCADE");
            execute("CREATE TYPE typeOfIntArray AS ( a INT[] );");
            execute(SQLMan.sqlTypeConversions);
            execute(SQLMan.sqlIntArrayFuncReg);
            execute(SQLMan.sqlRandomAgg);
            execute(SQLMan.sqlFuncMisc);
            regExplainProc("expl");
        }
    }
	/**
	 * Original version: 
	 * Reset the database schema that serves as Tuffy's workspace.
	 * 
	 * @see Config#db_schema
	 */
	/**public void resetSchema(String schema) {
		try {
			UIMan.verbose(3, "### Checking existence of " + schema);
			updateRaw("SET search_path = " + schema);
			UIMan.verbose(3, "### Reusing schema " + schema);
		} catch (SQLException e) {
			dropSchema(schema);
			UIMan.verbose(3, "### Creating schema " + schema);
			String sql = "CREATE SCHEMA " + schema + " AUTHORIZATION " + Config.db_username;
			update(sql);
			sql = "GRANT ALL ON SCHEMA " + schema + " TO " + Config.db_username;
			update(sql);
			execute("SET search_path = " + schema);
			execute("DROP TYPE IF EXISTS typeOfIntArray CASCADE");
			execute("CREATE TYPE typeOfIntArray AS ( a INT[] );");
			execute(SQLMan.sqlTypeConversions);
			execute(SQLMan.sqlIntArrayFuncReg);
			execute(SQLMan.sqlRandomAgg);
			execute(SQLMan.sqlFuncMisc);
            execute(SQLMan.stemFunc);
			regExplainProc("expl");
		}
	}*/

	/**
	 * Copy the tuples of a table to another.
	 * Can be used to check out the content of a temporary table.
	 * 
	 * @param src name of the source table
	 * @param dest name the destination table; will be dropped if already exists
	 */
	public void copyTable(String src, String dest) {
		dropTable(dest);
		String sql = "CREATE TABLE "+dest+" AS " +
		"SELECT * FROM " + src;
		try {
			updateRaw(sql);
		} catch (SQLException e) {
			ExceptionMan.handle(e);
		}
	}

	/**
	 * Commit the previous actions.
	 * Useless when AutoCommit is on, which is so by default.
	 */
	public void commit() {
//		try {
//			con.commit();
//		} catch (SQLException e) {
//			ExceptionMan.handle(e);
//		}
	}

	/**
	 * Specifies a JDBC connection.
	 */
	public RDB(String url, String user, String password){

		UIMan.verbose(1000, "------------------- Open a new DB " + this);

		registerDrivers();
		try {
			con = DriverManager.getConnection(url, user, password);
			con.setAutoCommit(true);
			//execute("SET work_mem = '100MB'");
			//execute("SET checkpoint_segments = 30");
			//execute("SET temp_buffers = '2000MB'");
			//execute("SET maintenance_work_mem = '100MB'");
			//execute("SET archive_mode = OFF");
			//execute("SET wal_buffers = '50MB'");
			//execute("SET shared_buffers = '500MB'");
			execute("set client_encoding='utf8'");
			/*
			if(Config.forceNestedLoop) {
				execute("SET enable_bitmapscan = 'off'");
				execute("SET enable_hashagg = 'off'");
				execute("SET enable_hashjoin = 'off'");
				execute("SET enable_indexscan = 'off'");
				execute("SET enable_mergejoin = 'off'");
				execute("SET enable_sort = 'off'");
				execute("SET enable_tidscan = 'off'");
			}
			if(Config.forceJoinOrder) {
				execute("SET join_collapse_limit = '1'");
			}
			 */    
			Runtime.getRuntime().addShutdownHook(new Thread() {
				public void run() {
					if(Config.exiting_mode) return;
					Config.exiting_mode = true;
					UIMan.setSilent(true);
					UIMan.setSilentErr(true);
					System.out.println("\n!!! Shutting down Tuffy !!!");
					if (currentlyRunningQuery != null){
						try {
							System.out.print("Cacelling currently running DB query...");
							currentlyRunningQuery.cancel();
							currentlyRunningQuery = null;
							System.out.println("Done.");
						} catch (SQLException e) {
							System.out.println("Failed.");
						}
					}

					System.out.print("Removing temporary dir '" + Config.getWorkingDir() + "'...");
					System.out.println(FileMan.removeDirectory(new File(Config.getWorkingDir()))?"OK" : "FAILED");

					if(!Config.keep_db_data){
						System.out.print("Removing database schema '" + Config.db_schema + "'...");
						System.out.println(dropSchema(Config.db_schema)?"OK" : "FAILED");
					}else{
						System.out.println("Data remains in schema '" + Config.db_schema + "'.");
					}
					try {
						if(con != null && !con.isClosed()) {
							con.close();
						}
					} catch (SQLException e) {
					}
				}
			});
		} catch (SQLException e) {
			System.err.println("Failed to connect to PostgreSQL!");
			System.err.println(e.getMessage());
			return;
		}
	}

	/**
	 * Set auto-commit state of this connection.
	 */
	public void setAutoCommit(boolean v){
		try {
			con.setAutoCommit(v);
		} catch (SQLException e) {
			System.err.println("Failed to set AutoCommit to " + v);
			System.err.println(e.getMessage());
		}
	}

	/**
	 * Read the current value of a sequence.
	 * 
	 * @param seq the name of the sequence
	 */
	public int getSequenceCurValue(String seq) {
		String s = "SELECT CURRVAL('"+seq+"')";
		ResultSet rs = query(s);
		if(rs == null) return -1;
		try {
			if(rs.next()) {
				return rs.getInt(1);
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return -1;
	}

	public long countTuplesAfterGroundingTimeout(String table) {
		String sql = "SELECT COUNT(*) FROM " + table;
		ResultSet rs;
		try {
			Statement stmt = con.createStatement(ResultSet.HOLD_CURSORS_OVER_COMMIT, 1);
			currentlyRunningQuery = stmt;
			stmt.setFetchSize(100000);
			rs = stmt.executeQuery(sql);
			currentlyRunningQuery = null;		
			if(rs == null) ExceptionMan.die("");
			try {
				if(rs.next()) {
					long c = rs.getLong(1);
					rs.close();
					return c;
				}
			} catch (SQLException e) {
				e.printStackTrace();
			}
		} catch (SQLException e) {
			UIMan.error("can't get number of tuples at grounding time out");
			ExceptionMan.handle(e);
		}
		return -1;
	}

	/**
	 * Count the tuples in a table.
	 */
	public long countTuples(String table) {
		if(Config.exiting_mode) ExceptionMan.die("");
		String s = "SELECT COUNT(*) FROM " + table;
		ResultSet rs = query(s);
		if(rs == null) ExceptionMan.die("");
		try {
			if(rs.next()) {
				long c = rs.getLong(1);
				rs.close();
				return c;
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return -1;
	}

	/**
	 * Close this connection.
	 */
	public void close() {
		
		try {
			if (con != null) {
				UIMan.verbose(1000, "------------------- Close a DB " + this);
				con.close();
				con = null;
			}
			
		} catch (SQLException e) {
			ExceptionMan.handle(e);
		}
		
	}

	/**
	 * Analyze a specific table.
	 * 
	 * @param rel name of the table
	 * 
	 * @see <a hef='http://www.postgresql.org/docs/current/static/sql-analyze.html'>
	 * the PostgreSQL doc</a>
	 */
	public void analyze(String rel) {
		String sql = "ANALYZE " + rel;
		this.update(sql);
	}

	/**
	 * Vacuum a specific table.
	 * 
	 * @param rel name of the table
	 * 
	 * @see <a hef='http://www.postgresql.org/docs/current/static/sql-vacuum.html'>
	 * the PostgreSQL doc</a>
	 */
	public void vacuum(String rel) {
		String sql = "VACUUM " + rel;
		this.update(sql);
	}

	/**
	 * Drop an index if it exists.
	 * 
	 * @param idx name of the index
	 */
	public void dropIndex(String idx){
		String sql = "DROP INDEX IF EXISTS " + idx;
		try {
			updateRaw(sql);
		} catch (SQLException e) {
			ExceptionMan.handle(e);
		}
	}

	/**
	 * Reset the value of a sequence to 1.
	 * 
	 * @param seq name of the sequence
	 */
	public void resetSequence(String seq) {
		String sql = "SELECT setval('" + seq +
		"', 1, false)";
		execute(sql);
	}

	/**
	 * Call a stored procedure that doesn't have any parameters.
	 * 
	 * @param proc name of the stored procedure
	 */
	public void callProcedure(String proc) {
		CallableStatement stmt = null;
		try {
			stmt = con.prepareCall("{call "+proc+"()}");
			stmt.execute();
			stmt.close();
		} catch (SQLException e) {
			ExceptionMan.handle(e);
		}
	}

	/**
	 * Call a function that returns a double.
	 * 
	 * @param func name of the function
	 * @param args arguments in the form of a string
	 * @return value returned by the function; null on error
	 */
	public Double callFunctionDouble(String func, String args) {
		CallableStatement stmt = null;
		try {
			if(args == null) args = "";
			stmt = con.prepareCall("{? = call "+func+"(" +args + ")}");
			stmt.registerOutParameter(1, java.sql.Types.DOUBLE);
			stmt.execute();
			double x = stmt.getDouble(1);
			stmt.close();
			return x;
		} catch (SQLException e) {
			ExceptionMan.handle(e);
		}
		return null;
	}

}