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

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

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

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;

import nl.topicus.jdbc.shaded.com.google.cloud.spanner.DatabaseClient;
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.parser.CCJSqlParserUtil;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.parser.TokenMgrError;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.statement.Statement;
import nl.topicus.jdbc.shaded.net.sf.jsqlparser.statement.select.Select;
import nl.topicus.jdbc.CloudSpannerConnection;
import nl.topicus.jdbc.exception.CloudSpannerSQLException;
import nl.topicus.jdbc.resultset.CloudSpannerResultSet;

/**
 * 
 * @author loite
 *
 */
public class CloudSpannerStatement extends AbstractCloudSpannerStatement
{
	protected ResultSet lastResultSet = null;

	protected int lastUpdateCount = -1;

	private Pattern nl.topicus.jdbc.shaded.com.entPattern = Pattern.nl.topicus.jdbc.shaded.com.ile("//.*|/\\*((.|\\n)(?!=*/))+\\*/|--.*(?=\\n)", Pattern.DOTALL);

	private BatchMode batchMode = BatchMode.None;

	private List batchStatements = new ArrayList<>();

	private enum BatchMode
	{
		None, DML, DDL;
	}

	public CloudSpannerStatement(CloudSpannerConnection connection, DatabaseClient dbClient)
	{
		super(connection, dbClient);
	}

	/**
	 * Does some formatting to DDL statements that might have been generated by
	 * standard SQL generators to make it nl.topicus.jdbc.shaded.com.atible with Google Cloud Spanner.
	 * We also need to get rid of any nl.topicus.jdbc.shaded.com.ents, as Google Cloud Spanner does not
	 * accept nl.topicus.jdbc.shaded.com.ents in DDL-statements.
	 * 
	 * @param sql
	 *            The sql to format
	 * @return The formatted DDL statement.
	 */
	protected String formatDDLStatement(String sql)
	{
		String result = removeComments(sql);
		String[] parts = getTokens(sql, 0);
		if (parts.length > 2)
		{
			if (parts[0].equalsIgnoreCase("create") && parts[1].equalsIgnoreCase("table"))
			{
				String sqlWithSingleSpaces = String.join(" ", parts);
				int primaryKeyIndex = sqlWithSingleSpaces.toUpperCase().indexOf(", PRIMARY KEY (");
				if (primaryKeyIndex > -1)
				{
					int endPrimaryKeyIndex = sqlWithSingleSpaces.indexOf(')', primaryKeyIndex);
					String primaryKeySpec = sqlWithSingleSpaces.substring(primaryKeyIndex + 2, endPrimaryKeyIndex + 1);
					sqlWithSingleSpaces = sqlWithSingleSpaces.replace(", " + primaryKeySpec, "");
					sqlWithSingleSpaces = sqlWithSingleSpaces + " " + primaryKeySpec;
					result = sqlWithSingleSpaces.replaceAll("\\s+\\)", ")");
				}
			}
		}

		return result;
	}

	@Override
	public void addBatch(String sql) throws SQLException
	{
		String[] sqlTokens = getTokens(sql);
		CustomDriverStatement custom = getCustomDriverStatement(sqlTokens);
		if (custom != null)
		{
			throw new SQLFeatureNotSupportedException("Custom statements may not be batched");
		}
		if (isSelectStatement(sqlTokens))
		{
			throw new SQLFeatureNotSupportedException("SELECT statements may not be batched");
		}
		boolean ddlStatement = isDDLStatement(sqlTokens);
		if (batchMode == BatchMode.None)
		{
			if (ddlStatement)
			{
				batchMode = BatchMode.DDL;
			}
			else
			{
				batchMode = BatchMode.DML;
			}
		}
		if (batchMode == BatchMode.DDL)
		{
			if (!ddlStatement)
			{
				throw new SQLFeatureNotSupportedException(
						"DML statements may not be batched together with DDL statements");
			}
			batchStatements.add(formatDDLStatement(sql));
		}
		else
		{
			if (ddlStatement)
			{
				throw new SQLFeatureNotSupportedException(
						"DDL statements may not be batched together with DML statements");
			}
			batchStatements.add(sql);
		}
	}

	@Override
	public void clearBatch() throws SQLException
	{
		batchStatements.clear();
		batchMode = BatchMode.None;
	}

	@Override
	public int[] executeBatch() throws SQLException
	{
		int[] res = new int[batchStatements.size()];
		if (batchMode == BatchMode.DDL)
		{
			executeDDL(batchStatements);
		}
		else
		{
			int index = 0;
			for (String sql : batchStatements)
			{
				PreparedStatement ps = getConnection().prepareStatement(sql);
				res[index] = ps.executeUpdate();
				index++;
			}
		}
		batchStatements.clear();
		batchMode = BatchMode.None;
		return res;
	}

