Skip to content

Commit 1421469

Browse files
committed
Ass back support for ucp and sqlite
1 parent 45cb347 commit 1421469

14 files changed

Lines changed: 608 additions & 5 deletions

File tree

jdbc-ucp/src/main/java/io/micronaut/configuration/jdbc/ucp/DatasourceFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public PoolDataSource dataSource(DatasourceConfiguration datasourceConfiguration
8585
PoolDataSource ds = datasourceConfiguration.getPoolDataSource();
8686
dataSources.put(datasourceConfiguration.getName(), ds);
8787

88-
return ds;
88+
return UcpSqliteProxyFactory.wrap(ds, datasourceConfiguration.getDriverClassName(), datasourceConfiguration.getUrl());
8989
}
9090

9191
/**
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2017-2026 original authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micronaut.configuration.jdbc.ucp;
17+
18+
import io.micronaut.core.annotation.Internal;
19+
import io.micronaut.jdbc.DelegatingDataSource;
20+
import io.micronaut.jdbc.JdbcSqliteSupport;
21+
import oracle.ucp.jdbc.PoolDataSource;
22+
23+
import java.lang.reflect.InvocationHandler;
24+
import java.lang.reflect.InvocationTargetException;
25+
import java.lang.reflect.Method;
26+
import java.lang.reflect.Proxy;
27+
import java.sql.Connection;
28+
import java.sql.SQLException;
29+
30+
/**
31+
* Creates SQLite-aware proxies for UCP datasources while preserving the {@link PoolDataSource} type.
32+
*
33+
* @since 7.0.0
34+
*/
35+
@Internal
36+
final class UcpSqliteProxyFactory {
37+
38+
private UcpSqliteProxyFactory() {
39+
}
40+
41+
static PoolDataSource wrap(PoolDataSource dataSource, String driverClassName, String jdbcUrl) {
42+
if (!JdbcSqliteSupport.isSqlite(driverClassName, jdbcUrl) || dataSource instanceof DelegatingDataSource) {
43+
return dataSource;
44+
}
45+
return (PoolDataSource) Proxy.newProxyInstance(
46+
dataSource.getClass().getClassLoader(),
47+
new Class<?>[]{PoolDataSource.class, DelegatingDataSource.class},
48+
new PoolDataSourceInvocationHandler(dataSource)
49+
);
50+
}
51+
52+
private static final class PoolDataSourceInvocationHandler implements InvocationHandler {
53+
54+
private final PoolDataSource target;
55+
56+
private PoolDataSourceInvocationHandler(PoolDataSource target) {
57+
this.target = target;
58+
}
59+
60+
@Override
61+
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
62+
String methodName = method.getName();
63+
if ("getTargetDataSource".equals(methodName) && (args == null || args.length == 0)) {
64+
return target;
65+
}
66+
if ("getConnection".equals(methodName)) {
67+
return wrapConnection(method, args);
68+
}
69+
if ("unwrap".equals(methodName) && args != null && args.length == 1 && args[0] instanceof Class<?> unwrapType) {
70+
if (unwrapType.isInstance(proxy)) {
71+
return proxy;
72+
}
73+
if (unwrapType.isInstance(target)) {
74+
return target;
75+
}
76+
return target.unwrap(unwrapType);
77+
}
78+
if ("isWrapperFor".equals(methodName) && args != null && args.length == 1 && args[0] instanceof Class<?> unwrapType) {
79+
return unwrapType.isInstance(proxy) || unwrapType.isInstance(target) || target.isWrapperFor(unwrapType);
80+
}
81+
if ("equals".equals(methodName) && args != null && args.length == 1) {
82+
return proxy == args[0];
83+
}
84+
if ("hashCode".equals(methodName) && (args == null || args.length == 0)) {
85+
return System.identityHashCode(proxy);
86+
}
87+
if ("toString".equals(methodName) && (args == null || args.length == 0)) {
88+
return target.toString();
89+
}
90+
try {
91+
return method.invoke(target, args);
92+
} catch (InvocationTargetException e) {
93+
throw e.getCause();
94+
}
95+
}
96+
97+
private Connection wrapConnection(Method method, Object[] args) throws Throwable {
98+
try {
99+
return JdbcSqliteSupport.wrapSqliteConnection((Connection) method.invoke(target, args));
100+
} catch (InvocationTargetException e) {
101+
throw e.getCause();
102+
} catch (SQLException e) {
103+
throw e;
104+
}
105+
}
106+
}
107+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package io.micronaut.configuration.jdbc.ucp
2+
3+
import io.micronaut.jdbc.DataSourceResolver
4+
import io.micronaut.jdbc.SQLiteAwareDataSourceResolver
5+
import oracle.ucp.jdbc.PoolDataSource
6+
import spock.lang.Specification
7+
8+
import java.sql.Connection
9+
import java.sql.SQLException
10+
11+
class UcpSqliteProxyFactorySpec extends Specification {
12+
13+
private static final String SQLITE_READ_ONLY_MESSAGE = "Cannot change read-only flag after establishing a connection. Use SQLiteConfig#setReadOnly and SQLiteConfig.createConnection()."
14+
15+
void "sqlite proxy preserves pool data source type and unwraps target"() {
16+
given:
17+
Connection connection = Mock() {
18+
isReadOnly() >> false
19+
setReadOnly(true) >> { throw new SQLException(SQLITE_READ_ONLY_MESSAGE) }
20+
}
21+
PoolDataSource target = Mock() {
22+
getConnection() >> connection
23+
}
24+
DataSourceResolver resolver = new SQLiteAwareDataSourceResolver()
25+
26+
when:
27+
PoolDataSource proxied = UcpSqliteProxyFactory.wrap(target, "org.sqlite.JDBC", "jdbc:sqlite:file:test")
28+
29+
then:
30+
proxied instanceof PoolDataSource
31+
resolver.resolve(proxied).is(target)
32+
33+
when:
34+
Connection wrapped = proxied.getConnection()
35+
wrapped.setReadOnly(true)
36+
37+
then:
38+
noExceptionThrown()
39+
wrapped.isReadOnly()
40+
}
41+
42+
void "non sqlite pool data source is unchanged"() {
43+
given:
44+
PoolDataSource target = Mock()
45+
46+
expect:
47+
UcpSqliteProxyFactory.wrap(target, "org.h2.Driver", "jdbc:h2:mem:test").is(target)
48+
}
49+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2017-2026 original authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micronaut.jdbc;
17+
18+
import io.micronaut.core.annotation.Internal;
19+
20+
import javax.sql.DataSource;
21+
22+
/**
23+
* Internal contract for datasource wrappers that can expose their underlying target datasource.
24+
*
25+
* @since 7.0.0
26+
*/
27+
@Internal
28+
public interface DelegatingDataSource extends DataSource {
29+
30+
/**
31+
* @return The underlying target datasource
32+
*/
33+
DataSource getTargetDataSource();
34+
}

jdbc/src/main/java/io/micronaut/jdbc/JdbcSqliteSupport.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,15 +105,16 @@ public static Connection wrapSqliteConnection(Connection connection) throws SQLE
105105
);
106106
}
107107

