From 5ef424120ed694b1f69daeafaa1454455d13dee2 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Wed, 31 Jan 2024 16:50:01 +0300 Subject: CI: enable SQLite * SQLite JDBC driver does not support generated keys feature. So we implement `returningId` helper * only one connection should be used from connection pooler * changes made to prevent connection locking --- src/main/java/com/juick/ActivityPubManager.java | 20 ++------- .../java/com/juick/service/BaseJdbcService.java | 14 +++++++ .../com/juick/service/MessagesServiceImpl.java | 46 +++++++++++++-------- src/main/java/com/juick/service/TagService.java | 2 - .../java/com/juick/service/TagServiceImpl.java | 22 +++++----- .../java/com/juick/service/UserServiceImpl.java | 22 ++++++---- src/main/java/com/juick/util/ReentrantLockMap.java | 47 ---------------------- src/main/resources/application.properties | 2 +- src/main/resources/schema-h2.sql | 6 +-- src/main/resources/schema-sqlite.sql | 8 ++-- .../java/com/juick/server/tests/ServerTests.java | 17 ++++---- src/test/resources/application-sqlite.yml | 7 ++-- 12 files changed, 89 insertions(+), 124 deletions(-) delete mode 100644 src/main/java/com/juick/util/ReentrantLockMap.java (limited to 'src') diff --git a/src/main/java/com/juick/ActivityPubManager.java b/src/main/java/com/juick/ActivityPubManager.java index f234969b..f4b87b05 100644 --- a/src/main/java/com/juick/ActivityPubManager.java +++ b/src/main/java/com/juick/ActivityPubManager.java @@ -431,8 +431,6 @@ public class ActivityPubManager implements ActivityListener, NotificationListene } } - private static final ReentrantLockMap verificationLock = new ReentrantLockMap(); - public User verifyActor(String method, String path, Map headers) { String signatureString = headers.get("signature"); if (StringUtils.isNotEmpty(signatureString)) { @@ -445,20 +443,10 @@ public class ActivityPubManager implements ActivityListener, NotificationListene // local user key = keystoreManager.getPublicKey(); } else { - ReentrantLock lock = null; - try { - lock = verificationLock.getLock(keyId.toASCIIString()); - lock.lock(); - var context = activityPubService.get(keyId); - if (context.isPresent()) { - actor = (Actor) context.get(); - key = KeystoreManager.publicKeyOf(actor); - } - } finally { - if (lock != null) { - lock.unlock(); - verificationLock.retainLock(keyId.toASCIIString()); - } + var context = activityPubService.get(keyId); + if (context.isPresent()) { + actor = (Actor) context.get(); + key = KeystoreManager.publicKeyOf(actor); } } if (key != null) { diff --git a/src/main/java/com/juick/service/BaseJdbcService.java b/src/main/java/com/juick/service/BaseJdbcService.java index d16dadd2..b51cbdc7 100644 --- a/src/main/java/com/juick/service/BaseJdbcService.java +++ b/src/main/java/com/juick/service/BaseJdbcService.java @@ -52,6 +52,11 @@ public class BaseJdbcService { @Value("#{new Boolean('${spring.sql.init.platform}' == 'mysql')}") private boolean haveNoOffsetDateTime; + @Value("#{new Boolean('${spring.sql.init.platform}' == 'sqlserver')}") + private boolean haveOutput; + @Value("#{new Boolean('${spring.sql.init.platform}' == 'h2')}") + private boolean haveDeltaTables; + public NamedParameterJdbcTemplate getNamedParameterJdbcTemplate() { return namedParameterJdbcTemplate; } @@ -124,4 +129,13 @@ public class BaseJdbcService { } return Types.TIMESTAMP_WITH_TIMEZONE; } + public String returningId(String insertClause, String valuesClause, String idColumn) { + if (haveOutput) { + return insertClause + " OUTPUT inserted."+ idColumn + " " + valuesClause; + } + if (haveDeltaTables) { + return "SELECT " + idColumn + " FROM FINAL TABLE (" + insertClause + " " + valuesClause + ")"; + } + return insertClause + " " + valuesClause + " RETURNING " + idColumn; + } } diff --git a/src/main/java/com/juick/service/MessagesServiceImpl.java b/src/main/java/com/juick/service/MessagesServiceImpl.java index 6874c7bc..1c504a2d 100644 --- a/src/main/java/com/juick/service/MessagesServiceImpl.java +++ b/src/main/java/com/juick/service/MessagesServiceImpl.java @@ -37,6 +37,7 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.lang.NonNull; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -133,22 +134,29 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ @Transactional @Override public int createMessage(final int uid, final String txt, final String attachment, @NonNull final Set tags) { - SimpleJdbcInsert simpleJdbcInsert = new SimpleJdbcInsert(getJdbcTemplate()).withTableName("messages") - .usingColumns("user_id", "attach", "ts", "readonly", "updated", "updated_at", "txt") - .usingGeneratedKeyColumns("message_id"); - var insertMap = new MapSqlParameterSource(); - insertMap.addValue("user_id", uid); var now = Instant.now(); - insertMap.addValue("ts", toDateTime(now.atOffset(ZoneOffset.UTC)), dateTimeType()); - insertMap.addValue("updated", toDateTime(now.atOffset(ZoneOffset.UTC)), dateTimeType()); - insertMap.addValue("updated_at", toDateTime(now.atOffset(ZoneOffset.UTC)), dateTimeType()); - insertMap.addValue("txt", StringUtils.defaultString(txt)); - if (StringUtils.isNotEmpty(attachment)) { - insertMap.addValue("attach", attachment); - } - insertMap.addValue("readonly", TagUtils.hasTag(tags, "readonly")); - int mid = simpleJdbcInsert.executeAndReturnKey(insertMap).intValue(); - if (mid > 0) { + var query = returningId(""" + INSERT INTO messages(user_id, attach, ts, readonly, updated, txt, updated_at)""", + "VALUES(?, ?, ?, ?, ?, ?, ?)", "message_id"); + Integer mid = getJdbcTemplate().execute((ConnectionCallback) con -> { + var nowObject = toDateTime(now.atOffset(ZoneOffset.UTC)); + var stmt = con.prepareStatement(query); + stmt.setInt(1, uid); + stmt.setString(2, attachment); + stmt.setObject(3, nowObject, dateTimeType()); + stmt.setBoolean(4, TagUtils.hasTag(tags, "readonly")); + stmt.setObject(5, nowObject, dateTimeType()); + stmt.setObject(6, StringUtils.defaultString(txt)); + stmt.setObject(7, nowObject, dateTimeType()); + + try (var resultSet = stmt.executeQuery()) { + if (resultSet.next()) { + return resultSet.getInt(1); + } + return 0; + } + }); + if (mid != null && mid > 0) { if (CollectionUtils.isNotEmpty(tags)) { var newTags = new ArrayList<>(tags); getJdbcTemplate().batchUpdate("INSERT INTO messages_tags(message_id, tag_id) VALUES (?, ?)", @@ -819,8 +827,12 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ new MapSqlParameterSource("ids", mids) .addValue("uid", uid), messageMapper); - msgs.forEach(m -> m.setTags(tagService.getMessageTags(m.getMid()).stream() - .map(TagStats::getTag).collect(Collectors.toSet()))); + + msgs.forEach(m -> { + var tags = tagService.getMessageTags(m.getMid()).stream() + .map(TagStats::getTag).collect(Collectors.toSet()); + m.setTags(tags); + }); Map> likes = updateReactionsFor(mids); msgs.forEach(i -> i.setReactions(likes.get(i.getMid()))); msgs.sort(Comparator.comparing(item -> mids.indexOf(item.getMid()))); diff --git a/src/main/java/com/juick/service/TagService.java b/src/main/java/com/juick/service/TagService.java index ef937460..dcdd3a26 100644 --- a/src/main/java/com/juick/service/TagService.java +++ b/src/main/java/com/juick/service/TagService.java @@ -51,12 +51,10 @@ public interface TagService { List getTagStats(); - @CacheEvict(value = "message_tags", key = "#mid") Set updateTags(int mid, Collection newTags); Pair> fromString(String txt); - @Cacheable(value = "message_tags") List getMessageTags(int mid); boolean blacklistTag(User user, Tag tag); diff --git a/src/main/java/com/juick/service/TagServiceImpl.java b/src/main/java/com/juick/service/TagServiceImpl.java index 90d0fc83..9f5c9356 100644 --- a/src/main/java/com/juick/service/TagServiceImpl.java +++ b/src/main/java/com/juick/service/TagServiceImpl.java @@ -24,6 +24,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.PreparedStatementCallback; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.support.GeneratedKeyHolder; @@ -132,19 +133,16 @@ public class TagServiceImpl extends BaseJdbcService implements TagService { @Transactional @Override public int createTag(final String name) { - KeyHolder holder = new GeneratedKeyHolder(); - getJdbcTemplate().update( - con -> { - PreparedStatement stmt = con.prepareStatement( - "INSERT INTO tags(name) VALUES (?)", - Statement.RETURN_GENERATED_KEYS); + return getJdbcTemplate().execute(returningId("INSERT INTO tags(name)", "VALUES (?)", "tag_id"), + (PreparedStatementCallback) stmt -> { stmt.setString(1, name); - return stmt; - }, - holder); - - return holder.getKeys().size() > 1 ? ((Number) holder.getKeys().get("tag_id")).intValue() - : holder.getKey().intValue(); + try (var resultSet = stmt.executeQuery()) { + if (resultSet.next()) { + return resultSet.getInt(1); + } + return 0; + } + }); } private class TagStatsMapper implements RowMapper { diff --git a/src/main/java/com/juick/service/UserServiceImpl.java b/src/main/java/com/juick/service/UserServiceImpl.java index 812a4e8b..0bbeea91 100644 --- a/src/main/java/com/juick/service/UserServiceImpl.java +++ b/src/main/java/com/juick/service/UserServiceImpl.java @@ -27,12 +27,16 @@ import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; +import org.springframework.dao.DataAccessException; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.UncategorizedSQLException; +import org.springframework.jdbc.core.PreparedStatementCallback; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -93,21 +97,25 @@ public class UserServiceImpl extends BaseJdbcService implements UserService { @Transactional @Override public Optional createUser(final String username, final String password) throws UsernameTakenException { - var userInsert = new SimpleJdbcInsert(getJdbcTemplate()) - .withTableName("users") - .usingColumns("nick", "passw") - .usingGeneratedKeyColumns("id"); var params = new MapSqlParameterSource(); params.addValue("nick", username); params.addValue("passw", password); try { - var uid = userInsert.executeAndReturnKey(params).intValue(); + Integer uid = getNamedParameterJdbcTemplate().execute(returningId("INSERT INTO users(nick, passw)", "VALUES (:nick, :passw)", "id"), + params, ps -> { + try (var resultSet = ps.executeQuery()) { + if (resultSet.next()) { + return resultSet.getInt(1); + } + return 0; + } + }); if (uid > 0) { getJdbcTemplate().update("INSERT INTO subscr_users(user_id, suser_id) VALUES (2, ?)", uid); return getUserByUID(uid); } - } catch (DataIntegrityViolationException | UncategorizedSQLException e) { - throw new UsernameTakenException(); + } catch (Exception e) { + return Optional.empty(); } return Optional.empty(); } diff --git a/src/main/java/com/juick/util/ReentrantLockMap.java b/src/main/java/com/juick/util/ReentrantLockMap.java deleted file mode 100644 index 36aee218..00000000 --- a/src/main/java/com/juick/util/ReentrantLockMap.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2008-2023, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.util; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReentrantLock; - -public class ReentrantLockMap { - - ConcurrentHashMap mapStringLock; - - public ReentrantLockMap() { - mapStringLock = new ConcurrentHashMap<>(); - } - - public ReentrantLock getLock(String key) { - ReentrantLock initValue = createIntanceLock(); - ReentrantLock lock = mapStringLock.putIfAbsent(key, initValue); - if (lock == null) { - lock = initValue; - } - return lock; - } - - public void retainLock(String key) { - mapStringLock.remove(key); - } - - protected ReentrantLock createIntanceLock() { - return new ReentrantLock(); - } -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 0ce5b76a..fe206251 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -10,7 +10,7 @@ spring.datasource.generate-unique-name=false spring.flyway.enabled=false spring.sql.init.platform=h2 spring.cache.type=simple -spring.cache.cache-names=help, usernames, users_by_name, discover, discussions, messages, replies, popular_tags, profiles, twitter_user, stats, message_tags +spring.cache.cache-names=help, usernames, users_by_name, discover, discussions, messages, replies, popular_tags, profiles, twitter_user, stats spring.profiles.active=@activatedProperties@ springdoc.swagger-ui.path=/api/swagger-ui.html server.forward-headers-strategy=framework diff --git a/src/main/resources/schema-h2.sql b/src/main/resources/schema-h2.sql index 7aaee493..5fd7fff8 100644 --- a/src/main/resources/schema-h2.sql +++ b/src/main/resources/schema-h2.sql @@ -114,7 +114,7 @@ CREATE MEMORY TABLE "PUBLIC"."REPLIES"( "USER_ID" INTEGER NOT NULL, "REPLYTO" SMALLINT DEFAULT '0' NOT NULL, "TS" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP NOT NULL, - "ATTACH" CHARACTER(3), + "ATTACH" ENUM('jpg', 'mp4', 'png') DEFAULT NULL, "TXT" CHARACTER VARYING NOT NULL, "UPDATED_AT" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP NOT NULL, "USER_URI" CHARACTER VARYING(255) DEFAULT NULL, @@ -131,7 +131,7 @@ CREATE MEMORY TABLE "PUBLIC"."MESSAGES"( "MAXREPLYID" SMALLINT DEFAULT '0' NOT NULL, "PRIVACY" TINYINT DEFAULT '1' NOT NULL, "READONLY" TINYINT DEFAULT '0' NOT NULL, - "ATTACH" CHARACTER(3), + "ATTACH" ENUM('jpg', 'mp4', 'png') DEFAULT NULL, "PLACE_ID" INTEGER DEFAULT NULL, "LAT" DECIMAL(10, 7) DEFAULT NULL, "LON" DECIMAL(10, 7) DEFAULT NULL, @@ -251,8 +251,6 @@ CREATE MEMORY TABLE "PUBLIC"."USER_SERVICES"( "TS" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, "SERVICE_TYPE" CHARACTER VARYING(255) DEFAULT 'fcm' NOT NULL ); -ALTER TABLE "PUBLIC"."MESSAGES" ADD CONSTRAINT "PUBLIC"."CONSTRAINT_131" CHECK("ATTACH" IN('jpg', 'mp4', 'png')) NOCHECK; -ALTER TABLE "PUBLIC"."REPLIES" ADD CONSTRAINT "PUBLIC"."CONSTRAINT_6C" CHECK("ATTACH" IN('jpg', 'mp4', 'png')) NOCHECK; ALTER TABLE "PUBLIC"."LOGINS" ADD CONSTRAINT "PUBLIC"."CONSTRAINT_86" UNIQUE("USER_ID"); ALTER TABLE "PUBLIC"."REACTIONS" ADD CONSTRAINT "PUBLIC"."CONSTRAINT_6" UNIQUE("LIKE_ID"); ALTER TABLE "PUBLIC"."SUBSCR_TAGS" ADD CONSTRAINT "PUBLIC"."CONSTRAINT_B" UNIQUE("TAG_ID", "SUSER_ID"); diff --git a/src/main/resources/schema-sqlite.sql b/src/main/resources/schema-sqlite.sql index b0858f1b..74ba28b6 100644 --- a/src/main/resources/schema-sqlite.sql +++ b/src/main/resources/schema-sqlite.sql @@ -1,5 +1,3 @@ -PRAGMA journal_mode=WAL; -PRAGMA busy_timeout=30000; CREATE TABLE user_services ( user_id INTEGER NOT NULL, regid character varying(1024) NOT NULL, @@ -44,7 +42,7 @@ CREATE TABLE facebook ( ts DEFAULT (strftime('%s','now') || substr(strftime('%f','now'),4)) NOT NULL, fb_name character varying(64), fb_link character varying(255) NOT NULL, - crosspost boolean DEFAULT true NOT NULL, + crosspost boolean DEFAULT TRUE NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ); CREATE TABLE favorites ( @@ -91,7 +89,7 @@ CREATE TABLE messages ( replies smallint DEFAULT (0) NOT NULL, maxreplyid smallint DEFAULT (0) NOT NULL, privacy smallint DEFAULT (1) NOT NULL, - readonly boolean DEFAULT false NOT NULL, + readonly boolean DEFAULT FALSE NOT NULL, attach TEXT CHECK (attach IN ('jpg', 'mp4', 'png')), place_id bigint, lat numeric(10,7), @@ -215,7 +213,7 @@ CREATE TABLE twitter ( access_token_secret character varying(64) NOT NULL, uname character varying(64) NOT NULL, ts DEFAULT (strftime('%s','now') || substr(strftime('%f','now'),4)) NOT NULL, - crosspost boolean DEFAULT true NOT NULL, + crosspost boolean DEFAULT TRUE NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ); CREATE TABLE users ( diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index 81718cab..64fbd030 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -371,10 +371,7 @@ public class ServerTests { int mid2 = messagesService.createMessage(user.getUid(), "yo2", null, tagList); Message msg2 = messagesService.getMessage(mid2).get(); assertEquals(1, msg2.getTags().size()); - Exception exc = assertThrows(UsernameTakenException.class, () -> { - userService.createUser("ugnicH", "x"); - }); - assertEquals("Username taken", exc.getMessage()); + assertThat(userService.createUser("ugnicH", "x").isEmpty(), is(true)); User hugnich = userService.createUser("hugnich", "x").orElseThrow(IllegalStateException::new); int rid = messagesService.createReply(msg2.getMid(), 0, hugnich, "bla-bla", null); assertEquals(1, rid); @@ -929,7 +926,7 @@ public class ServerTests { @Test public void mailParserTest() throws Exception { emailService.addEmail(ugnich.getUid(), "ugnich@example.com"); - int mid = messagesService.createMessage(ugnich.getUid(), "text", StringUtils.EMPTY, Set.of()); + int mid = messagesService.createMessage(ugnich.getUid(), "text", null, Set.of()); String mail = String.format( "MIME-Version: 1.0\n" + "Received: by 10.176.0.242 with HTTP; Fri, 16 Mar 2018 05:31:50 -0700 (PDT)\n" @@ -1974,7 +1971,7 @@ public class ServerTests { String deleteJsonStr = IOUtils.toString(new ClassPathResource("delete.json").getURI(), StandardCharsets.UTF_8); Delete delete = jsonMapper.readValue(deleteJsonStr, Delete.class); - int mid = messagesService.createMessage(ugnich.getUid(), "YO", "", Set.of()); + int mid = messagesService.createMessage(ugnich.getUid(), "YO", null, Set.of()); User extUser = new User(); extUser.setUri(URI.create("http://localhost:8080/users/xwatt")); int rid = messagesService.createReply(mid, 0, extUser, "PEOPLE", null); @@ -2098,7 +2095,7 @@ public class ServerTests { User newUser = userService.createUser("newuser", "assword").orElseThrow(IllegalStateException::new); assertThat(newUser.getUid(), greaterThanOrEqualTo(0)); assertThat(newUser.getSeen(), is(nullValue())); - messagesService.createMessage(newUser.getUid(), "YO", "", Set.of()); + messagesService.createMessage(newUser.getUid(), "YO", null, Set.of()); assertThat(userService.getUserByUID(newUser.getUid()).get().getSeen(), greaterThanOrEqualTo(now)); } @@ -2255,7 +2252,7 @@ public class ServerTests { @Test public void federatedAttachmentsAsLinks() throws Exception { - int mid = messagesService.createMessage(ugnich.getUid(), "test", StringUtils.EMPTY, Set.of()); + int mid = messagesService.createMessage(ugnich.getUid(), "test", null, Set.of()); Message testMessage = MockUtils.mockMessage(mid, freefd, "reply"); String activity = IOUtils.toString(noteWithDocument.getInputStream(), StandardCharsets.UTF_8); Announce announce = jsonMapper.readValue(activity, Announce.class); @@ -2633,7 +2630,7 @@ public class ServerTests { @Test @Transactional public void isSubscribed() { - int mid = messagesService.createMessage(ugnich.getUid(), "test", "", Collections.emptySet()); + int mid = messagesService.createMessage(ugnich.getUid(), "test", null, Collections.emptySet()); var msg = messagesService.getMessage(ugnich.getUid(), mid).orElseThrow(); assertThat(msg.isSubscribed(), is(false)); subscriptionService.subscribeMessage(msg, ugnich); @@ -2661,7 +2658,7 @@ public class ServerTests { @Test public void messagePrivacyFlow() throws Exception { userService.setPremium(ugnich.getUid(), true); - int mid = messagesService.createMessage(ugnich.getUid(), "hidden", "", Collections.emptySet()); + int mid = messagesService.createMessage(ugnich.getUid(), "hidden", null, Collections.emptySet()); var msg = messagesService.getMessage(ugnich.getUid(), mid).orElseThrow(); assertThat(msg.isFriendsOnly(), is(false)); mockMvc.perform(post("/api/messages/set_privacy?mid=" + mid) diff --git a/src/test/resources/application-sqlite.yml b/src/test/resources/application-sqlite.yml index 6a9c0516..9ffa0833 100644 --- a/src/test/resources/application-sqlite.yml +++ b/src/test/resources/application-sqlite.yml @@ -1,8 +1,9 @@ spring: datasource: - maxActive: 1 - type: org.springframework.jdbc.datasource.SimpleDriverDataSource - url: jdbc:sqlite:data.db + hikari: + maximumPoolSize: 1 + connectionTimeout: 3000 + url: jdbc:sqlite:data.db?journal_mode=WAL sql: init: platform: sqlite -- cgit v1.2.3