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

org.neo4j.jdbc.StatementImpl Maven / Gradle / Ivy

There is a newer version: 6.0.1
Show newest version
/*
 * Copyright (c) 2023-2024 "Neo4j,"
 * Neo4j Sweden AB [https://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Licensed 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
 *
 *     https://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.neo4j.jdbc;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLWarning;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.UnaryOperator;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import org.neo4j.cypherdsl.support.schema_name.SchemaNames;
import org.neo4j.jdbc.internal.bolt.response.ResultSummary;
import org.neo4j.jdbc.internal.bolt.response.SummaryCounters;
import org.neo4j.jdbc.values.Values;

non-sealed class StatementImpl implements Neo4jStatement {

	// Adding the comment /*+ NEO4J FORCE_CYPHER */ to your Cypher statement will make the
	// JDBC driver opt-out from translating it to Cypher, even if the driver has been
	// configured for automatic translation.
	private static final Pattern PATTERN_ENFORCE_CYPHER = Pattern
		.compile("(['`\"])?[^'`\"]*/\\*\\+ NEO4J FORCE_CYPHER \\*/[^'`\"]*(['`\"])?");

	private static final Logger LOGGER = Logger.getLogger(Neo4jStatement.class.getCanonicalName());

	static final int DEFAULT_BUFFER_SIZE_FOR_INCOMING_STREAMS = 4096;
	static final Charset DEFAULT_ASCII_CHARSET_FOR_INCOMING_STREAM = StandardCharsets.ISO_8859_1;

	private final Connection connection;

	private final Neo4jTransactionSupplier transactionSupplier;

	private int fetchSize = DEFAULT_FETCH_SIZE;

	private int maxRows;

	private int maxFieldSize;

	protected ResultSet resultSet;

	private int updateCount = -1;

	private boolean multipleResultsApi;

	private int queryTimeout;

	protected boolean poolable;

	private boolean closeOnCompletion;

	private boolean closed;

	private final UnaryOperator sqlProcessor;

	private final Warnings warnings;

	private final AtomicBoolean resultSetAcquired = new AtomicBoolean(false);

	private final Map transactionMetadata = new ConcurrentHashMap<>();

	StatementImpl(Connection connection, Neo4jTransactionSupplier transactionSupplier,
			UnaryOperator sqlProcessor, Warnings localWarnings) {
		this.connection = Objects.requireNonNull(connection);
		this.transactionSupplier = Objects.requireNonNull(transactionSupplier);
		this.sqlProcessor = Objects.requireNonNullElseGet(sqlProcessor, UnaryOperator::identity);
		this.warnings = Objects.requireNonNullElseGet(localWarnings, Warnings::new);
	}

	/**
	 * This is for use with LocalStatement.
	 */
	StatementImpl() {
		this.connection = null;
		this.transactionSupplier = null;
		this.sqlProcessor = UnaryOperator.identity();
		this.warnings = new Warnings();
	}

	@Override
	public ResultSet executeQuery(String sql) throws SQLException {
		return executeQuery0(sql, true, Map.of());
	}

	protected final ResultSet executeQuery0(String sql, boolean applyProcessor, Map parameters)
			throws SQLException {
		assertIsOpen();
		closeResultSet();
		this.updateCount = -1;
		this.multipleResultsApi = false;
		if (applyProcessor) {
			sql = processSQL(sql);
		}
		var transaction = this.transactionSupplier.getTransaction(this.transactionMetadata);
		var fetchSize = (this.maxRows > 0) ? Math.min(this.maxRows, this.fetchSize) : this.fetchSize;
		var runAndPull = transaction.runAndPull(sql, getParameters(parameters), fetchSize, this.queryTimeout);
		this.resultSet = new ResultSetImpl(this, transaction, runAndPull.runResponse(), runAndPull.pullResponse(),
				this.fetchSize, this.maxRows, this.maxFieldSize);
		this.resultSetAcquired.set(false);
		return this.resultSet;
	}

	@Override
	public int executeUpdate(String sql) throws SQLException {
		return executeUpdate0(sql, true, Map.of());
	}

	protected final int executeUpdate0(String sql, boolean applyProcessor, Map parameters)
			throws SQLException {
		assertIsOpen();
		closeResultSet();
		this.updateCount = -1;
		this.multipleResultsApi = false;
		if (applyProcessor) {
			sql = processSQL(sql);
		}
		var transaction = this.transactionSupplier.getTransaction(this.transactionMetadata);
		return transaction.runAndDiscard(sql, getParameters(parameters), this.queryTimeout, transaction.isAutoCommit())
			.resultSummary()
			.map(ResultSummary::counters)
			.map(SummaryCounters::totalCount)
			.orElse(0);
	}

	@Override
	public void close() throws SQLException {
		if (this.closed) {
			return;
		}
		closeResultSet();
		this.closed = true;
	}

	@Override
	public int getMaxFieldSize() throws SQLException {
		assertIsOpen();
		return this.maxFieldSize;
	}

	@Override
	public void setMaxFieldSize(int max) throws SQLException {
		assertIsOpen();
		if (max < 0) {
			throw new SQLException("Max field size can not be negative");
		}
		this.maxFieldSize = max;
	}

	@Override
	public int getMaxRows() throws SQLException {
		assertIsOpen();
		return this.maxRows;
	}

	@Override
	public void setMaxRows(int max) throws SQLException {
		assertIsOpen();
		if (max < 0) {
			throw new SQLException("Max rows can not be negative");
		}
		this.maxRows = max;
	}

	@Override
	public void setEscapeProcessing(boolean ignored) throws SQLException {
		assertIsOpen();
	}

	@Override
	public int getQueryTimeout() throws SQLException {
		assertIsOpen();
		return this.queryTimeout;
	}

	@Override
	public void setQueryTimeout(int seconds) throws SQLException {
		assertIsOpen();
		if (seconds < 0) {
			throw new SQLException("Query timeout can not be negative");
		}
		this.queryTimeout = seconds;
	}

	@Override
	public void cancel() throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	@Override
	public SQLWarning getWarnings() throws SQLException {
		assertIsOpen();
		return this.warnings.get();
	}

	@Override
	public void clearWarnings() throws SQLException {
		assertIsOpen();
		this.warnings.clear();
	}

	@Override
	public void setCursorName(String name) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	@Override
	public boolean execute(String sql) throws SQLException {
		return execute0(sql, true, Map.of());
	}

	protected final boolean execute0(String sql, boolean applyProcessor, Map parameters)
			throws SQLException {
		assertIsOpen();
		closeResultSet();
		this.updateCount = -1;
		this.multipleResultsApi = true;
		if (applyProcessor) {
			sql = processSQL(sql);
		}
		var transaction = this.transactionSupplier.getTransaction(this.transactionMetadata);
		var fetchSize = (this.maxRows > 0) ? Math.min(this.maxRows, this.fetchSize) : this.fetchSize;
		var runAndPull = transaction.runAndPull(sql, getParameters(parameters), fetchSize, this.queryTimeout);
		var pullResponse = runAndPull.pullResponse();
		this.resultSet = new ResultSetImpl(this, transaction, runAndPull.runResponse(), pullResponse, this.fetchSize,
				this.maxRows, this.maxFieldSize);
		this.updateCount = pullResponse.resultSummary()
			.map(summary -> summary.counters().totalCount())
			.filter(count -> count > 0)
			.orElse(-1);
		return this.updateCount == -1;
	}

	private static Map getParameters(Map parameters) throws SQLException {
		var result = Objects.requireNonNullElseGet(parameters, Map::of);
		for (Map.Entry entry : result.entrySet()) {
			if (entry.getValue() instanceof Reader reader) {
				try (reader) {
					StringBuilder buf = new StringBuilder();
					char[] buffer = new char[DEFAULT_BUFFER_SIZE_FOR_INCOMING_STREAMS];
					int len;
					while ((len = reader.read(buffer)) != -1) {
						buf.append(buffer, 0, len);
					}
					entry.setValue(Values.value(buf.toString()));
				}
				catch (IOException ex) {
					throw new SQLException(ex);
				}
			}
			else if (entry.getValue() instanceof InputStream inputStream) {
				try (var in = new BufferedInputStream(inputStream); var out = new ByteArrayOutputStream()) {
					in.transferTo(out);
					entry.setValue(Values.value(out.toByteArray()));
				}
				catch (IOException ex) {
					throw new SQLException(ex);
				}
			}
		}
		return result;
	}

	@Override
	public ResultSet getResultSet() throws SQLException {
		assertIsOpen();
		if (!this.resultSetAcquired.compareAndSet(false, true)) {
			throw new SQLException("Result set has already been acquired");
		}
		return (this.multipleResultsApi && this.updateCount == -1) ? this.resultSet : null;
	}

	@Override
	public int getUpdateCount() throws SQLException {
		assertIsOpen();
		return (this.multipleResultsApi) ? this.updateCount : -1;
	}

	@Override
	public boolean getMoreResults() throws SQLException {
		assertIsOpen();
		if (this.multipleResultsApi) {
			closeResultSet();
			this.updateCount = -1;
		}
		return false;
	}

	@Override
	public void setFetchDirection(int direction) throws SQLException {
		assertIsOpen();
		// this hint is not supported
	}

	@Override
	public int getFetchDirection() throws SQLException {
		assertIsOpen();
		return ResultSet.FETCH_FORWARD;
	}

	@Override
	public void setFetchSize(int rows) throws SQLException {
		assertIsOpen();
		if (rows < 0) {
			throw new SQLException("Fetch size can not be negative");
		}
		this.fetchSize = (rows > 0) ? rows : DEFAULT_FETCH_SIZE;
	}

	@Override
	public int getFetchSize() throws SQLException {
		assertIsOpen();
		return this.fetchSize;
	}

	@Override
	public int getResultSetConcurrency() throws SQLException {
		assertIsOpen();
		return ResultSet.CONCUR_READ_ONLY;
	}

	@Override
	public int getResultSetType() throws SQLException {
		assertIsOpen();
		return ResultSet.TYPE_FORWARD_ONLY;
	}

	@Override
	public void addBatch(String sql) throws SQLException {
		throw new SQLException("Not supported");
	}

	@Override
	public void clearBatch() throws SQLException {
		throw new SQLException("Not supported");
	}

	@Override
	public int[] executeBatch() throws SQLException {
		throw new SQLException("Not supported");
	}

	@Override
	public Connection getConnection() throws SQLException {
		assertIsOpen();
		return this.connection;
	}

	@Override
	public boolean getMoreResults(int current) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	@Override
	public ResultSet getGeneratedKeys() throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	@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();
	}

	@Override
	public int getResultSetHoldability() throws SQLException {
		assertIsOpen();
		return ResultSet.CLOSE_CURSORS_AT_COMMIT;
	}

	@Override
	public boolean isClosed() {
		return this.closed;
	}

	@Override
	public void setPoolable(boolean poolable) throws SQLException {
		assertIsOpen();
		this.poolable = poolable;
	}

	@Override
	public boolean isPoolable() throws SQLException {
		assertIsOpen();
		return this.poolable;
	}

	@Override
	public void closeOnCompletion() throws SQLException {
		assertIsOpen();
		this.closeOnCompletion = true;
	}

	@Override
	public boolean isCloseOnCompletion() throws SQLException {
		assertIsOpen();
		return this.closeOnCompletion;
	}

	@Override
	public  T unwrap(Class iface) throws SQLException {
		if (iface.isAssignableFrom(getClass())) {
			return iface.cast(this);
		}
		else {
			throw new SQLException("This object does not implement the given interface");
		}
	}

	@Override
	public boolean isWrapperFor(Class iface) {
		return iface.isAssignableFrom(getClass());
	}

	@Override
	public String enquoteIdentifier(String identifier, boolean alwaysQuote) throws SQLException {
		return SchemaNames.sanitize(identifier, alwaysQuote)
			.orElseThrow(() -> new SQLException("Cannot quote identifier " + identifier));
	}

	protected void assertIsOpen() throws SQLException {
		if (this.closed) {
			throw new SQLException("The statement set is closed");
		}
	}

	private void closeResultSet() throws SQLException {
		if (this.resultSet != null) {
			this.resultSet.close();
			this.resultSet = null;
			this.resultSetAcquired.set(false);
		}
	}

	protected final String processSQL(String sql) throws SQLException {
		try {
			var processor = forceCypher(sql) ? UnaryOperator.identity() : this.sqlProcessor;
			var processedSQL = processor.apply(sql);
			if (LOGGER.isLoggable(Level.FINE) && !processedSQL.equals(sql)) {
				LOGGER.log(Level.FINE, "Processed ''{0}'' into ''{1}''", new Object[] { sql, processedSQL });
			}
			return processedSQL;
		}
		catch (IllegalArgumentException | IllegalStateException ex) {
			throw new SQLException(Optional.ofNullable(ex.getCause()).orElse(ex));
		}
	}

	static boolean forceCypher(String sql) {
		var matcher = PATTERN_ENFORCE_CYPHER.matcher(sql);
		while (matcher.find()) {
			if (matcher.group(1) != null && matcher.group(1).equals(matcher.group(2))) {
				continue;
			}
			return true;
		}
		return false;
	}

	@Override
	public Neo4jStatement withMetadata(Map metadata) {
		if (metadata != null) {
			this.transactionMetadata.putAll(metadata);
		}
		return this;
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy