All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nl.topicus.jdbc.statement.CloudSpannerPreparedStatement Maven / Gradle / Ivy

There is a newer version: 1.1.6
Show newest version
package nl.topicus.jdbc.statement;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;

import nl.topicus.jdbc.shaded.com.google.cloud.spanner.DatabaseClient;
import nl.topicus.jdbc.shaded.com.google.cloud.spanner.KeySet;
import nl.topicus.jdbc.shaded.com.google.cloud.spanner.Mutation;
import nl.topicus.jdbc.shaded.com.google.cloud.spanner.Mutation.WriteBuilder;
import nl.topicus.jdbc.shaded.com.google.cloud.spanner.ReadContext;
import nl.topicus.jdbc.shaded.com.google.rpc.Code;

import nl.topicus.jdbc.shaded.net.sf.jsqlparser.JSQLParserException;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.expression.Expression;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.expression.ExpressionVisitorAdapter;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.expression.JdbcParameter;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.expression.operators.relational.ItemsList;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.parser.CCJSqlParserUtil;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.parser.TokenMgrError;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.schema.Column;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.schema.Table;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.statement.Statement;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.statement.delete.Delete;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.statement.insert.Insert;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.statement.select.FromItemVisitorAdapter;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.statement.select.PlainSelect;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.statement.select.Select;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.statement.select.SelectBody;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.statement.select.SelectVisitorAdapter;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.statement.select.SubSelect;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.statement.update.Update;
import nl.topicus.jdbc.CloudSpannerConnection;
import nl.topicus.jdbc.MetaDataStore.TableKeyMetaData;
import nl.topicus.jdbc.exception.CloudSpannerSQLException;
import nl.topicus.jdbc.resultset.CloudSpannerResultSet;
import nl.topicus.jdbc.statement.AbstractTablePartWorker.DMLOperation;

/**
 * 
 * @author loite
 *
 */
public class CloudSpannerPreparedStatement extends AbstractCloudSpannerPreparedStatement
{
	private static final String INVALID_WHERE_CLAUSE_DELETE_MESSAGE = "The DELETE statement does not contain a valid WHERE clause. DELETE statements must contain a WHERE clause specifying the value of the primary key of the record(s) to be deleted in the form 'ID=value' or 'ID1=value1 AND ID2=value2'";

	private static final String INVALID_WHERE_CLAUSE_UPDATE_MESSAGE = "The UPDATE statement does not contain a valid WHERE clause. UPDATE statements must contain a WHERE clause specifying the value of the primary key of the record(s) to be deleted in the form 'ID=value' or 'ID1=value1 AND ID2=value2'";

	static final String PARSE_ERROR = "Error while parsing sql statement ";

	private final String sql;

	private final String[] sqlTokens;

	/**
	 * Flag indicating that an INSERT INTO ... ON DUPLICATE KEY UPDATE statement
	 * should be forced to do only an update
	 */
	private boolean forceUpdate;

	private List batchMutations = new ArrayList<>();

	public CloudSpannerPreparedStatement(String sql, CloudSpannerConnection connection, DatabaseClient dbClient)
	{
		super(connection, dbClient);
		this.sql = sql;
		this.sqlTokens = getTokens(sql);
	}

	@Override
	public ResultSet executeQuery(String sql) throws SQLException
	{
		throw new CloudSpannerSQLException(
				"The executeQuery(String sql)-method may not be called on a PreparedStatement",
				Code.FAILED_PRECONDITION);
	}

