44import java .sql .PreparedStatement ;
55import java .sql .ResultSet ;
66import java .sql .SQLException ;
7+ import java .sql .Statement ;
78import java .util .ArrayList ;
89import java .util .HashSet ;
910import java .util .List ;
1011import java .util .Set ;
1112import java .util .UUID ;
13+ import java .util .concurrent .ConcurrentHashMap ;
1214
1315import com .bencodez .simpleapi .sql .mysql .AbstractSqlTable ;
1416import com .bencodez .simpleapi .sql .mysql .DbType ;
2830 */
2931public 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