108-
static final class SQLiteAwareDataSource implements DataSource {
108+
static final class SQLiteAwareDataSource implements DelegatingDataSource {
109109

110110
private final DataSource targetDataSource;
111111

112112
private SQLiteAwareDataSource(DataSource targetDataSource) {
113113
this.targetDataSource = targetDataSource;
114114
}
115115

116-
DataSource getTargetDataSource() {
116+
@Override
117+
public DataSource getTargetDataSource() {
117118
return targetDataSource;
118119
}
119120

jdbc/src/main/java/io/micronaut/jdbc/SQLiteAwareDataSourceResolver.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ public final class SQLiteAwareDataSourceResolver implements DataSourceResolver {
3131

3232
@Override
3333
public DataSource resolve(DataSource dataSource) {
34-
if (dataSource instanceof JdbcSqliteSupport.SQLiteAwareDataSource sqliteAwareDataSource) {
35-
return sqliteAwareDataSource.getTargetDataSource();
34+
if (dataSource instanceof DelegatingDataSource delegatingDataSource) {
35+
return delegatingDataSource.getTargetDataSource();
3636
}
3737
return dataSource;
3838
}

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ include 'tests:jooq-tests:jooq-r2dbc-postgres'
6969
include 'tests:vertx-pg-client-native'
7070

7171
include 'tests:jdbc-ucp-tests:jdbc-ucp-oracle'
72+
include 'tests:jdbc-ucp-tests:jdbc-ucp-sqlite'
7273
include 'tests:jdbc-dbcp-tests:jdbc-dbcp-postgres'
7374
include 'tests:jdbc-hikari-tests:jdbc-hikari-h2'
7475
include 'tests:jdbc-hikari-tests:jdbc-hikari-sqlite'
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
plugins {
2+
id 'io.micronaut.build.internal.test-application'
3+
}
4+
5+
dependencies {
6+
implementation projects.micronautJdbcUcp
7+
implementation projects.micronautTests.micronautCommonSync
8+
9+
runtimeOnly libs.managed.sqlite.jdbc
10+
11+
testRuntimeOnly(mnLogging.logback.classic)
12+
testImplementation projects.micronautTests.micronautCommonTests
13+
testImplementation(mnData.micronaut.data.tx.jdbc)
14+
}
15+
16+
micronaut {
17+
testResources {
18+
enabled = false
19+
}
20+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package example.jdbc.ucp.sqlite;
2+
3+
import example.domain.IOwner;
4+
import io.micronaut.core.annotation.Creator;
5+
import io.micronaut.serde.annotation.Serdeable;
6+
7+
@Serdeable
8+
public class Owner implements IOwner {
9+
10+
private Long id;
11+
private String name;
12+
private int age;
13+
14+
Owner() {
15+
}
16+
17+
@Creator
18+
public Owner(Long id, String name, int age) {
19+
this.id = id;
20+
this.name = name;
21+
this.age = age;
22+
}
23+
24+
@Override
25+
public Long getId() {
26+
return id;
27+
}
28+
29+
public void setId(Long id) {
30+
this.id = id;
31+
}
32+
33+
@Override
34+
public String getName() {
35+
return name;
36+
}
37+
38+
@Override
39+
public void setName(String name) {
40+
this.name = name;
41+
}
42+
43+
@Override
44+
public int getAge() {
45+
return age;
46+
}
47+
48+
@Override
49+
public void setAge(int age) {
50+
this.age = age;
51+
}
52+
}

0 commit comments

Comments
 (0)