	@Override
	public ResultSet executeQuery() throws SQLException
	{
		CustomDriverStatement custom = getCustomDriverStatement(sqlTokens);
		if (custom != null && custom.isQuery())
		{
			return custom.executeQuery(sqlTokens);
		}
		Statement statement;
		try
		{
			statement = CCJSqlParserUtil.parse(sanitizeSQL(sql));
		}
		catch (JSQLParserException | TokenMgrError e)
		{
			throw new CloudSpannerSQLException(PARSE_ERROR + sql + ": " + e.getLocalizedMessage(),
					Code.INVALID_ARGUMENT, e);
		}
		if (statement instanceof Select)
		{
			determineForceSingleUseReadContext((Select) statement);
			nl.topicus.jdbc.shaded.com.google.cloud.spanner.Statement.Builder builder = createSelectBuilder(statement, sql);
			try (ReadContext context = getReadContext())
			{
				nl.topicus.jdbc.shaded.com.google.cloud.spanner.ResultSet rs = context.executeQuery(builder.build());
				return new CloudSpannerResultSet(this, rs);
			}
		}
		throw new CloudSpannerSQLException("SQL statement not suitable for executeQuery. Expected SELECT-statement.",
				Code.INVALID_ARGUMENT);
	}

	private nl.topicus.jdbc.shaded.com.google.cloud.spanner.Statement.Builder createSelectBuilder(Statement statement, String sql)
	{
		String namedSql = convertPositionalParametersToNamedParameters(sql);
		nl.topicus.jdbc.shaded.com.google.cloud.spanner.Statement.Builder builder = nl.topicus.jdbc.shaded.com.google.cloud.spanner.Statement.newBuilder(namedSql);
		setSelectParameters(((Select) statement).getSelectBody(), builder);

		return builder;
	}

	private String convertPositionalParametersToNamedParameters(String sql)
	{
		boolean inString = false;
		StringBuilder res = new StringBuilder(sql);
		int i = 0;
		int parIndex = 1;
		while (i < res.length())
		{
			char c = res.charAt(i);
			if (c == '\'')
			{
				inString = !inString;
			}
			else if (c == '?' && !inString)
			{
				res.replace(i, i + 1, "@p" + parIndex);
				parIndex++;
			}
			i++;
		}

		return res.toString();
	}

	private void setSelectParameters(SelectBody body, nl.topicus.jdbc.shaded.com.google.cloud.spanner.Statement.Builder builder)
	{
		if (body instanceof PlainSelect)
		{
			setPlainSelectParameters((PlainSelect) body, builder);
		}
		else
		{
			body.accept(new SelectVisitorAdapter()
			{
				@Override
				public void visit(PlainSelect plainSelect)
				{
					setPlainSelectParameters(plainSelect, builder);
				}
			});
		}
	}

	private void setPlainSelectParameters(PlainSelect plainSelect, nl.topicus.jdbc.shaded.com.google.cloud.spanner.Statement.Builder builder)
	{
		if (plainSelect.getFromItem() != null)
		{
			plainSelect.getFromItem().accept(new FromItemVisitorAdapter()
			{
				private int tableCount = 0;

				@Override
				public void visit(Table table)
				{
					tableCount++;
					if (tableCount == 1)
						getParameterStore().setTable(unquoteIdentifier(table.getFullyQualifiedName()));
					else
						getParameterStore().setTable(null);
				}
			});
		}
		setWhereParameters(plainSelect.getWhere(), builder);
		if (plainSelect.getLimit() != null)
		{
			setWhereParameters(plainSelect.getLimit().getRowCount(), builder);
		}
		if (plainSelect.getOffset() != null && plainSelect.getOffset().isOffsetJdbcParameter())
		{
			ValueBinderExpressionVisitorAdapter binder = new ValueBinderExpressionVisitorAdapter<>(
					getParameterStore(), builder.bind("p" + getParameterStore().getHighestIndex()), null);
			binder.setValue(getParameterStore().getParameter(getParameterStore().getHighestIndex()));
			getParameterStore().setType(getParameterStore().getHighestIndex(), Types.BIGINT);
		}
	}

