Skip to content

Repositories do not work with SQLite #3560

@APXEOLOG

Description

@APXEOLOG

Expected Behavior

Both read and write methods should be executable from the Repository (i.e., findAll() and deleteAll()) while using SQLite

Actual Behaviour

An error occurs during execution

io.micronaut.data.connection.exceptions.ConnectionException: Could not set JDBC Connection [org.sqlite.jdbc4.JDBC4Connection@6c63e2b6] read-only: Cannot change read-only flag after establishing a connection. Use SQLiteConfig#setReadOnly and SQLiteConfig.createConnection().
	at io.micronaut.data.connection.support.JdbcConnectionUtils.setConnectionReadOnly(JdbcConnectionUtils.java:170)
	at io.micronaut.data.connection.support.JdbcConnectionUtils.applyReadOnly(JdbcConnectionUtils.java:75)
	at io.micronaut.data.connection.jdbc.operations.DefaultDataSourceConnectionOperations.lambda$setupConnection$0(DefaultDataSourceConnectionOperations.java:67)
	at java.base/java.util.Optional.ifPresent(Optional.java:178)
	at io.micronaut.data.connection.jdbc.operations.DefaultDataSourceConnectionOperations.setupConnection(DefaultDataSourceConnectionOperations.java:65)
	at io.micronaut.data.connection.support.AbstractConnectionOperations.executeWithNewConnection(AbstractConnectionOperations.java:166)
	at io.micronaut.data.connection.support.AbstractConnectionOperations.execute(AbstractConnectionOperations.java:114)
	at io.micronaut.data.jdbc.operations.DefaultJdbcRepositoryOperations.executeRead(DefaultJdbcRepositoryOperations.java:816)
	at io.micronaut.data.jdbc.operations.DefaultJdbcRepositoryOperations.findAll(DefaultJdbcRepositoryOperations.java:557)
	at io.micronaut.data.jdbc.operations.DefaultJdbcRepositoryOperations.findAll(DefaultJdbcRepositoryOperations.java:145)
	at io.micronaut.data.runtime.intercept.DefaultFindAllInterceptor.intercept(DefaultFindAllInterceptor.java:49)
	at io.micronaut.data.runtime.intercept.DataIntroductionAdvice.intercept(DataIntroductionAdvice.java:84)
	at io.micronaut.aop.chain.MethodInterceptorChain.proceed(MethodInterceptorChain.java:143)
	at com.apxeolog.olympus.repository.GridReadRepository$Intercepted.findByGridIdIn(Unknown Source)
	at com.apxeolog.olympus.service.GridRelationService.updateGridRelationsBatch(GridRelationService.java:95)
	at com.apxeolog.olympus.util.TaskProcessor.processLoop(TaskProcessor.java:91)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:545)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:328)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
	at java.base/java.lang.Thread.run(Thread.java:1474)
Caused by: java.sql.SQLException: Cannot change read-only flag after establishing a connection. Use SQLiteConfig#setReadOnly and SQLiteConfig.createConnection().
	at org.sqlite.jdbc3.JDBC3Connection.setReadOnly(JDBC3Connection.java:146)
	at io.micronaut.data.connection.support.JdbcConnectionUtils.setConnectionReadOnly(JdbcConnectionUtils.java:167)

Steps To Reproduce

  1. Simple SQLite setup
... micronaut 4.9.4
annotationProcessor("io.micronaut.data:micronaut-data-processor")
implementation("io.micronaut.data:micronaut-data-jdbc")
implementation("io.micronaut.sql:micronaut-jdbc-hikari")

implementation("org.xerial:sqlite-jdbc:3.50.3.0")
datasources:
  default:
    url: jdbc:sqlite:./data/test.db?journal_mode=WAL&synchronous=NORMAL&cache_size=-32000&temp_store=MEMORY&busy_timeout=5000
    driverClassName: org.sqlite.JDBC
    dialect: H2
    hikari:
      maximum-pool-size: 1
      minimum-idle: 0
@JdbcRepository(dialect = Dialect.H2)
public interface TestRepository extends CrudRepository<Entity, Long> {

}
  1. Call different (read/write) repository methods
testRepository.deleteAll();
testRepository.findAll();

The problem here is that Repository forces a specific connection state, i.e., (call chain):

@NonNull
    @Override
    public <T, R> List<R> findAll(@NonNull PreparedQuery<T, R> preparedQuery) {
        SqlPreparedQuery<T, R> sqlPreparedQuery = getSqlPreparedQuery(preparedQuery);
        return executeRead(connection -> findAll(connection, sqlPreparedQuery, true), sqlPreparedQuery.getInvocationContext());
    }


...

private <I> I executeRead(Function<Connection, I> fn, AnnotationMetadata annotationMetadata) {
        if (!jdbcConfiguration.isAllowConnectionPerOperation() && connectionOperations.findConnectionStatus().isEmpty()) {
            throw connectionNotFoundAndNewNotAllowed();
        }
        return connectionOperations.execute(ConnectionDefinition.READ_ONLY.withAnnotationMetadata(annotationMetadata), status -> {
            Connection connection = status.getConnection();
            applySchema(connection);
            return fn.apply(connection);
        });
    }

...


@Override
    protected void setupConnection(ConnectionStatus<Connection> connectionStatus) {
        connectionStatus.getDefinition().isReadOnly().ifPresent(readOnly -> {
            List<Runnable> onCompleteCallbacks = new ArrayList<>(1);
            JdbcConnectionUtils.applyReadOnly(LOG, connectionStatus.getConnection(), readOnly, onCompleteCallbacks);
            if (!onCompleteCallbacks.isEmpty()) {
                connectionStatus.registerSynchronization(new ConnectionSynchronization() {
                    @Override
                    public void executionComplete() {
                        for (Runnable onCompleteCallback : onCompleteCallbacks) {
                            onCompleteCallback.run();
                        }
                    }
                });
            }
        });
    }

...


public static void applyReadOnly(Logger logger,
                                     Connection connection,
                                     boolean isReadOnly,
                                     List<Runnable> onCompleteCallbacks) {
        boolean connectionReadOnly = isReadOnly(connection);
        if (connectionReadOnly != isReadOnly) {
            setConnectionReadOnly(logger, connection, isReadOnly);
            onCompleteCallbacks.add(() -> setConnectionReadOnly(logger, connection, connectionReadOnly));
        }
    }

If the current connection state does not match the one enforced by the Repository operation, it would call connection.setReadOnly which is not supported by the SQLite driver.
Naturally, if you plan to write anything, you would not create a read-only connection from the beginning, so any READ method from the repository would result in the error

Environment Information

  • Windows 10
  • OpenJDK 25

Example Application

No response

Version

4.9.4

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions