44import java .sql .PreparedStatement ;
55import java .sql .ResultSet ;
66import java .sql .SQLException ;
7+ import java .sql .Statement ; // NEW
78import java .util .ArrayList ;
89import java .util .List ;
10+ import java .util .Set ; // NEW
911import java .util .UUID ;
12+ import java .util .concurrent .ConcurrentHashMap ; // NEW
1013
1114import com .bencodez .simpleapi .sql .mysql .AbstractSqlTable ;
1215import com .bencodez .simpleapi .sql .mysql .DbType ;
1720
1821public abstract class ProxyOnlineVoteCacheTable extends AbstractSqlTable {
1922
23+ // Prevent repeated startup DDL across multiple instances/subclasses
24+ private static final Set <String > MIGRATED_VOTEID = ConcurrentHashMap .newKeySet ();
25+ private static final Set <String > ENSURED_INDEXES = ConcurrentHashMap .newKeySet ();
26+
2027 @ Override
2128 public String getPrimaryKeyColumn () {
2229 return "id" ;
@@ -67,9 +74,18 @@ public void removeVotesByUuid(String uuid) {
6774 public ProxyOnlineVoteCacheTable (MySQL existingMysql , String tablePrefix , boolean debug ) {
6875 super ((tablePrefix != null ? tablePrefix : "" ) + "votingplugin_onlinevotecache" , existingMysql , debug );
6976
77+ // best-effort migrations (no nested connections)
7078 alterColumnType ("uuid" , bestUuidType ());
71- addVoteIdColumnIfMissing ();
72- ensureIndexes ();
79+ addVoteIdColumnIfMissingOnce ();
80+ ensureIndexesOnce ();
81+ }
82+
83+ public ProxyOnlineVoteCacheTable (MysqlConfig config , boolean debug ) {
84+ super ("votingplugin_onlinevotecache" , config , debug );
85+
86+ alterColumnType ("uuid" , bestUuidType ());
87+ addVoteIdColumnIfMissingOnce ();
88+ ensureIndexesOnce ();
7389 }
7490
7591 public void updateVoteText (OfflineBungeeVote vote , String newText ) {
@@ -100,40 +116,46 @@ public void updateVoteText(OfflineBungeeVote vote, String newText) {
100116 }
101117 }
102118
103- public ProxyOnlineVoteCacheTable (MysqlConfig config , boolean debug ) {
104- super ("votingplugin_onlinevotecache" , config , debug );
119+ private void ensureIndexesOnce () {
120+ if (getDbType () != DbType .POSTGRESQL ) {
121+ return ;
122+ }
105123
106- alterColumnType ("uuid" , bestUuidType ());
107- addVoteIdColumnIfMissing ();
108- ensureIndexes ();
124+ String key = getDbType () + ":" + getTableName () + ":indexes" ;
125+ if (!ENSURED_INDEXES .add (key )) {
126+ return ;
127+ }
128+
129+ // Use ONE connection for all index DDL to avoid pool starvation (pool=1 safe)
130+ try (Connection conn = mysql .getConnectionManager ().getConnection (); Statement st = conn .createStatement ()) {
131+
132+ st .executeUpdate ("CREATE INDEX IF NOT EXISTS idx_uuid ON " + qi (getTableName ()) + " (" + qi ("uuid" ) + ");" );
133+ st .executeUpdate ("CREATE INDEX IF NOT EXISTS idx_time ON " + qi (getTableName ()) + " (" + qi ("time" ) + ");" );
134+
135+ } catch (SQLException e ) {
136+ debug (e );
137+ }
109138 }
110139
111- private void ensureIndexes () {
112- if (getDbType () == DbType .POSTGRESQL ) {
113- try {
114- new Query (mysql ,
115- "CREATE INDEX IF NOT EXISTS idx_uuid ON " + qi (getTableName ()) + " (" + qi ("uuid" ) + ");" )
116- .executeUpdate ();
117- new Query (mysql ,
118- "CREATE INDEX IF NOT EXISTS idx_time ON " + qi (getTableName ()) + " (" + qi ("time" ) + ");" )
119- .executeUpdate ();
120- } catch (SQLException e ) {
121- debug (e );
122- }
140+ private void addVoteIdColumnIfMissingOnce () {
141+ String key = getDbType () + ":" + getTableName () + ":voteid" ;
142+ if (!MIGRATED_VOTEID .add (key )) {
143+ return ;
123144 }
145+ addVoteIdColumnIfMissing ();
124146 }
125147
126148 private void addVoteIdColumnIfMissing () {
127- String schemaFilter ;
128- if (getDbType () == DbType .POSTGRESQL ) {
129- schemaFilter = "table_schema = current_schema()" ;
130- } else {
131- schemaFilter = "TABLE_SCHEMA = DATABASE()" ;
132- }
149+ final boolean pg = getDbType () == DbType .POSTGRESQL ;
150+
151+ final String schemaFilter = pg ? "table_schema = current_schema()" : "TABLE_SCHEMA = DATABASE()" ;
133152
134- String checkSql = "SELECT 1 FROM information_schema.columns WHERE " + schemaFilter
135- + " AND table_name = ? AND column_name = ? LIMIT 1;" ;
153+ // Lowercase compare avoids casing issues on some setups
154+ final String checkSql = "SELECT 1 FROM information_schema.columns WHERE " + schemaFilter
155+ + " AND LOWER(table_name) = LOWER(?) AND LOWER(column_name) = LOWER(?) LIMIT 1;" ;
136156
157+ // IMPORTANT: do check + alter on SAME connection to avoid deadlock when
158+ // poolSize=1
137159 try (Connection conn = mysql .getConnectionManager ().getConnection ();
138160 PreparedStatement ps = conn .prepareStatement (checkSql )) {
139161
@@ -142,12 +164,14 @@ private void addVoteIdColumnIfMissing() {
142164
143165 try (ResultSet rs = ps .executeQuery ()) {
144166 if (rs .next ()) {
145- return ;
167+ return ; // exists
146168 }
147169 }
148170
149171 String alter = "ALTER TABLE " + qi (getTableName ()) + " ADD COLUMN " + qi ("voteid" ) + " VARCHAR(36);" ;
150- new Query (mysql , alter ).executeUpdate ();
172+ try (Statement st = conn .createStatement ()) {
173+ st .executeUpdate (alter );
174+ }
151175
152176 } catch (SQLException e ) {
153177 logSevere ("Failed to add voteid column to " + getTableName () + ": " + e .getMessage ());
@@ -162,7 +186,7 @@ public void insertVote(UUID voteId, String uuid, String playerName, String servi
162186
163187 String sql = "INSERT INTO " + qi (getTableName ()) + " (" + qi ("uuid" ) + ", " + qi ("voteid" ) + ", "
164188 + qi ("playerName" ) + ", " + qi ("service" ) + ", " + qi ("time" ) + ", " + qi ("realVote" ) + ", "
165- + qi ("text" ) + ") " + " VALUES (?, ?, ?, ?, ?, ?, ?);" ;
189+ + qi ("text" ) + ") VALUES (?, ?, ?, ?, ?, ?, ?);" ;
166190
167191 try (Connection conn = mysql .getConnectionManager ().getConnection ();
168192 PreparedStatement ps = conn .prepareStatement (sql )) {
0 commit comments