	private void setWhereParameters(Expression where, nl.topicus.jdbc.shaded.com.google.cloud.spanner.Statement.Builder builder)
	{
		if (where != null)
		{
			where.accept(new ExpressionVisitorAdapter()
			{
				private String currentCol = null;

				@Override
				public void visit(Column col)
				{
					currentCol = unquoteIdentifier(col.getFullyQualifiedName());
				}

				@Override
				public void visit(JdbcParameter parameter)
				{
					parameter.accept(new ValueBinderExpressionVisitorAdapter<>(getParameterStore(),
							builder.bind("p" + parameter.getIndex()), currentCol));
					currentCol = null;
				}

				@Override
				public void visit(SubSelect subSelect)
				{
					setSelectParameters(subSelect.getSelectBody(), builder);
				}

			});
		}
	}

	private boolean isDDLStatement()
	{
		return isDDLStatement(sqlTokens);
	}

	@Override
	public void addBatch() throws SQLException
	{
		if (getConnection().getAutoCommit())
		{
			throw new SQLFeatureNotSupportedException(
					"Batching of statements is only allowed when not running in autocommit mode");
		}
		if (isDDLStatement())
		{
			throw new SQLFeatureNotSupportedException("DDL statements may not be batched");
		}
		if (isSelectStatement(sqlTokens))
		{
			throw new SQLFeatureNotSupportedException("SELECT statements may not be batched");
		}
		Mutations mutations = createMutations();
		batchMutations.add(mutations);
		getParameterStore().clearParameters();
	}

	@Override
	public void clearBatch() throws SQLException
	{
		batchMutations.clear();
		getParameterStore().clearParameters();
	}

	@Override
	public int[] executeBatch() throws SQLException
	{
		int[] res = new int[batchMutations.size()];
		int index = 0;
		for (Mutations mutation : batchMutations)
		{
			res[index] = (int) writeMutations(mutation);
			index++;
		}
		batchMutations.clear();
		getParameterStore().clearParameters();
		return res;
	}

	@Override
	public int executeUpdate() throws SQLException
	{
		CustomDriverStatement custom = getCustomDriverStatement(sqlTokens);
		if (custom != null && !custom.isQuery())
		{
			return custom.executeUpdate(sqlTokens);
		}
		if (isDDLStatement())
		{
			String ddl = formatDDLStatement(sql);
			return executeDDL(ddl);
		}
		Mutations mutations = createMutations();
		return (int) writeMutations(mutations);
	}

	private Mutations createMutations() throws SQLException
	{
		return createMutations(sql, false, false);
	}