	protected int executeDDL(String ddl) throws SQLException
	{
		getConnection().executeDDL(Arrays.asList(ddl));
		return 0;
	}

	protected void executeDDL(List ddl) throws SQLException
	{
		getConnection().executeDDL(ddl);
	}

	@Override
	public ResultSet executeQuery(String sql) throws SQLException
	{
		String[] sqlTokens = getTokens(sql);
		CustomDriverStatement custom = getCustomDriverStatement(sqlTokens);
		if (custom != null && custom.isQuery())
		{
			return custom.executeQuery(sqlTokens);
		}
		try (ReadContext context = getReadContext())
		{
			nl.topicus.jdbc.shaded.com.google.cloud.spanner.ResultSet rs = context.executeQuery(nl.topicus.jdbc.shaded.com.google.cloud.spanner.Statement.of(sql));
			return new CloudSpannerResultSet(this, rs);
		}
	}

	@Override
	public int executeUpdate(String sql) throws SQLException
	{
		String[] sqlTokens = getTokens(sql);
		CustomDriverStatement custom = getCustomDriverStatement(sqlTokens);
		if (custom != null && !custom.isQuery())
		{
			return custom.executeUpdate(sqlTokens);
		}
		PreparedStatement ps = getConnection().prepareStatement(sql);
		return ps.executeUpdate();
	}

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

	private static final String[] DDL_STATEMENTS = { "CREATE", "ALTER", "DROP" };

	/**
	 * Do a quick check if this SQL statement is a DDL statement
	 * 
	 * @param sqlTokens
	 *            The statement to check
	 * @return true if the SQL statement is a DDL statement
	 */
	protected boolean isDDLStatement(String[] sqlTokens)
	{
		if (sqlTokens.length > 0)
		{
			for (String statement : DDL_STATEMENTS)
			{
				if (sqlTokens[0].equalsIgnoreCase(statement))
					return true;
			}
		}

		return false;
	}

	/**
	 * Remove nl.topicus.jdbc.shaded.com.ents from the given sql string and split it into parts based
	 * on all space characters
	 * 
	 * @param sql
	 * @return String array with all the parts of the sql statement
	 */
	protected String[] getTokens(String sql)
	{
		return getTokens(sql, 5);
	}

	/**
	 * Remove nl.topicus.jdbc.shaded.com.ents from the given sql string and split it into parts based
	 * on all space characters
	 * 
	 * @param sql
	 *            The sql statement to break into parts
	 * @param limit
	 *            The maximum number of times the pattern should be applied
	 * @return String array with all the parts of the sql statement
	 */
	protected String[] getTokens(String sql, int limit)
	{
		String result = removeComments(sql);
		String generated = result.replaceFirst("=", " = ");
		return generated.split("\\s+", limit);
	}

	protected String removeComments(String sql)
	{
		return nl.topicus.jdbc.shaded.com.entPattern.matcher(sql).replaceAll("").trim();
	}

	protected boolean isSelectStatement(String[] sqlTokens)
	{
		if (sqlTokens.length > 0 && sqlTokens[0].equalsIgnoreCase("SELECT"))
			return true;

		return false;
	}

	public abstract class CustomDriverStatement
	{
		private final String statement;

		private final boolean query;

		private CustomDriverStatement(String statement, boolean query)
		{
			this.statement = statement;
			this.query = query;
		}

		protected final boolean isQuery()
		{
			return query;
		}

		protected final boolean execute(String[] sqlTokens) throws SQLException
		{
			if (query)
			{
				lastResultSet = executeQuery(sqlTokens);
				lastUpdateCount = -1;
				return true;
			}
			else
			{
				lastResultSet = null;
				lastUpdateCount = executeUpdate(sqlTokens);
				return false;
			}
		}

		protected ResultSet executeQuery(String[] sqlTokens) throws SQLException
		{
			throw new IllegalArgumentException("This statement is not valid for execution as a query");
		}

		protected int executeUpdate(String[] sqlTokens) throws SQLException
		{
			throw new IllegalArgumentException("This statement is not valid for execution as an update");
		}
	}

	private class ShowDdlOperations extends CustomDriverStatement
	{
		private ShowDdlOperations()
		{
			super("SHOW_DDL_OPERATIONS", true);
		}

		@Override
		public ResultSet executeQuery(String[] sqlTokens) throws SQLException
		{
			return getConnection().getRunningDDLOperations(CloudSpannerStatement.this);
		}
	}

	private class CleanDdlOperations extends CustomDriverStatement
	{
		private CleanDdlOperations()
		{
			super("CLEAN_DDL_OPERATIONS", false);
		}

