Skip to content

Commit 499e4c9

Browse files
committed
Refactor database migration handling
1 parent 59bd789 commit 499e4c9

File tree

1 file changed

+60
-49
lines changed

1 file changed

+60
-49
lines changed

VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/cache/ProxyVoteCacheTable.java

Lines changed: 60 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
import java.sql.PreparedStatement;
55
import java.sql.ResultSet;
66
import java.sql.SQLException;
7+
import java.sql.Statement;
78
import java.util.ArrayList;
89
import java.util.HashSet;
910
import java.util.List;
1011
import java.util.Set;
1112
import java.util.UUID;
13+
import java.util.concurrent.ConcurrentHashMap;
1214

1315
import com.bencodez.simpleapi.sql.mysql.AbstractSqlTable;
1416
import com.bencodez.simpleapi.sql.mysql.DbType;
@@ -28,6 +30,11 @@
2830
*/
2931
public abstract class ProxyVoteCacheTable extends AbstractSqlTable {
3032

33+
// Ensure we don't run the same migration repeatedly during startup (or for each
34+
// subclass).
35+
private static final Set<String> MIGRATED_VOTEID = ConcurrentHashMap.newKeySet();
36+
private static final Set<String> ENSURED_INDEXES = ConcurrentHashMap.newKeySet();
37+
3138
// ---- Required hooks ----
3239

3340
@Override
@@ -105,71 +112,83 @@ public void updateVoteText(OfflineBungeeVote vote, String server, String newText
105112
public ProxyVoteCacheTable(MySQL existingMysql, String tablePrefix, boolean debug) {
106113
super((tablePrefix != null ? tablePrefix : "") + "votingplugin_votecache", existingMysql, debug);
107114

108-
// best-effort migrations
115+
// best-effort migrations (safe for pool size 1; no nested connections)
109116
alterColumnType("uuid", bestUuidType());
110-
addVoteIdColumnIfMissing();
111-
ensureIndexes();
117+
addVoteIdColumnIfMissingOnce();
118+
ensureIndexesOnce();
112119
}
113120

114121
public ProxyVoteCacheTable(MysqlConfig config, boolean debug) {
115122
super("votingplugin_votecache", config, debug);
116123

117124
alterColumnType("uuid", bestUuidType());
118-
addVoteIdColumnIfMissing();
119-
ensureIndexes();
125+
addVoteIdColumnIfMissingOnce();
126+
ensureIndexesOnce();
120127
}
121128

122129
// ---- Migrations / indexes ----
123130

124-
private void ensureIndexes() {
125-
// Postgres doesn't support index defs inside CREATE TABLE the same way we do
126-
// for MySQL.
127-
// Create them best-effort; ignore duplicate errors on MySQL.
128-
if (getDbType() == DbType.POSTGRESQL) {
129-
try {
130-
new Query(mysql,
131-
"CREATE INDEX IF NOT EXISTS idx_server ON " + qi(getTableName()) + " (" + qi("server") + ");")
132-
.executeUpdate();
133-
new Query(mysql,
134-
"CREATE INDEX IF NOT EXISTS idx_uuid ON " + qi(getTableName()) + " (" + qi("uuid") + ");")
135-
.executeUpdate();
136-
new Query(mysql,
137-
"CREATE INDEX IF NOT EXISTS idx_time ON " + qi(getTableName()) + " (" + qi("time") + ");")
138-
.executeUpdate();
139-
} catch (SQLException e) {
140-
debug(e);
141-
}
142-
} else {
143-
// MySQL indexes are already in CREATE TABLE; nothing required here.
131+
private void ensureIndexesOnce() {
132+
if (getDbType() != DbType.POSTGRESQL) {
133+
return; // MySQL indexes are already in CREATE TABLE
134+
}
135+
136+
final String key = getDbType() + ":" + getTableName();
137+
if (!ENSURED_INDEXES.add(key)) {
138+
return;
139+
}
140+
141+
// Use ONE connection for all index statements (no pool churn).
142+
try (Connection conn = mysql.getConnectionManager().getConnection(); Statement st = conn.createStatement()) {
143+
144+
st.executeUpdate(
145+
"CREATE INDEX IF NOT EXISTS idx_server ON " + qi(getTableName()) + " (" + qi("server") + ");");
146+
st.executeUpdate("CREATE INDEX IF NOT EXISTS idx_uuid ON " + qi(getTableName()) + " (" + qi("uuid") + ");");
147+
st.executeUpdate("CREATE INDEX IF NOT EXISTS idx_time ON " + qi(getTableName()) + " (" + qi("time") + ");");
148+
149+
} catch (SQLException e) {
150+
debug(e);
144151
}
145152
}
146153

147-
private void addVoteIdColumnIfMissing() {
148-
// Works in both MySQL and Postgres using information_schema
149-
String schemaFilter;
150-
if (getDbType() == DbType.POSTGRESQL) {
151-
schemaFilter = "table_schema = current_schema()";
152-
} else {
153-
schemaFilter = "TABLE_SCHEMA = DATABASE()";
154+
private void addVoteIdColumnIfMissingOnce() {
155+
final String key = getDbType() + ":" + getTableName() + ":voteid";
156+
if (!MIGRATED_VOTEID.add(key)) {
157+
return;
154158
}
159+
addVoteIdColumnIfMissing();
160+
}
161+
162+
private void addVoteIdColumnIfMissing() {
163+
// Avoid nested connections: do check + alter using the SAME connection.
164+
final boolean pg = getDbType() == DbType.POSTGRESQL;
155165

156-
String checkSql = "SELECT 1 FROM information_schema.columns WHERE " + schemaFilter
157-
+ " AND table_name = ? AND column_name = ? LIMIT 1;";
166+
final String schemaFilter = pg ? "table_schema = current_schema()" : "TABLE_SCHEMA = DATABASE()";
167+
168+
// Case-insensitive match helps with MySQL lower_case_table_names / quoting
169+
// differences.
170+
final String checkSql = "SELECT 1 FROM information_schema.columns WHERE " + schemaFilter
171+
+ " AND LOWER(table_name) = LOWER(?) AND LOWER(column_name) = LOWER(?) LIMIT 1;";
158172

159173
try (Connection conn = mysql.getConnectionManager().getConnection();
160174
PreparedStatement ps = conn.prepareStatement(checkSql)) {
161175

162176
ps.setString(1, getTableName());
163-
ps.setString(2, getDbType() == DbType.POSTGRESQL ? "voteid" : "voteid");
177+
ps.setString(2, "voteid");
164178

165179
try (ResultSet rs = ps.executeQuery()) {
166180
if (rs.next()) {
167181
return; // exists
168182
}
169183
}
170184

171-
String alter = "ALTER TABLE " + qi(getTableName()) + " ADD COLUMN " + qi("voteid") + " VARCHAR(36);";
172-
new Query(mysql, alter).executeUpdate();
185+
// Execute ALTER using same connection to avoid deadlock when pool size is
186+
// small.
187+
final String alter = "ALTER TABLE " + qi(getTableName()) + " ADD COLUMN " + qi("voteid") + " VARCHAR(36);";
188+
189+
try (Statement st = conn.createStatement()) {
190+
st.executeUpdate(alter);
191+
}
173192

174193
} catch (SQLException e) {
175194
// don't hard fail startup
@@ -185,7 +204,7 @@ public void insertVote(UUID voteId, String uuid, String playerName, String servi
185204

186205
String sql = "INSERT INTO " + qi(getTableName()) + " (" + qi("uuid") + ", " + qi("voteid") + ", "
187206
+ qi("playerName") + ", " + qi("service") + ", " + qi("time") + ", " + qi("realVote") + ", "
188-
+ qi("text") + ", " + qi("server") + ") " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?);";
207+
+ qi("text") + ", " + qi("server") + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?);";
189208

190209
try (Connection conn = mysql.getConnectionManager().getConnection();
191210
PreparedStatement ps = conn.prepareStatement(sql)) {
@@ -234,17 +253,9 @@ public VoteRow getVoteById(int id) {
234253
}
235254

236255
public List<VoteRow> getVotesByUUID(String uuid) {
237-
String sql;
238-
Object[] params;
239-
240-
if (getDbType() == DbType.POSTGRESQL) {
241-
sql = "SELECT * FROM " + qi(getTableName()) + " WHERE " + qi("uuid") + " = ?;";
242-
params = new Object[] { UUID.fromString(uuid) };
243-
} else {
244-
sql = "SELECT * FROM " + qi(getTableName()) + " WHERE " + qi("uuid") + " = ?;";
245-
params = new Object[] { uuid };
246-
}
247-
256+
String sql = "SELECT * FROM " + qi(getTableName()) + " WHERE " + qi("uuid") + " = ?;";
257+
Object[] params = (getDbType() == DbType.POSTGRESQL) ? new Object[] { UUID.fromString(uuid) }
258+
: new Object[] { uuid };
248259
return selectVotes(sql, params);
249260
}
250261

0 commit comments

Comments
 (0)