	private Mutations createMutations(String sql, boolean forceUpdate, boolean generateParameterMetaData)
			throws SQLException
	{
		try
		{
			if (getConnection().isReadOnly())
			{
				throw new CloudSpannerSQLException("The connection is in read-only mode. Mutations are not allowed.",
						Code.FAILED_PRECONDITION);
			}
			if (isDDLStatement())
			{
				throw new CloudSpannerSQLException(
						"Cannot create mutation for DDL statement. Expected INSERT, UPDATE or DELETE",
						Code.INVALID_ARGUMENT);
			}
			Statement statement = CCJSqlParserUtil.parse(sanitizeSQL(sql));
			if (statement instanceof Insert)
			{
				Insert insertStatement = (Insert) statement;
				if (generateParameterMetaData || insertStatement.getSelect() == null)
					return new Mutations(createInsertMutation(insertStatement, generateParameterMetaData));
				return new Mutations(createInsertWithSelectStatement(insertStatement, forceUpdate));
			}
			else if (statement instanceof Update)
			{
				Update updateStatement = (Update) statement;
				if (updateStatement.getSelect() != null)
					throw new CloudSpannerSQLException(
							"UPDATE statement using SELECT is not supported. Try to re-write the statement as an INSERT INTO ... SELECT A, B, C FROM TABLE WHERE ... ON DUPLICATE KEY UPDATE",
							Code.INVALID_ARGUMENT);
				if (updateStatement.getTables().size() > 1)
					throw new CloudSpannerSQLException(
							"UPDATE statement using multiple tables is not supported. Try to re-write the statement as an INSERT INTO ... SELECT A, B, C FROM TABLE WHERE ... ON DUPLICATE KEY UPDATE",
							Code.INVALID_ARGUMENT);

				if (generateParameterMetaData || isSingleRowWhereClause(
						getConnection().getTable(unquoteIdentifier(updateStatement.getTables().get(0).getName())),
						updateStatement.getWhere()))
					return new Mutations(createUpdateMutation(updateStatement, generateParameterMetaData));
				// Translate into an 'INSERT ... SELECT ... ON DUPLICATE KEY
				// UPDATE'-statement
				String insertSQL = createInsertSelectOnDuplicateKeyUpdateStatement(updateStatement);
				return createMutations(insertSQL, true, false);
			}
			else if (statement instanceof Delete)
			{
				Delete deleteStatement = (Delete) statement;
				if (generateParameterMetaData || deleteStatement.getWhere() == null
						|| isSingleRowWhereClause(
								getConnection().getTable(unquoteIdentifier(deleteStatement.getTable().getName())),
								deleteStatement.getWhere()))
					return new Mutations(createDeleteMutation(deleteStatement, generateParameterMetaData));
				return new Mutations(createDeleteWorker(deleteStatement));
			}
			else
			{
				throw new CloudSpannerSQLException(
						"Unrecognized or unsupported SQL-statment: Expected one of INSERT, UPDATE or DELETE. Please note that batching of prepared statements is not supported for SELECT-statements.",
						Code.INVALID_ARGUMENT);
			}
		}
		catch (JSQLParserException | IllegalArgumentException | TokenMgrError e)
		{
			throw new CloudSpannerSQLException(PARSE_ERROR + sql + ": " + e.getLocalizedMessage(),
					Code.INVALID_ARGUMENT, e);
		}
	}

	private Mutation createInsertMutation(Insert insert, boolean generateParameterMetaData) throws SQLException
	{
		ItemsList items = insert.getItemsList();
		if (generateParameterMetaData && items == null && insert.getSelect() != null)
		{
			// Just initialize the parameter meta data of the select statement
			createSelectBuilder(insert.getSelect(), insert.getSelect().toString());
			return null;
		}
		if (!(items instanceof ExpressionList))
		{
			throw new CloudSpannerSQLException("Insert statement must specify a list of values", Code.INVALID_ARGUMENT);
		}
		if (insert.getColumns() == null || insert.getColumns().isEmpty())
		{
			throw new CloudSpannerSQLException("Insert statement must specify a list of column names",
					Code.INVALID_ARGUMENT);
		}
		List expressions = ((ExpressionList) items).getExpressions();
		String table = unquoteIdentifier(insert.getTable().getFullyQualifiedName());
		getParameterStore().setTable(table);
		WriteBuilder builder;
		if (insert.isUseDuplicate())
		{
			/**
			 * Do an insert-or-update. BUT: Cloud Spanner does not support
			 * supplying different values for the insert and update statements,
			 * meaning that only the values specified in the INSERT part of the
			 * statement will be considered. Anything specified in the 'ON
			 * DUPLICATE KEY UPDATE ...' statement will be ignored.
			 */
			if (this.forceUpdate)
				builder = Mutation.newUpdateBuilder(table);
			else
				builder = Mutation.newInsertOrUpdateBuilder(table);
		}
		else
		{
			/**
			 * Just do an insert and throw an error if a row with the specified
			 * key alread exists.
			 */
			builder = Mutation.newInsertBuilder(table);
		}
		int index = 0;
		for (Column col : insert.getColumns())
		{
			String columnName = unquoteIdentifier(col.getFullyQualifiedName());
			expressions.get(index).accept(new ValueBinderExpressionVisitorAdapter<>(getParameterStore(),
					builder.set(columnName), columnName));
			index++;
		}
		return builder.build();
	}

