Ether Framework
Unified API docs for Ether modules
Loading...
Searching...
No Matches
SQLiteDatabaseClient.java
Go to the documentation of this file.
1package dev.rafex.ether.database.sqlite.client;
2
3/*-
4 * #%L
5 * ether-database-sqlite
6 * %%
7 * Copyright (C) 2025 - 2026 Raúl Eduardo González Argote
8 * %%
9 * Permission is hereby granted, free of charge, to any person obtaining a copy
10 * of this software and associated documentation files (the "Software"), to deal
11 * in the Software without restriction, including without limitation the rights
12 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 * copies of the Software, and to permit persons to whom the Software is
14 * furnished to do so, subject to the following conditions:
15 *
16 * The above copyright notice and this permission notice shall be included in
17 * all copies or substantial portions of the Software.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 * THE SOFTWARE.
26 * #L%
27 */
28
29import java.sql.Connection;
30import java.sql.SQLException;
31import java.util.List;
32import java.util.Objects;
33import java.util.Optional;
34
35import javax.sql.DataSource;
36
37import dev.rafex.ether.database.core.DatabaseClient;
38import dev.rafex.ether.database.core.exceptions.DatabaseAccessException;
39import dev.rafex.ether.database.core.mapping.ResultSetExtractor;
40import dev.rafex.ether.database.core.mapping.RowMapper;
41import dev.rafex.ether.database.core.sql.SqlQuery;
42import dev.rafex.ether.database.core.sql.StatementBinder;
43import dev.rafex.ether.database.core.transaction.TransactionCallback;
44import dev.rafex.ether.database.sqlite.config.SQLiteConfig;
45import dev.rafex.ether.database.sqlite.config.SQLitePragmas;
46import dev.rafex.ether.database.sqlite.errors.SQLiteErrorClassifier;
47import dev.rafex.ether.jdbc.client.JdbcDatabaseClient;
48
49/**
50 * SQLite-specific wrapper around {@link JdbcDatabaseClient}.
51 * <p>
52 * This class provides SQLite-specific configuration and error handling
53 * while delegating all database operations to {@link JdbcDatabaseClient}.
54 */
55public class SQLiteDatabaseClient implements DatabaseClient {
56
57 private final JdbcDatabaseClient delegate;
58 private final Optional<SQLiteConfig> config;
59
60 /**
61 * Creates a new {@code SQLiteDatabaseClient} with the specified data source.
62 *
63 * @param dataSource the data source to use for database connections
64 * @throws NullPointerException if {@code dataSource} is {@code null}
65 */
66 public SQLiteDatabaseClient(final DataSource dataSource) {
67 this(new JdbcDatabaseClient(dataSource), Optional.empty());
68 }
69
70 /**
71 * Creates a new {@code SQLiteDatabaseClient} with the specified data source
72 * and configuration.
73 *
74 * @param dataSource the data source to use for database connections
75 * @param config the SQLite configuration to apply
76 * @throws NullPointerException if {@code dataSource} or {@code config} is {@code null}
77 */
78 public SQLiteDatabaseClient(final DataSource dataSource, final SQLiteConfig config) {
79 this(new JdbcDatabaseClient(dataSource), Optional.of(Objects.requireNonNull(config, "config")));
80 }
81
82 private SQLiteDatabaseClient(final JdbcDatabaseClient delegate, final Optional<SQLiteConfig> config) {
83 this.delegate = Objects.requireNonNull(delegate, "delegate");
84 this.config = Objects.requireNonNull(config, "config");
85 }
86
87 /**
88 * Returns a builder for creating {@code SQLiteDatabaseClient} instances.
89 *
90 * @param dataSource the data source to use
91 * @return a builder instance
92 */
93 public static Builder builder(final DataSource dataSource) {
94 return new Builder(dataSource);
95 }
96
97 /**
98 * Builder for {@link SQLiteDatabaseClient}.
99 */
100 public static final class Builder {
101 private final DataSource dataSource;
102 private SQLiteConfig config;
103
104 private Builder(final DataSource dataSource) {
105 this.dataSource = Objects.requireNonNull(dataSource, "dataSource");
106 this.config = SQLiteConfig.defaults();
107 }
108
109 /**
110 * Enables Write-Ahead Logging (WAL) mode.
111 *
112 * @return this builder
113 */
114 public Builder withWalEnabled() {
115 this.config = SQLiteConfig.builder()
116 .journalMode(dev.rafex.ether.database.sqlite.config.JournalMode.WAL)
117 .synchronousMode(config.synchronousMode())
118 .foreignKeys(config.foreignKeys())
119 .busyTimeout(config.busyTimeout())
120 .caseSensitiveLike(config.caseSensitiveLike())
121 .recursiveTriggers(config.recursiveTriggers())
122 .autoVacuum(config.autoVacuum())
123 .build();
124 return this;
125 }
126
127 /**
128 * Sets the synchronous mode.
129 *
130 * @param mode the synchronous mode
131 * @return this builder
132 */
133 public Builder withSynchronousMode(final dev.rafex.ether.database.sqlite.config.SynchronousMode mode) {
134 this.config = SQLiteConfig.builder()
135 .journalMode(config.journalMode())
136 .synchronousMode(mode)
137 .foreignKeys(config.foreignKeys())
138 .busyTimeout(config.busyTimeout())
139 .caseSensitiveLike(config.caseSensitiveLike())
140 .recursiveTriggers(config.recursiveTriggers())
141 .autoVacuum(config.autoVacuum())
142 .build();
143 return this;
144 }
145
146 /**
147 * Sets the journal mode.
148 *
149 * @param mode the journal mode
150 * @return this builder
151 */
152 public Builder withJournalMode(final dev.rafex.ether.database.sqlite.config.JournalMode mode) {
153 this.config = SQLiteConfig.builder()
154 .journalMode(mode)
155 .synchronousMode(config.synchronousMode())
156 .foreignKeys(config.foreignKeys())
157 .busyTimeout(config.busyTimeout())
158 .caseSensitiveLike(config.caseSensitiveLike())
159 .recursiveTriggers(config.recursiveTriggers())
160 .autoVacuum(config.autoVacuum())
161 .build();
162 return this;
163 }
164
165 /**
166 * Enables or disables foreign key constraints.
167 *
168 * @param enabled {@code true} to enable foreign keys, {@code false} to disable
169 * @return this builder
170 */
171 public Builder withForeignKeys(final boolean enabled) {
172 this.config = SQLiteConfig.builder()
173 .journalMode(config.journalMode())
174 .synchronousMode(config.synchronousMode())
175 .foreignKeys(enabled)
176 .busyTimeout(config.busyTimeout())
177 .caseSensitiveLike(config.caseSensitiveLike())
178 .recursiveTriggers(config.recursiveTriggers())
179 .autoVacuum(config.autoVacuum())
180 .build();
181 return this;
182 }
183
184 /**
185 * Sets the busy timeout in milliseconds.
186 *
187 * @param timeout the busy timeout in milliseconds
188 * @return this builder
189 */
190 public Builder withBusyTimeout(final int timeout) {
191 this.config = SQLiteConfig.builder()
192 .journalMode(config.journalMode())
193 .synchronousMode(config.synchronousMode())
194 .foreignKeys(config.foreignKeys())
195 .busyTimeout(timeout)
196 .caseSensitiveLike(config.caseSensitiveLike())
197 .recursiveTriggers(config.recursiveTriggers())
198 .autoVacuum(config.autoVacuum())
199 .build();
200 return this;
201 }
202
203 /**
204 * Sets a custom SQLite configuration.
205 *
206 * @param config the SQLite configuration
207 * @return this builder
208 */
209 public Builder withConfig(final SQLiteConfig config) {
210 this.config = Objects.requireNonNull(config, "config");
211 return this;
212 }
213
214 /**
215 * Builds the {@code SQLiteDatabaseClient}.
216 *
217 * @return a new {@code SQLiteDatabaseClient} instance
218 */
219 public SQLiteDatabaseClient build() {
220 return new SQLiteDatabaseClient(dataSource, config);
221 }
222 }
223
224 /**
225 * Returns the SQLite configuration, if present.
226 *
227 * @return an optional containing the SQLite configuration, or empty if not configured
228 */
229 public Optional<SQLiteConfig> getConfig() {
230 return config;
231 }
232
233 /**
234 * Wraps a SQL exception with SQLite-specific error classification.
235 *
236 * @param message the error message
237 * @param cause the SQL exception
238 * @return a classified runtime exception
239 */
240 private RuntimeException wrapException(final String message, final SQLException cause) {
241 // For now, just use DatabaseAccessException
242 // In the future, we could add SQLite-specific exception types
243 return new DatabaseAccessException(message, cause);
244 }
245
246 /**
247 * Executes an operation with SQLite error handling.
248 *
249 * @param operation the database operation to execute
250 * @param <T> the return type
251 * @return the operation result
252 */
253 private <T> T executeWithErrorHandling(final DatabaseOperation<T> operation) {
254 try {
255 return operation.execute();
256 } catch (DatabaseAccessException e) {
257 // Re-throw with SQLite error classification if the cause is SQLException
258 Throwable cause = e.getCause();
259 if (cause instanceof SQLException) {
260 throw wrapException(e.getMessage(), (SQLException) cause);
261 }
262 throw e;
263 }
264 }
265
266 @Override
267 public <T> T query(final SqlQuery query, final ResultSetExtractor<T> extractor) {
268 return executeWithErrorHandling(() -> delegate.query(query, extractor));
269 }
270
271 @Override
272 public <T> List<T> queryList(final SqlQuery query, final RowMapper<T> mapper) {
273 return executeWithErrorHandling(() -> delegate.queryList(query, mapper));
274 }
275
276 @Override
277 public <T> Optional<T> queryOne(final SqlQuery query, final RowMapper<T> mapper) {
278 return executeWithErrorHandling(() -> delegate.queryOne(query, mapper));
279 }
280
281 @Override
282 public int execute(final SqlQuery query) {
283 return executeWithErrorHandling(() -> delegate.execute(query));
284 }
285
286 @Override
287 public long[] batch(final String sql, final List<StatementBinder> binders) {
288 return executeWithErrorHandling(() -> delegate.batch(sql, binders));
289 }
290
291 @Override
292 public <T> T inTransaction(final TransactionCallback<T> callback) {
293 return executeWithErrorHandling(() -> {
294 // JdbcDatabaseClient doesn't implement TransactionRunner,
295 // so we need to implement transaction handling ourselves
296 // For now, throw UnsupportedOperationException
297 throw new UnsupportedOperationException(
298 "Transaction support not implemented in SQLiteDatabaseClient. " +
299 "Use JdbcDatabaseClient directly for transaction support."
300 );
301 });
302 }
303
304 /**
305 * Functional interface for database operations.
306 */
307 @FunctionalInterface
308 private interface DatabaseOperation<T> {
310 }
311}
static Builder builder(final DataSource dataSource)
Returns a builder for creating SQLiteDatabaseClient instances.
SQLiteDatabaseClient(final DataSource dataSource, final SQLiteConfig config)
Creates a new SQLiteDatabaseClient with the specified data source and configuration.
Optional< SQLiteConfig > getConfig()
Returns the SQLite configuration, if present.
long[] batch(final String sql, final List< StatementBinder > binders)
int execute(final SqlQuery query)
Executes a SQL statement (INSERT, UPDATE, DELETE, or stored procedure call).
SQLiteDatabaseClient(final DataSource dataSource)
Creates a new SQLiteDatabaseClient with the specified data source.
Configuration for SQLite database connections.
static SQLiteConfig defaults()
Returns the default configuration.
static Builder builder()
Creates a new builder with default values:
Implementation of DatabaseClient using JDBC.
Typed configuration entry points and composition APIs for Ether applications.
SQLite configuration and PRAGMA utilities.
SQLite-specific SQL parameter helpers.