/*

   Derby - Class org.apache.derby.impl.sql.GenericStatement

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF 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 org.apache.derby.impl.sql;

import java.sql.Timestamp;

import org.apache.derby.iapi.error.StandardException;
import org.apache.derby.iapi.reference.SQLState;
import org.apache.derby.iapi.services.daemon.IndexStatisticsDaemon;
import org.apache.derby.iapi.services.loader.GeneratedClass;
import org.apache.derby.iapi.services.monitor.Monitor;
import org.apache.derby.shared.common.sanity.SanityManager;
import org.apache.derby.iapi.services.stream.HeaderPrintWriter;
import org.apache.derby.iapi.sql.PreparedStatement;
import org.apache.derby.iapi.sql.Statement;
import org.apache.derby.iapi.sql.compile.CompilerContext;
import org.apache.derby.iapi.sql.compile.Parser;
import org.apache.derby.iapi.sql.compile.Visitable;
import org.apache.derby.iapi.sql.compile.ASTVisitor;
import org.apache.derby.iapi.sql.conn.LanguageConnectionContext;
import org.apache.derby.iapi.sql.conn.StatementContext;
import org.apache.derby.iapi.sql.dictionary.DataDictionary;
import org.apache.derby.iapi.sql.dictionary.SchemaDescriptor;
import org.apache.derby.iapi.sql.dictionary.TableDescriptor;
import org.apache.derby.impl.sql.compile.StatementNode;
import org.apache.derby.impl.sql.conn.GenericLanguageConnectionContext;
import org.apache.derby.iapi.transaction.TransactionControl;
import org.apache.derby.iapi.util.InterruptStatus;

public class GenericStatement
	implements Statement {

	// these fields define the identity of the statement
	private final SchemaDescriptor compilationSchema;
	private final String			statementText;
        private final boolean isForReadOnly;
	private int                      prepareIsolationLevel;
	private GenericPreparedStatement preparedStmt;

	/**
	 * Constructor for a Statement given the text of the statement in a String
	 * @param compilationSchema schema
	 * @param statementText	The text of the statement
	 * @param isForReadOnly if the statement is opened with level CONCUR_READ_ONLY
	 */

	public GenericStatement(SchemaDescriptor compilationSchema, String statementText, boolean isForReadOnly)
	{
		this.compilationSchema = compilationSchema;
		this.statementText = statementText;
		this.isForReadOnly = isForReadOnly;
	}

	/*
	 * Statement interface
	 */

	
	/* RESOLVE: may need error checking, debugging code here */
	public PreparedStatement prepare(LanguageConnectionContext lcc) throws StandardException
	{
		/*
		** Note: don't reset state since this might be
		** a recompilation of an already prepared statement.
		*/ 
		return prepare(lcc, false);
	}
	public PreparedStatement prepare(LanguageConnectionContext lcc, boolean forMetaData) throws StandardException
	{
		/*
		** Note: don't reset state since this might be
		** a recompilation of an already prepared statement.
		*/ 

        final int depth = lcc.getStatementDepth();
        String prevErrorId = null;
        while (true) {
            boolean recompile = false;
            try {
                return prepMinion(lcc, true, (Object[]) null,
                                  (SchemaDescriptor) null, forMetaData);
            } catch (StandardException se) {
                // There is a chance that we didn't see the invalidation
                // request from a DDL operation in another thread because
                // the statement wasn't registered as a dependent until
                // after the invalidation had been completed. Assume that's
                // what has happened if we see a conglomerate does not exist
                // error, and force a retry even if the statement hasn't been
                // invalidated.
                if (SQLState.STORE_CONGLOMERATE_DOES_NOT_EXIST.equals(
                        se.getMessageId())) {
                    // STORE_CONGLOMERATE_DOES_NOT_EXIST has exactly one
                    // argument: the conglomerate id
                    String conglomId = String.valueOf(se.getArguments()[0]);

                    // Request a recompile of the statement if a conglomerate
                    // disappears while we are compiling it. But if we have
                    // already retried once because the same conglomerate was
                    // missing, there's probably no hope that yet another retry
                    // will help, so let's break out instead of potentially
                    // looping infinitely.
                    if (!conglomId.equals(prevErrorId)) {
                        recompile = true;
                    }

                    prevErrorId = conglomId;
                }
                throw se;
            } finally {
                // Check if the statement was invalidated while it was
                // compiled. If so, the newly compiled plan may not be
                // up to date anymore, so we recompile the statement
                // if this happens. Note that this is checked in a finally
                // block, so we also retry if an exception was thrown. The
                // exception was probably thrown because of the changes
                // that invalidated the statement. If not, recompiling
                // will also fail, and the exception will be exposed to
                // the caller.
                //
                // invalidatedWhileCompiling and isValid are protected by
                // synchronization on the prepared statement.
                synchronized (preparedStmt) {
                    if (recompile || preparedStmt.invalidatedWhileCompiling) {
                        preparedStmt.isValid = false;
                        preparedStmt.invalidatedWhileCompiling = false;
                        recompile = true;
                    }
                }

                if (recompile) {
                    // A new statement context is pushed while compiling.
                    // Typically, this context is popped by an error
                    // handler at a higher level. But since we retry the
                    // compilation, the error handler won't be invoked, so
                    // the stack must be reset to its original state first.
                    while (lcc.getStatementDepth() > depth) {
                        lcc.popStatementContext(
                                lcc.getStatementContext(), null);
                    }

                    // Don't return yet. The statement was invalidated, so
                    // we must retry the compilation.
                    continue;
                }
            }
        }
	}

	private PreparedStatement prepMinion(LanguageConnectionContext lcc, boolean cacheMe, Object[] paramDefaults,
		SchemaDescriptor spsSchema, boolean internalSQL)
		throws StandardException
	{
						  
		long				beginTime = 0;
		long				parseTime = 0;
		long				bindTime = 0;
		long				optimizeTime = 0;
		long				generateTime = 0;
		Timestamp			beginTimestamp = null;
		Timestamp			endTimestamp = null;
		StatementContext	statementContext = null;

		// verify it isn't already prepared...
		// if it is, and is valid, simply return that tree.
		// if it is invalid, we will recompile now.
		if (preparedStmt != null) {
			if (preparedStmt.upToDate())
				return preparedStmt;
		}

		// Start a new optimizer trace for this statement
		if (lcc.optimizerTracingIsOn())
        {
            lcc.getOptimizerTracer().traceStartStatement( getSource() );
        }

		beginTime = getCurrentTimeMillis(lcc);
		/* beginTimestamp only meaningful if beginTime is meaningful.
		 * beginTime is meaningful if STATISTICS TIMING is ON.
		 */
		if (beginTime != 0)
		{
			beginTimestamp = new Timestamp(beginTime);
		}

        /** set the prepare isolation from the LanguageConnectionContext now as
		 * we need to consider it in caching decisions
		 */
		prepareIsolationLevel = lcc.getPrepareIsolationLevel();

		/* a note on statement caching:
		 * 
		 * A GenericPreparedStatement (GPS) is only added it to the cache if the
		 * parameter cacheMe is set to TRUE when the GPS is created.
		 * 
		 * Earlier only CacheStatement (CS) looked in the statement cache for a
		 * prepared statement when prepare was called. Now the functionality 
		 * of CS has been folded into GenericStatement (GS). So we search the
		 * cache for an existing PreparedStatement only when cacheMe is TRUE.
		 * i.e if the user calls prepare with cacheMe set to TRUE:
		 * then we 
		 *         a) look for the prepared statement in the cache.
		 *         b) add the prepared statement to the cache.
		 *
		 * In cases where the statement cache has been disabled (by setting the
		 * relevant Derby property) then the value of cacheMe is irrelevant.
		 */ 
		boolean foundInCache = false;
		if (preparedStmt == null) 
		{
			if (cacheMe)
				preparedStmt = (GenericPreparedStatement)((GenericLanguageConnectionContext)lcc).lookupStatement(this);

			if (preparedStmt == null) 
			{
				preparedStmt = new GenericPreparedStatement(this);
			}
			else
			{
				foundInCache = true;
			}
		}

		// if anyone else also has this prepared statement,
		// we don't want them trying to compile with it while
		// we are.  So, we synchronize on it and re-check
		// its validity first.
		// this is a no-op if and until there is a central
		// cache of prepared statement objects...
		synchronized (preparedStmt) 
		{

			for (;;) {

				if (foundInCache) {
					if (preparedStmt.referencesSessionSchema()) {
						// cannot use this state since it is private to a connection.
						// switch to a new statement.
						foundInCache = false;
						preparedStmt = new GenericPreparedStatement(this);
						break;
					}
				}

				// did it get updated while we waited for the lock on it?
				if (preparedStmt.upToDate()) {
					return preparedStmt;
				}

                if (!preparedStmt.isCompiling()) {
					break;
				}

				try {
					preparedStmt.wait();
				} catch (InterruptedException ie) {
                    InterruptStatus.setInterrupted();
				}
			}

            preparedStmt.beginCompiling();
		}

		try {

			HeaderPrintWriter istream = lcc.getLogStatementText() ? Monitor.getStream() : null;

			/*
			** For stored prepared statements, we want all
			** errors, etc in the context of the underlying
			** EXECUTE STATEMENT statement, so don't push/pop
			** another statement context unless we don't have
			** one.  We won't have one if it is an internal
			** SPS (e.g. jdbcmetadata).
			*/
			if (!preparedStmt.isStorable() || lcc.getStatementDepth() == 0)
			{
				// since this is for compilation only, set atomic
				// param to true and timeout param to 0
				statementContext = lcc.pushStatementContext(true, isForReadOnly, getSource(),
                                                            null, false, 0L);
			}



			/*
			** RESOLVE: we may ultimately wish to pass in
			** whether we are a jdbc metadata query or not to
			** get the CompilerContext to make the createDependency()
			** call a noop.
			*/
			CompilerContext cc = lcc.pushCompilerContext(compilationSchema);
			
			if (prepareIsolationLevel != 
				TransactionControl.UNSPECIFIED_ISOLATION_LEVEL)
			{
				cc.setScanIsolationLevel(prepareIsolationLevel);
			}


			// Look for stored statements that are in a system schema
			// and with a match compilation schema. If so, allow them
			// to compile using internal SQL constructs.

			if (internalSQL ||
				(spsSchema != null) && (spsSchema.isSystemSchema()) &&
					(spsSchema.equals(compilationSchema))) {
						cc.setReliability(CompilerContext.INTERNAL_SQL_LEGAL);
			}

			try 
			{
				// Statement logging if lcc.getLogStatementText() is true
				if (istream != null)
				{
					String xactId = lcc.getTransactionExecute().getActiveStateTxIdString();
					istream.printlnWithHeader(LanguageConnectionContext.xidStr + 
											  xactId + 
											  "), " +
											  LanguageConnectionContext.lccStr +
												  lcc.getInstanceNumber() +
											  "), " +
											  LanguageConnectionContext.dbnameStr +
												  lcc.getDbname() +
											  "), " +
											  LanguageConnectionContext.drdaStr +
												  lcc.getDrdaID() +
											  "), Begin compiling prepared statement: " + 
											  getSource() +
											  " :End prepared statement");
				}

				Parser p = cc.getParser();

				cc.setCurrentDependent(preparedStmt);

				//Only top level statements go through here, nested statement
				//will invoke this method from other places
				StatementNode qt = (StatementNode)
                        p.parseStatement(statementText, paramDefaults);

				parseTime = getCurrentTimeMillis(lcc);

                // Call user-written tree-printer if it exists
                walkAST( lcc, qt, ASTVisitor.AFTER_PARSE);

				if (SanityManager.DEBUG) 
				{
					if (SanityManager.DEBUG_ON("DumpParseTree")) 
					{
						SanityManager.GET_DEBUG_STREAM().print(
							"\n\n============PARSE===========\n\n");
						qt.treePrint();
						lcc.getPrintedObjectsMap().clear();
					}

					if (SanityManager.DEBUG_ON("StopAfterParsing")) 
					{
                        lcc.setLastQueryTree( qt );
                        
						throw StandardException.newException(SQLState.LANG_STOP_AFTER_PARSING);
					}
				}

				/*
				** Tell the data dictionary that we are about to do
				** a bunch of "get" operations that must be consistent with
				** each other.
				*/
				
				DataDictionary dataDictionary = lcc.getDataDictionary();

				int ddMode = dataDictionary == null ? 0 : dataDictionary.startReading(lcc);

				try
				{
					// start a nested transaction -- all locks acquired by bind
					// and optimize will be released when we end the nested
					// transaction.
					lcc.beginNestedTransaction(true);

					qt.bindStatement();
					bindTime = getCurrentTimeMillis(lcc);

                    // Call user-written tree-printer if it exists
                    walkAST( lcc, qt, ASTVisitor.AFTER_BIND);

					if (SanityManager.DEBUG) 
					{
						if (SanityManager.DEBUG_ON("DumpBindTree")) 
						{
							SanityManager.GET_DEBUG_STREAM().print(
								"\n\n============BIND===========\n\n");
							qt.treePrint();
							lcc.getPrintedObjectsMap().clear();
						}

						if (SanityManager.DEBUG_ON("StopAfterBinding")) {
							throw StandardException.newException(SQLState.LANG_STOP_AFTER_BINDING);
						}
					}

					//Derby424 - In order to avoid caching select statements referencing
					// any SESSION schema objects (including statements referencing views
					// in SESSION schema), we need to do the SESSION schema object check
					// here.  
					//a specific eg for statement referencing a view in SESSION schema 
					//CREATE TABLE t28A (c28 int)
					//INSERT INTO t28A VALUES (280),(281)
					//CREATE VIEW SESSION.t28v1 as select * from t28A
					//SELECT * from SESSION.t28v1 should show contents of view and we
					// should not cache this statement because a user can later define
					// a global temporary table with the same name as the view name.
					//Following demonstrates that
					//DECLARE GLOBAL TEMPORARY TABLE SESSION.t28v1(c21 int, c22 int) not
					//     logged
					//INSERT INTO SESSION.t28v1 VALUES (280,1),(281,2)
					//SELECT * from SESSION.t28v1 should show contents of global temporary
					//table and not the view.  Since this select statement was not cached
					// earlier, it will be compiled again and will go to global temporary
					// table to fetch data. This plan will not be cached either because
					// select statement is using SESSION schema object.
					//
					//Following if statement makes sure that if the statement is
					// referencing SESSION schema objects, then we do not want to cache it.
					// We will remove the entry that was made into the cache for 
					//this statement at the beginning of the compile phase.
					//The reason we do this check here rather than later in the compile
					// phase is because for a view, later on, we loose the information that
					// it was referencing SESSION schema because the reference
					//view gets replaced with the actual view definition. Right after
					// binding, we still have the information on the view and that is why
					// we do the check here.
					if (preparedStmt.referencesSessionSchema(qt)) {
						if (foundInCache)
							((GenericLanguageConnectionContext)lcc).removeStatement(this);
					}

                    // stop adding privileges for user-defined types
                    cc.skipTypePrivileges( true );
                    
					qt.optimizeStatement();

					optimizeTime = getCurrentTimeMillis(lcc);

                    // Call user-written tree-printer if it exists
                    walkAST( lcc, qt, ASTVisitor.AFTER_OPTIMIZE);

					// Statement logging if lcc.getLogStatementText() is true
					if (istream != null)
					{
						String xactId = lcc.getTransactionExecute().getActiveStateTxIdString();
						istream.printlnWithHeader(LanguageConnectionContext.xidStr + 
												  xactId + 
												  "), " +
												  LanguageConnectionContext.lccStr +
												  lcc.getInstanceNumber() +
												  "), " +
												  LanguageConnectionContext.dbnameStr +
												  lcc.getDbname() +
												  "), " +
												  LanguageConnectionContext.drdaStr +
												  lcc.getDrdaID() +
												  "), End compiling prepared statement: " + 
												  getSource() +
												  " :End prepared statement");
					}
				}

				catch (StandardException se)
				{
					lcc.commitNestedTransaction();

					// Statement logging if lcc.getLogStatementText() is true
					if (istream != null)
					{
						String xactId = lcc.getTransactionExecute().getActiveStateTxIdString();
						istream.printlnWithHeader(LanguageConnectionContext.xidStr + 
												  xactId + 
												  "), " +
												  LanguageConnectionContext.lccStr +
												  lcc.getInstanceNumber() +
												  "), " +
												  LanguageConnectionContext.dbnameStr +
												  lcc.getDbname() +
												  "), " +
												  LanguageConnectionContext.drdaStr +
												  lcc.getDrdaID() +
												  "), Error compiling prepared statement: " + 
												  getSource() +
												  " :End prepared statement");
					}
					throw se;
				}

				finally
				{
					/* Tell the data dictionary that we are done reading */
					if (dataDictionary != null)
					dataDictionary.doneReading(ddMode, lcc);
				}

				/* we need to move the commit of nested sub-transaction
				 * after we mark PS valid, during compilation, we might need
				 * to get some lock to synchronize with another thread's DDL
				 * execution, in particular, the compilation of insert/update/
				 * delete vs. create index/constraint (see Beetle 3976).  We
				 * can't release such lock until after we mark the PS valid.
				 * Otherwise we would just erase the DDL's invalidation when
				 * we mark it valid.
				 */
				try		// put in try block, commit sub-transaction if bad
				{
					if (SanityManager.DEBUG) 
					{
						if (SanityManager.DEBUG_ON("DumpOptimizedTree")) 
						{
							SanityManager.GET_DEBUG_STREAM().print(
								"\n\n============OPT===========\n\n");
							qt.treePrint();
							lcc.getPrintedObjectsMap().clear();
						}

						if (SanityManager.DEBUG_ON("StopAfterOptimizing")) 
						{
							throw StandardException.newException(SQLState.LANG_STOP_AFTER_OPTIMIZING);
						}
					}

					GeneratedClass ac = qt.generate(preparedStmt.getByteCodeSaver());

					generateTime = getCurrentTimeMillis(lcc);
					/* endTimestamp only meaningful if generateTime is meaningful.
					 * generateTime is meaningful if STATISTICS TIMING is ON.
					 */
					if (generateTime != 0)
					{
						endTimestamp = new Timestamp(generateTime);
					}

					if (SanityManager.DEBUG) 
					{
						if (SanityManager.DEBUG_ON("StopAfterGenerating")) 
						{
							throw StandardException.newException(SQLState.LANG_STOP_AFTER_GENERATING);
						}
					}

					/*
						copy over the compile-time created objects
						to the prepared statement.  This always happens
						at the end of a compile, so there is no need
						to erase the previous entries on a re-compile --
						this erases as it replaces.  Set the activation
						class in case it came from a StorablePreparedStatement
					*/
					preparedStmt.setConstantAction( qt.makeConstantAction() );
					preparedStmt.setSavedObjects( cc.getSavedObjects() );
					preparedStmt.setRequiredPermissionsList(cc.getRequiredPermissionsList());
                    preparedStmt.incrementVersionCounter();
					preparedStmt.setActivationClass(ac);
					preparedStmt.setNeedsSavepoint(qt.needsSavepoint());
					preparedStmt.setCursorInfo((CursorInfo)cc.getCursorInfo());
					preparedStmt.setIsAtomic(qt.isAtomic());
					preparedStmt.setExecuteStatementNameAndSchema(
												qt.executeStatementName(),
												qt.executeSchemaName()
												);
					preparedStmt.setSPSName(qt.getSPSName());
					preparedStmt.completeCompile(qt);
					preparedStmt.setCompileTimeWarnings(cc.getWarnings());

                    // Schedule updates of any stale index statistics we may
                    // have detected when creating the plan.
                    TableDescriptor[] tds = qt.updateIndexStatisticsFor();
                    if (tds.length > 0) {
                        IndexStatisticsDaemon isd = lcc.getDataDictionary().
                            getIndexStatsRefresher(true);
                        if (isd != null) {
                            for (int i=0; i < tds.length; i++) {
                                isd.schedule(tds[i]);
                            }
                        }
                    }
                }
				catch (StandardException e) 	// hold it, throw it
				{
					lcc.commitNestedTransaction();
					throw e;
				}

				if (lcc.getRunTimeStatisticsMode())
				{
					preparedStmt.setCompileTimeMillis(
						parseTime - beginTime, //parse time
						bindTime - parseTime, //bind time
						optimizeTime - bindTime, //optimize time
						generateTime - optimizeTime, //generate time
						generateTime - beginTime, //total compile time
						beginTimestamp,
						endTimestamp);
				}

			}
			finally // for block introduced by pushCompilerContext()
			{
				lcc.popCompilerContext( cc );
			}
		}
		catch (StandardException se)
		{
			if (foundInCache)
				((GenericLanguageConnectionContext)lcc).removeStatement(this);

			throw se;
		}
		finally
		{
            preparedStmt.endCompiling();
		}

		lcc.commitNestedTransaction();

		if (statementContext != null)
			lcc.popStatementContext(statementContext, null);

		return preparedStmt;
	}

    /** Walk the AST, using a (user-supplied) Visitor */
    private void walkAST( LanguageConnectionContext lcc, Visitable queryTree, int phase ) throws StandardException
    {
        ASTVisitor visitor = lcc.getASTVisitor();
        if ( visitor != null )
        {
            visitor.begin( statementText, phase );
            queryTree.accept( visitor );
            visitor.end( phase );
        }
    }

	/**
	 * Generates an execution plan given a set of named parameters.
	 * Does so for a storable prepared statement.
	 *
	 * @param	paramDefaults		Parameter defaults
	 *
	 * @return A PreparedStatement that allows execution of the execution
	 *	   plan.
	 * @exception StandardException	Thrown if this is an
	 *	   execution-only version of the module (the prepare() method
	 *	   relies on compilation).
	 */
	public	PreparedStatement prepareStorable(
				LanguageConnectionContext lcc,
				PreparedStatement ps,
				Object[]			paramDefaults,
				SchemaDescriptor	spsSchema,
				boolean internalSQL)
		throws StandardException
	{
		if (ps == null)
			ps = new GenericStorablePreparedStatement(this);
		else
			((GenericPreparedStatement) ps).statement = this;

		this.preparedStmt = (GenericPreparedStatement) ps;
		return prepMinion(lcc, false, paramDefaults, spsSchema, internalSQL);
	}

	public String getSource() {
		return statementText;
	}

	public String getCompilationSchema() {
		return compilationSchema.getDescriptorName();
	}

	private static long getCurrentTimeMillis(LanguageConnectionContext lcc)
	{
		if (lcc.getStatisticsTiming())
		{
			return System.currentTimeMillis();
		}
		else
		{
			return 0;
		}
	}

    /**
     * Return the {@link PreparedStatement} currently associated with this
     * statement.
     *
     * @return the prepared statement that is associated with this statement
     */
    public PreparedStatement getPreparedStatement() {
        return preparedStmt;
    }

	/*
	** Identity
	*/
    @Override
	public boolean equals(Object other) {

		if (other instanceof GenericStatement) {

			GenericStatement os = (GenericStatement) other;

			return statementText.equals(os.statementText) && isForReadOnly==os.isForReadOnly
				&& compilationSchema.equals(os.compilationSchema) &&
				(prepareIsolationLevel == os.prepareIsolationLevel);
		}

		return false;
	}

    @Override
	public int hashCode() {

		return statementText.hashCode();
	}
}