	private Mutation createUpdateMutation(Update update, boolean generateParameterMetaData) throws SQLException
	{
		if (update.getTables().isEmpty())
			throw new CloudSpannerSQLException("No table found in update statement", Code.INVALID_ARGUMENT);
		if (update.getTables().size() > 1)
			throw new CloudSpannerSQLException("Update statements for multiple tables at once are not supported",
					Code.INVALID_ARGUMENT);
		String table = unquoteIdentifier(update.getTables().get(0).getFullyQualifiedName());
		getParameterStore().setTable(table);
		List expressions = update.getExpressions();
		WriteBuilder builder = Mutation.newUpdateBuilder(table);
		int index = 0;
		for (Column col : update.getColumns())
		{
			String columnName = unquoteIdentifier(col.getFullyQualifiedName());
			expressions.get(index).accept(new ValueBinderExpressionVisitorAdapter<>(getParameterStore(),
					builder.set(columnName), columnName));
			index++;
		}
		visitUpdateWhereClause(update.getWhere(), builder, generateParameterMetaData);

		return builder.build();
	}

	private Mutation createDeleteMutation(Delete delete, boolean generateParameterMetaData) throws SQLException
	{
		String table = unquoteIdentifier(delete.getTable().getFullyQualifiedName());
		getParameterStore().setTable(table);
		Expression where = delete.getWhere();
		if (where == null)
		{
			// Delete all
			return Mutation.delete(table, KeySet.all());
		}
		else
		{
			// Delete one
			DeleteKeyBuilder keyBuilder = new DeleteKeyBuilder(getConnection().getTable(table),
					generateParameterMetaData);
			visitDeleteWhereClause(where, keyBuilder, generateParameterMetaData);
			return Mutation.delete(table, keyBuilder.getKeyBuilder().build());
		}
	}

	private void visitDeleteWhereClause(Expression where, DeleteKeyBuilder keyBuilder,
			boolean generateParameterMetaData) throws SQLException
	{
		if (where != null)
		{
			DMLWhereClauseVisitor whereClauseVisitor = new DMLWhereClauseVisitor(getParameterStore())
			{

				@Override
				protected void visitExpression(Column col, Expression expression)
				{
					String columnName = unquoteIdentifier(col.getFullyQualifiedName());
					keyBuilder.set(columnName);
					expression.accept(
							new KeyBuilderExpressionVisitorAdapter(getParameterStore(), columnName, keyBuilder));
				}

			};
			where.accept(whereClauseVisitor);
			if (!generateParameterMetaData && !whereClauseVisitor.isValid())
			{
				throw new CloudSpannerSQLException(INVALID_WHERE_CLAUSE_DELETE_MESSAGE, Code.INVALID_ARGUMENT);
			}
		}
	}

	private boolean isSingleRowWhereClause(TableKeyMetaData table, Expression where)
	{
		if (where != null)
		{
			SingleRowWhereClauseValidator validator = new SingleRowWhereClauseValidator(table);
			DMLWhereClauseVisitor whereClauseVisitor = new DMLWhereClauseVisitor(getParameterStore())
			{

				@Override
				protected void visitExpression(Column col, Expression expression)
				{
					String columnName = unquoteIdentifier(col.getFullyQualifiedName());
					validator.set(columnName);
					expression.accept(
							new SingleRowWhereClauseValidatorExpressionVisitorAdapter(getParameterStore(), validator));
				}

			};
			where.accept(whereClauseVisitor);
			return whereClauseVisitor.isValid() && validator.isValid();
		}
		return false;
	}

	private void visitUpdateWhereClause(Expression where, WriteBuilder builder, boolean generateParameterMetaData)
			throws SQLException
	{
		if (where != null)
		{
			DMLWhereClauseVisitor whereClauseVisitor = new DMLWhereClauseVisitor(getParameterStore())
			{

				@Override
				protected void visitExpression(Column col, Expression expression)
				{
					String columnName = unquoteIdentifier(col.getFullyQualifiedName());
					expression.accept(new ValueBinderExpressionVisitorAdapter<>(getParameterStore(),
							builder.set(columnName), columnName));
				}

			};
			where.accept(whereClauseVisitor);
			if (!generateParameterMetaData && !whereClauseVisitor.isValid())
			{
				throw new CloudSpannerSQLException(INVALID_WHERE_CLAUSE_UPDATE_MESSAGE, Code.INVALID_ARGUMENT);
			}
		}
		else
		{
			throw new SQLException(INVALID_WHERE_CLAUSE_UPDATE_MESSAGE);
		}
	}

	@Override
	public boolean execute() throws SQLException
	{
		CustomDriverStatement custom = getCustomDriverStatement(sqlTokens);
		if (custom != null)
			return custom.execute(sqlTokens);
		Statement statement = null;
		boolean ddl = isDDLStatement();
		if (!ddl)
		{
			try
			{
				statement = CCJSqlParserUtil.parse(sanitizeSQL(sql));
			}
			catch (JSQLParserException | TokenMgrError e)
			{
				throw new CloudSpannerSQLException(PARSE_ERROR + sql + ": " + e.getLocalizedMessage(),
						Code.INVALID_ARGUMENT, e);
			}
		}
		if (!ddl && statement instanceof Select)
		{
			lastResultSet = executeQuery();
			lastUpdateCount = -1;
			return true;
		}
		else
		{
			lastUpdateCount = executeUpdate();
			lastResultSet = null;
			return false;
		}
	}

	@Override
	public CloudSpannerParameterMetaData getParameterMetaData() throws SQLException
	{
		// parse the SQL statement without executing it
		try
		{
			if (isDDLStatement())
			{
				throw new CloudSpannerSQLException("Cannot get parameter meta data for DDL statement",
						Code.INVALID_ARGUMENT);
			}
			Statement statement = CCJSqlParserUtil.parse(sanitizeSQL(sql));
			if (statement instanceof Insert || statement instanceof Update || statement instanceof Delete)
			{
				// Create mutation, but don't do anything with it. This
				// initializes column names of the parameter store.
				createMutations(sql, false, true);
			}
			else if (statement instanceof Select)
			{
				// Create select builder, but don't do anything with it. This
				// initializes column names of the parameter store.
				createSelectBuilder(statement, sql);
			}
		}
		catch (JSQLParserException | TokenMgrError e)
		{
			throw new CloudSpannerSQLException(PARSE_ERROR + sql + ": " + e.getLocalizedMessage(),
					Code.INVALID_ARGUMENT, e);
		}
		return new CloudSpannerParameterMetaData(this);
	}

	private InsertWorker createInsertWithSelectStatement(Insert insert, boolean forceUpdate) throws SQLException
	{
		Select select = insert.getSelect();
		if (select == null)
		{
			throw new CloudSpannerSQLException("Insert statement must contain a select statement",
					Code.INVALID_ARGUMENT);
		}
		boolean isDuplicate = insert.isUseDuplicate();
		InsertWorker.DMLOperation mode;
		if (forceUpdate)
			mode = DMLOperation.UPDATE;
		else if (isDuplicate)
			mode = DMLOperation.ONDUPLICATEKEYUPDATE;
		else
			mode = DMLOperation.INSERT;
		return new InsertWorker(getConnection(), select, insert, getConnection().isAllowExtendedMode(), mode);
	}

	private DeleteWorker createDeleteWorker(Delete delete) throws SQLException
	{
		if (delete.getTable() == null || (delete.getTables() != null && delete.getTables().size() > 0))
		{
			throw new CloudSpannerSQLException("DELETE statement must contain only one table", Code.INVALID_ARGUMENT);
		}
		return new DeleteWorker(getConnection(), delete, getConnection().isAllowExtendedMode());
	}

	boolean isForceUpdate()
	{
		return forceUpdate;
	}

	void setForceUpdate(boolean forceUpdate)
	{
		this.forceUpdate = forceUpdate;
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy