/**
* Copyright (C) 2009-2013 FoundationDB, LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.foundationdb.sql.server;
import com.foundationdb.ais.model.ForeignKey;
import com.foundationdb.qp.operator.QueryContext;
import com.foundationdb.qp.operator.StoreAdapterHolder;
import com.foundationdb.server.error.AkibanInternalException;
import com.foundationdb.server.error.ImplicitlyCommittedException;
import com.foundationdb.server.error.InvalidOperationException;
import com.foundationdb.server.error.InvalidParameterValueException;
import com.foundationdb.server.error.NoTransactionInProgressException;
import com.foundationdb.server.error.TransactionAbortedException;
import com.foundationdb.server.error.TransactionInProgressException;
import com.foundationdb.server.error.TransactionReadOnlyException;
import com.foundationdb.server.service.blob.LobService;
import com.foundationdb.server.types.FormatOptions;
import com.foundationdb.server.types.service.TypesRegistryService;
import com.foundationdb.server.service.ServiceManager;
import com.foundationdb.server.service.dxl.DXLService;
import com.foundationdb.server.service.externaldata.ExternalDataService;
import com.foundationdb.server.service.monitor.SessionMonitor;
import com.foundationdb.server.service.routines.RoutineLoader;
import com.foundationdb.server.service.security.SecurityService;
import com.foundationdb.server.service.session.Session;
import com.foundationdb.server.service.transaction.TransactionService;
import com.foundationdb.server.service.tree.KeyCreator;
import com.foundationdb.server.types.common.types.TypesTranslator;
import com.foundationdb.sql.optimizer.AISBinderContext;
import com.foundationdb.sql.optimizer.rule.PipelineConfiguration;
import com.foundationdb.sql.optimizer.rule.cost.CostEstimator;
import com.foundationdb.sql.parser.IsolationLevel;
import java.io.IOException;
import java.util.*;
public abstract class ServerSessionBase extends AISBinderContext implements ServerSession
{
public static final String COMPILER_PROPERTIES_PREFIX = "optimizer.";
public static final String PIPELINE_PROPERTIES_PREFIX = "fdbsql.pipeline.";
protected final ServerServiceRequirements reqs;
protected Properties compilerProperties;
protected Map<String,Object> attributes = new HashMap<>();
protected PipelineConfiguration pipelineConfiguration;
protected Boolean directEnabled;
protected Session session;
protected StoreAdapterHolder adapters = new StoreAdapterHolder();
protected ServerTransaction transaction;
protected boolean transactionDefaultReadOnly = false;
protected IsolationLevel transactionDefaultIsolationLevel = IsolationLevel.UNSPECIFIED_ISOLATION_LEVEL;
protected ServerTransaction.PeriodicallyCommit transactionPeriodicallyCommit = ServerTransaction.PeriodicallyCommit.OFF;
protected ServerSessionMonitor sessionMonitor;
protected Long queryTimeoutMilli = null;
protected ServerValueEncoder.ZeroDateTimeBehavior zeroDateTimeBehavior = ServerValueEncoder.ZeroDateTimeBehavior.NONE;
protected FormatOptions options = new FormatOptions();
protected QueryContext.NotificationLevel maxNotificationLevel = QueryContext.NotificationLevel.INFO;
private Set<UUID> lobsCreated = new HashSet<>();
private LobService lobService;
public ServerSessionBase(ServerServiceRequirements reqs) {
this.reqs = reqs;
lobService = getServiceManager().getServiceByClass(LobService.class);
}
@Override
public void setProperty(String key, String value) {
String ovalue = (String)properties.get(key); // Not inheriting.
super.setProperty(key, value);
try {
if (!propertySet(key, properties.getProperty(key)))
sessionChanged(); // Give individual handlers a chance.
}
catch (InvalidOperationException ex) {
super.setProperty(key, ovalue);
try {
if (!propertySet(key, properties.getProperty(key)))
sessionChanged();
}
catch (InvalidOperationException ex2) {
throw new AkibanInternalException("Error recovering " + key + " setting",
ex2);
}
throw ex;
}
}
protected void setProperties(Properties properties) {
super.setProperties(properties);
for (String key : properties.stringPropertyNames()) {
propertySet(key, properties.getProperty(key));
}
sessionChanged();
}
/** React to a property change.
* Implementers are not required to remember the old state on
* error, but must not leave things in such a mess that reverting
* to the old value will not work.
* @see InvalidParameterValueException
**/
protected boolean propertySet(String key, String value) {
if ("zeroDateTimeBehavior".equals(key)) {
zeroDateTimeBehavior = ServerValueEncoder.ZeroDateTimeBehavior.fromProperty(value);
return true;
}
if (("binary_output").equals(key)){
FormatOptions.BinaryFormatOption bfo = FormatOptions.BinaryFormatOption.fromProperty(value);
options.set(bfo);
return true;
}
if (("jsonbinary_output").equals(key)) {
FormatOptions.JsonBinaryFormatOption bfo = FormatOptions.JsonBinaryFormatOption.fromProperty(value);
options.set(bfo);
return true;
}
if ("maxNotificationLevel".equals(key)) {
maxNotificationLevel = (value == null) ?
QueryContext.NotificationLevel.INFO :
QueryContext.NotificationLevel.valueOf(value);
return true;
}
if ("queryTimeoutSec".equals(key)) {
if (value == null)
queryTimeoutMilli = null;
else
queryTimeoutMilli = (long)(Double.parseDouble(value) * 1000);
return true;
}
if ("statement_timeout".equals(key)) {
if (value == null)
queryTimeoutMilli = null;
else
queryTimeoutMilli = Long.parseLong(value);
return true;
}
if ("transactionPeriodicallyCommit".equals(key)) {
transactionPeriodicallyCommit = ServerTransaction.PeriodicallyCommit.fromProperty(value);
return true;
}
if ("constraintCheckTime".equals(key)) {
reqs.txnService().setSessionOption(session,
TransactionService.SessionOption.CONSTRAINT_CHECK_TIME,
value);
return true;
}
return false;
}
@Override
public void setDefaultSchemaName(String defaultSchemaName) {
super.setDefaultSchemaName(defaultSchemaName);
sessionChanged();
}
@Override
public String getSessionSetting(String key) {
return getProperty(key);
}
protected abstract void sessionChanged();
@Override
public Map<String,Object> getAttributes() {
return attributes;
}
@Override
public Object getAttribute(String key) {
return attributes.get(key);
}
@Override
public void setAttribute(String key, Object attr) {
attributes.put(key, attr);
sessionChanged();
}
@Override
public DXLService getDXL() {
return reqs.dxl();
}
@Override
public Session getSession() {
return session;
}
@Override
public AISBinderContext getBinderContext() {
return this;
}
@Override
public Properties getCompilerProperties() {
if (compilerProperties == null)
compilerProperties = reqs.config().deriveProperties(COMPILER_PROPERTIES_PREFIX);
return compilerProperties;
}
@Override
public SessionMonitor getSessionMonitor() {
return sessionMonitor;
}
@Override
public StoreAdapterHolder getStoreHolder() {
return adapters;
}
@Override
public TransactionService getTransactionService() {
return reqs.txnService();
}
@Override
public boolean isTransactionActive() {
return (transaction != null);
}
@Override
public boolean isTransactionRollbackPending() {
return ((transaction != null) && transaction.isRollbackPending());
}
@Override
public void beginTransaction() {
if (transaction != null)
throw new TransactionInProgressException();
transaction = new ServerTransaction(this, transactionDefaultReadOnly, transactionDefaultIsolationLevel, transactionPeriodicallyCommit);
}
@Override
public void commitTransaction() {
if (transaction == null) {
warnClient(new NoTransactionInProgressException());
return;
}
try {
transaction.commit();
}
finally {
transaction = null;
}
}
@Override
public void rollbackTransaction() {
if (transaction == null) {
warnClient(new NoTransactionInProgressException());
return;
}
try {
transaction.rollback();
}
finally {
transaction = null;
}
}
@Override
public void setTransactionReadOnly(boolean readOnly) {
if (transaction == null)
throw new NoTransactionInProgressException();
transaction.setReadOnly(readOnly);
}
@Override
public void setTransactionDefaultReadOnly(boolean readOnly) {
this.transactionDefaultReadOnly = readOnly;
}
@Override
public ServerTransaction.PeriodicallyCommit getTransactionPeriodicallyCommit() {
return transactionPeriodicallyCommit;
}
@Override
public void setTransactionPeriodicallyCommit(ServerTransaction.PeriodicallyCommit periodicallyCommit) {
this.transactionPeriodicallyCommit = periodicallyCommit;
}
@Override
public IsolationLevel getTransactionIsolationLevel() {
IsolationLevel level;
if (transaction == null)
level = transactionDefaultIsolationLevel;
else
level = transaction.getIsolationLevel();
if (level == IsolationLevel.UNSPECIFIED_ISOLATION_LEVEL)
level = getTransactionService().actualIsolationLevel(level);
return level;
}
@Override
public IsolationLevel setTransactionIsolationLevel(IsolationLevel level) {
if (transaction == null)
throw new NoTransactionInProgressException();
return transaction.setIsolationLevel(level);
}
@Override
public IsolationLevel setTransactionDefaultIsolationLevel(IsolationLevel level) {
this.transactionDefaultIsolationLevel = getTransactionService().actualIsolationLevel(level);
return this.transactionDefaultIsolationLevel;
}
@Override
public TypesRegistryService typesRegistryService() {
return reqs.typesRegistryService();
}
@Override
public TypesTranslator typesTranslator() {
return reqs.dxl().ddlFunctions().getTypesTranslator();
}
@Override
public RoutineLoader getRoutineLoader() {
return reqs.routineLoader();
}
@Override
public ExternalDataService getExternalDataService() {
return reqs.externalData();
}
@Override
public SecurityService getSecurityService() {
return reqs.securityService();
}
@Override
public ServiceManager getServiceManager() {
return reqs.serviceManager();
}
@Override
public Date currentTime() {
return new Date();
}
@Override
public long getQueryTimeoutMilli() {
if (queryTimeoutMilli != null)
return queryTimeoutMilli;
else
return reqs.config().queryTimeoutMilli();
}
@Override
public ServerValueEncoder.ZeroDateTimeBehavior getZeroDateTimeBehavior() {
return zeroDateTimeBehavior;
}
@Override
public FormatOptions getFormatOptions() {
return options;
}
@Override
public CostEstimator costEstimator(ServerOperatorCompiler compiler, KeyCreator keyCreator) {
return new ServerCostEstimator(this, reqs, compiler, keyCreator);
}
protected void initAdapters(ServerOperatorCompiler compiler) {
adapters.init(session, reqs.config(), reqs.store());
}
/** Prepare to execute given statement.
* Uses current global transaction or makes a new local one.
* Returns any local transaction that should be committed / rolled back immediately.
*/
protected boolean beforeExecute(ServerStatement stmt) {
ServerStatement.TransactionMode transactionMode = stmt.getTransactionMode();
boolean localTransaction = false;
if(transaction != null && transactionMode == ServerStatement.TransactionMode.IMPLICIT_COMMIT_AND_NEW){
warnClient(new ImplicitlyCommittedException());
commitTransaction();
}
if (transaction != null) {
if(transactionMode == ServerStatement.TransactionMode.IMPLICIT_COMMIT) {
warnClient(new ImplicitlyCommittedException());
commitTransaction();
} else {
// Use global transaction.
transaction.checkTransactionMode(transactionMode);
}
}
else {
switch (transactionMode) {
case REQUIRED:
case REQUIRED_WRITE:
throw new NoTransactionInProgressException();
case READ:
case NEW:
case IMPLICIT_COMMIT_AND_NEW:
transaction = new ServerTransaction(this, true, transactionDefaultIsolationLevel, ServerTransaction.PeriodicallyCommit.OFF);
localTransaction = true;
break;
case WRITE:
case NEW_WRITE:
transaction = new ServerTransaction(this, transactionDefaultReadOnly, transactionDefaultIsolationLevel, transactionPeriodicallyCommit);
if (transaction.isReadOnly()) {
transaction.abort();
transaction = null;
throw new TransactionReadOnlyException();
}
transaction.beforeUpdate();
localTransaction = true;
break;
}
}
if (isTransactionRollbackPending()) {
ServerStatement.TransactionAbortedMode abortedMode = stmt.getTransactionAbortedMode();
switch (abortedMode) {
case ALLOWED:
break;
case NOT_ALLOWED:
throw new TransactionAbortedException();
default:
throw new IllegalStateException("Unknown mode: " + abortedMode);
}
}
return localTransaction;
}
/** Complete execute given statement.
* @see #beforeExecute
* @param allowsPeriodicCommit Some places where this is called are after a parse or prepare statement, we don't want
* to commit in those instances, only when a user statement was actually executed.
*/
protected void afterExecute(ServerStatement stmt,
boolean localTransaction,
boolean success, boolean allowsPeriodicCommit) {
if (localTransaction) {
if (success)
commitTransaction();
else
try {
transaction.abort();
}
finally {
transaction = null;
}
}
else if (transaction != null) {
// Make changes visible in open global transaction.
ServerStatement.TransactionMode transactionMode = stmt.getTransactionMode();
switch (transactionMode) {
case REQUIRED_WRITE:
case WRITE:
transaction.afterUpdate();
break;
}
if (allowsPeriodicCommit && success && !transaction.isRollbackPending()) {
if (transactionPeriodicallyCommit == ServerTransaction.PeriodicallyCommit.USERLEVEL &&
transaction.shouldPeriodicallyCommit()) {
commitTransaction();
} else {
// Give periodic commit a chance if enabled.
transaction.checkPeriodicallyCommit();
}
}
}
}
/** Should be called when embedded connection is opened, possibly
* within a routine call. */
protected void inheritFromCall() {
ServerCallContextStack stack = ServerCallContextStack.get();
stack.addCallee(this);
ServerCallContextStack.Entry call = stack.current();
if (call != null) {
ServerSessionBase server = (ServerSessionBase)call.getContext().getServer();
defaultSchemaName = server.defaultSchemaName;
session = server.session;
transaction = server.transaction;
transactionDefaultReadOnly = server.transactionDefaultReadOnly;
sessionMonitor.setCallerSessionId(server.getSessionMonitor().getSessionId());
}
if (transaction == null) {
transaction = stack.getSharedTransaction();
}
}
/** Called when routine exits to give embedded connection a chance
* to clean up and report leaks.
* @param topLevel <code>true</code> if this was the last call, which should force cleanup.
* @param success <code>false</code> is cleaning up due to error.
* @return <code>true</code> if needs to be kept open for
* outstanding <code>ResultSet</code>s.
*/
public boolean endCall(ServerQueryContext context,
ServerRoutineInvocation invocation,
boolean topLevel, boolean success) {
return false;
}
/** Called when embedded connection is closed, in case {@link inheritFromCall} was
* invoked at top-level. */
protected void endExplicit() {
ServerCallContextStack stack = ServerCallContextStack.get();
stack.endCallee(this);
}
public boolean shouldNotify(QueryContext.NotificationLevel level) {
return (level.ordinal() <= maxNotificationLevel.ordinal());
}
@Override
public void warnClient(InvalidOperationException e) {
try {
notifyClient(QueryContext.NotificationLevel.WARNING, e.getCode(), e.getShortMessage());
} catch(IOException ioe) {
// Ignore
}
}
@Override
public boolean isSchemaAccessible(String schemaName) {
return reqs.securityService().isAccessible(session, schemaName);
}
@Override
public PipelineConfiguration getPipelineConfiguration() {
if (pipelineConfiguration == null)
pipelineConfiguration = new PipelineConfiguration(reqs.config().deriveProperties(PIPELINE_PROPERTIES_PREFIX));
return pipelineConfiguration;
}
@Override
public void setDeferredForeignKey(ForeignKey foreignKey, boolean deferred) {
reqs.txnService().setDeferredForeignKey(session, foreignKey, deferred);
}
}