		@Override
		public int executeUpdate(String[] sqlTokens) throws SQLException
		{
			return getConnection().clearFinishedDDLOperations();
		}
	}

	private class WaitForDdlOperations extends CustomDriverStatement
	{
		private WaitForDdlOperations()
		{
			super("WAIT_FOR_DDL_OPERATIONS", false);
		}

		@Override
		public int executeUpdate(String[] sqlTokens) throws SQLException
		{
			getConnection().waitForDdlOperations();
			return 0;
		}
	}

	private class SetConnectionProperty extends CustomDriverStatement
	{
		private SetConnectionProperty()
		{
			super("SET_CONNECTION_PROPERTY", false);
		}

		@Override
		public int executeUpdate(String[] sqlTokens) throws SQLException
		{
			if (sqlTokens.length != 4 || !"=".equals(sqlTokens[2]))
				throw new CloudSpannerSQLException(
						"Invalid argument(s) for SET_CONNECTION_PROPERTY. Expected \"SET_CONNECTION_PROPERTY propertyName=propertyValue\"",
						Code.INVALID_ARGUMENT);
			return getConnection().setDynamicConnectionProperty(sqlTokens[1], sqlTokens[3]);
		}
	}

	private class GetConnectionProperty extends CustomDriverStatement
	{
		private GetConnectionProperty()
		{
			super("GET_CONNECTION_PROPERTY", true);
		}

		@Override
		public ResultSet executeQuery(String[] sqlTokens) throws SQLException
		{
			if (sqlTokens.length == 1)
				return getConnection().getDynamicConnectionProperties(CloudSpannerStatement.this);
			if (sqlTokens.length == 2)
				return getConnection().getDynamicConnectionProperty(CloudSpannerStatement.this, sqlTokens[1]);
			throw new CloudSpannerSQLException(
					"Invalid argument(s) for GET_CONNECTION_PROPERTY. Expected \"GET_CONNECTION_PROPERTY propertyName\" or \"GET_CONNECTION_PROPERTY\"",
					Code.INVALID_ARGUMENT);
		}
	}

	private final List CUSTOM_DRIVER_STATEMENTS = Arrays.asList(new ShowDdlOperations(),
			new CleanDdlOperations(), new WaitForDdlOperations(), new SetConnectionProperty(),
			new GetConnectionProperty());

	/**
	 * Checks if a sql statement is a custom statement only recognized by this
	 * driver
	 * 
	 * @param sqlTokens
	 *            The statement to check
	 * @return The custom driver statement if the given statement is a custom
	 *         statement only recognized by the Cloud Spanner JDBC driver, such
	 *         as show_ddl_operations
	 */
	protected CustomDriverStatement getCustomDriverStatement(String[] sqlTokens)
	{
		if (sqlTokens.length > 0)
		{
			for (CustomDriverStatement statement : CUSTOM_DRIVER_STATEMENTS)
			{
				if (sqlTokens[0].equalsIgnoreCase(statement.statement))
				{
					return statement;
				}
			}
		}
		return null;
	}

	@Override
	public ResultSet getResultSet() throws SQLException
	{
		return lastResultSet;
	}

	@Override
	public int getUpdateCount() throws SQLException
	{
		return lastUpdateCount;
	}

	@Override
	public boolean getMoreResults() throws SQLException
	{
		moveToNextResult(CLOSE_CURRENT_RESULT);
		return false;
	}

	@Override
	public boolean getMoreResults(int current) throws SQLException
	{
		moveToNextResult(current);
		return false;
	}

	private void moveToNextResult(int current) throws SQLException
	{
		if (current != java.sql.Statement.KEEP_CURRENT_RESULT && lastResultSet != null)
			lastResultSet.close();
		lastResultSet = null;
		lastUpdateCount = -1;
	}

	@Override
	public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException
	{
		throw new SQLFeatureNotSupportedException();
	}

	@Override
	public int executeUpdate(String sql, int[] columnIndexes) throws SQLException
	{
		throw new SQLFeatureNotSupportedException();
	}

	@Override
	public int executeUpdate(String sql, String[] columnNames) throws SQLException
	{
		throw new SQLFeatureNotSupportedException();
	}

	@Override
	public boolean execute(String sql, int autoGeneratedKeys) throws SQLException
	{
		throw new SQLFeatureNotSupportedException();
	}

	@Override
	public boolean execute(String sql, int[] columnIndexes) throws SQLException
	{
		throw new SQLFeatureNotSupportedException();
	}

	@Override
	public boolean execute(String sql, String[] columnNames) throws SQLException
	{
		throw new SQLFeatureNotSupportedException();
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy