Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.43% covered (warning)
69.43%
947 / 1364
36.90% covered (danger)
36.90%
69 / 187
CRAP
0.00% covered (danger)
0.00%
0 / 1
Database
69.43% covered (warning)
69.43%
947 / 1364
36.90% covered (danger)
36.90%
69 / 187
6560.86
0.00% covered (danger)
0.00%
0 / 1
 __construct
95.74% covered (success)
95.74%
45 / 47
0.00% covered (danger)
0.00%
0 / 1
14
 initConnection
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
2.00
 open
n/a
0 / 0
n/a
0 / 0
0
 getAttributes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setLogger
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getServerInfo
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 tablePrefix
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 dbSchema
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 getLBInfo
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setLBInfo
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 lastDoneWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sessionLocksPending
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTransactionRoundFname
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 isOpen
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDomainID
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 strencode
n/a
0 / 0
n/a
0 / 0
0
 installErrorHandler
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 restoreErrorHandler
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getLastPHPError
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 connectionErrorLogger
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLogContext
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 close
84.21% covered (warning)
84.21%
16 / 19
0.00% covered (danger)
0.00%
0 / 1
6.14
 assertHasConnectionHandle
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 closeConnection
n/a
0 / 0
n/a
0 / 0
0
 doSingleStatementQuery
n/a
0 / 0
n/a
0 / 0
0
 hasPermanentTable
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 registerTempTables
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 query
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 executeQuery
96.55% covered (success)
96.55%
28 / 29
0.00% covered (danger)
0.00%
0 / 1
10
 attemptQuery
74.47% covered (warning)
74.47%
70 / 94
0.00% covered (danger)
0.00%
0 / 1
15.81
 handleErroredQuery
50.00% covered (danger)
50.00%
16 / 32
0.00% covered (danger)
0.00%
0 / 1
22.50
 makeCommentedSql
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 beginIfImplied
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 assertQueryIsCurrentlyAllowed
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
5.00
 assessConnectionLoss
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
380
 handleSessionLossPreconnect
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 doHandleSessionLossPreconnect
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isQueryTimeoutError
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 reportQueryError
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
3.19
 getQueryExceptionAndLog
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 getQueryException
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
 newExceptionAfterConnectError
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 newSelectQueryBuilder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newUnionQueryBuilder
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newUpdateQueryBuilder
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newDeleteQueryBuilder
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newInsertQueryBuilder
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newReplaceQueryBuilder
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 selectField
69.23% covered (warning)
69.23%
9 / 13
0.00% covered (danger)
0.00%
0 / 1
7.05
 selectFieldValues
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
5.39
 select
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 selectRow
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 estimateRowCount
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 selectRowCount
92.59% covered (success)
92.59%
25 / 27
0.00% covered (danger)
0.00%
0 / 1
7.02
 lockForUpdate
50.00% covered (danger)
50.00%
4 / 8
0.00% covered (danger)
0.00%
0 / 1
4.12
 fieldExists
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 tableExists
n/a
0 / 0
n/a
0 / 0
0
 indexExists
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 indexUnique
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getPrimaryKeyColumns
n/a
0 / 0
n/a
0 / 0
0
 indexInfo
n/a
0 / 0
n/a
0 / 0
0
 insert
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
 checkInsertWarnings
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 update
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 databasesAreIndependent
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 selectDomain
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
2.50
 doSelectDomain
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDBname
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getServer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getServerName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addQuotes
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
6.73
 expr
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 andExpr
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orExpr
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 replace
81.82% covered (warning)
81.82%
18 / 22
0.00% covered (danger)
0.00%
0 / 1
5.15
 upsert
93.65% covered (success)
93.65%
59 / 63
0.00% covered (danger)
0.00%
0 / 1
10.03
 getInsertIdColumnForUpsert
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getValueTypesForWithClause
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 deleteJoin
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 insertSelect
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
3
 isInsertSelectSafe
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doInsertSelectGeneric
90.00% covered (success)
90.00%
27 / 30
0.00% covered (danger)
0.00%
0 / 1
7.05
 doInsertSelectNative
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 isConnectionError
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isKnownStatementRollbackError
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 serverIsReadOnly
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onTransactionResolution
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onTransactionCommitOrIdle
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
5.05
 onTransactionPreCommitOrIdle
71.43% covered (warning)
71.43%
10 / 14
0.00% covered (danger)
0.00%
0 / 1
6.84
 setTransactionListener
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTrxEndCallbackSuppression
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 runOnTransactionIdleCallbacks
73.91% covered (warning)
73.91%
17 / 23
0.00% covered (danger)
0.00%
0 / 1
7.87
 runTransactionListenerCallbacks
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
6.99
 runTransactionPostCommitCallbacks
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 runTransactionPostRollbackCallbacks
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 startAtomic
76.92% covered (warning)
76.92%
20 / 26
0.00% covered (danger)
0.00%
0 / 1
7.60
 endAtomic
83.33% covered (warning)
83.33%
15 / 18
0.00% covered (danger)
0.00%
0 / 1
6.17
 cancelAtomic
97.44% covered (success)
97.44%
38 / 39
0.00% covered (danger)
0.00%
0 / 1
6
 doAtomicSection
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 begin
68.42% covered (warning)
68.42%
13 / 19
0.00% covered (danger)
0.00%
0 / 1
5.79
 doBegin
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 commit
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
7
 rollback
77.14% covered (warning)
77.14%
27 / 35
0.00% covered (danger)
0.00%
0 / 1
9.97
 setTransactionManager
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 flushSession
60.00% covered (warning)
60.00%
18 / 30
0.00% covered (danger)
0.00%
0 / 1
12.10
 doFlushSession
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 flushSnapshot
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
3.01
 duplicateTableStructure
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 listTables
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 affectedRows
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 insertId
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 lastInsertId
n/a
0 / 0
n/a
0 / 0
0
 ping
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 replaceLostConnection
69.44% covered (warning)
69.44%
25 / 36
0.00% covered (danger)
0.00%
0 / 1
3.26
 getCacheSetOptions
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 encodeBlob
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 decodeBlob
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setSessionOptions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sourceFile
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 sourceStream
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
240
 streamStatementEnd
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 lock
76.19% covered (warning)
76.19%
16 / 21
0.00% covered (danger)
0.00%
0 / 1
3.12
 doLock
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 unlock
59.09% covered (warning)
59.09%
13 / 22
0.00% covered (danger)
0.00%
0 / 1
3.62
 doUnlock
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getScopedLockAndFlush
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
5.03
 dropTable
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 truncateTable
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 isReadOnly
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getReadOnlyReason
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 getBindingHandle
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
3.19
 commenceCriticalSection
75.00% covered (warning)
75.00%
18 / 24
0.00% covered (danger)
0.00%
0 / 1
4.25
 completeCriticalSection
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
6.20
 __toString
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 __clone
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
6
 __sleep
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 __destruct
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
4.18
 setFlag
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clearFlag
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 restoreFlags
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFlag
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 trxLevel
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 trxTimestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 trxStatus
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 writesPending
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 writesOrCallbacksPending
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 pendingWriteQueryDuration
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 pendingWriteCallers
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 pendingWriteAndCallbackCallers
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 runOnTransactionPreCommitCallbacks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 explicitTrxActive
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 implicitOrderby
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 selectSQLText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 buildComparison
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeWhereFrom2d
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 factorConds
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 bitNot
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 bitAnd
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 bitOr
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildConcat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildGroupConcat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildGreatest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildLeast
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildSubstring
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildStringCast
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildIntegerCast
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 tableName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 tableNamesN
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addIdentifierQuotes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isQuotedIdentifier
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildLike
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 anyChar
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 anyString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 limitResult
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 unionSupportsOrderAndLimit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 unionQueries
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 conditional
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 strreplace
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 timestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 timestampOrNull
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getInfinity
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 encodeExpiry
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 decodeExpiry
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setTableAliases
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTableAliases
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildGroupConcatField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildSelectSubquery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildExcludedValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSchemaVars
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 primaryPosWait
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPrimaryPos
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLag
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSessionLagStatus
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6namespace Wikimedia\Rdbms;
7
8use Exception;
9use InvalidArgumentException;
10use LogicException;
11use Psr\Log\LoggerAwareInterface;
12use Psr\Log\LoggerInterface;
13use Psr\Log\NullLogger;
14use RuntimeException;
15use Stringable;
16use Throwable;
17use Wikimedia\Rdbms\Database\DatabaseFlags;
18use Wikimedia\Rdbms\Platform\SQLPlatform;
19use Wikimedia\Rdbms\Replication\ReplicationReporter;
20use Wikimedia\RequestTimeout\CriticalSectionProvider;
21use Wikimedia\RequestTimeout\CriticalSectionScope;
22use Wikimedia\ScopedCallback;
23use Wikimedia\Telemetry\NoopTracer;
24use Wikimedia\Telemetry\SpanInterface;
25use Wikimedia\Telemetry\TracerInterface;
26use Wikimedia\Timestamp\TimestampFormat as TS;
27
28/**
29 * A single concrete connection to a relational database.
30 *
31 * This is the base class for all connection-specific relational database handles.
32 * No two instances of this class should share the same underlying network connection.
33 *
34 * @see IDatabase
35 * @ingroup Database
36 * @since 1.28
37 */
38abstract class Database implements Stringable, IDatabaseForOwner, IMaintainableDatabase, LoggerAwareInterface {
39    /** @var CriticalSectionProvider|null */
40    protected $csProvider;
41    /** @var LoggerInterface */
42    protected $logger;
43    /** @var callable Error logging callback */
44    protected $errorLogger;
45    /** @var callable Deprecation logging callback */
46    protected $deprecationLogger;
47    /** @var callable|null */
48    protected $profiler;
49    /** @var TracerInterface */
50    private $tracer;
51    /** @var TransactionManager */
52    private $transactionManager;
53
54    /** @var DatabaseDomain */
55    protected $currentDomain;
56    /** @var DatabaseFlags */
57    protected $flagsHolder;
58
59    // phpcs:ignore MediaWiki.Commenting.PropertyDocumentation.ObjectTypeHintVar
60    /** @var object|resource|null Database connection */
61    protected $conn;
62
63    /** @var string|null Readable name or host/IP of the database server */
64    protected $serverName;
65    /** @var bool Whether this PHP instance is for a CLI script */
66    protected $cliMode;
67    /** @var int|null Maximum seconds to wait on connection attempts */
68    protected $connectTimeout;
69    /** @var int|null Maximum seconds to wait on receiving query results */
70    protected $receiveTimeout;
71    /** @var string Agent name for query profiling */
72    protected $agent;
73    /** @var array<string,mixed> Connection parameters used by initConnection() and open() */
74    protected $connectionParams;
75    /** @var string[]|int[]|float[] SQL variables values to use for all new connections */
76    protected $connectionVariables;
77    /** @var int Row batch size to use for emulated INSERT SELECT queries */
78    protected $nonNativeInsertSelectBatchSize;
79
80    /** @var bool Whether to use SSL connections */
81    protected $ssl;
82    /** @var bool Whether to check for warnings */
83    protected $strictWarnings;
84    /** @var array Current LoadBalancer tracking information */
85    protected $lbInfo = [];
86    /** @var string|false Current SQL query delimiter */
87    protected $delimiter = ';';
88
89    /** @var string|bool|null Stashed value of html_errors INI setting */
90    private $htmlErrors;
91
92    /** @var array<string,array> Map of (lock name => (UNIX time,trx ID)) */
93    protected $sessionNamedLocks = [];
94    /** @var array<string,array<string, TempTableInfo>> Map of (DB name => table name => info) */
95    protected $sessionTempTables = [];
96
97    /** @var int Affected row count for the last statement to query() */
98    protected $lastQueryAffectedRows = 0;
99    /** @var int|null Insert (row) ID for the last statement to query() (null if not supported) */
100    protected $lastQueryInsertId;
101
102    /** @var int|null Affected row count for the last query method call; null if unspecified */
103    protected $lastEmulatedAffectedRows;
104    /** @var int|null Insert (row) ID for the last query method call; null if unspecified */
105    protected $lastEmulatedInsertId;
106
107    /** @var string Last error during connection; empty string if none */
108    protected $lastConnectError = '';
109
110    /** @var float UNIX timestamp of the last server response */
111    private $lastPing = 0.0;
112    /** @var float|null UNIX timestamp of the last committed write */
113    private $lastWriteTime;
114    /** @var string|false The last PHP error from a query or connection attempt */
115    private $lastPhpError = false;
116
117    /** @var int|null Current critical section numeric ID */
118    private $csmId;
119    /** @var string|null Last critical section caller name */
120    private $csmFname;
121    /** @var Exception|null Last unresolved critical section error */
122    private $csmError;
123
124    /** Whether the database is a file on disk */
125    public const ATTR_DB_IS_FILE = 'db-is-file';
126    /** Lock granularity is on the level of the entire database */
127    public const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
128    /** The SCHEMA keyword refers to a grouping of tables in a database */
129    public const ATTR_SCHEMAS_AS_TABLE_GROUPS = 'supports-schemas';
130
131    /** New Database instance will not be connected yet when returned */
132    public const NEW_UNCONNECTED = 0;
133    /** New Database instance will already be connected when returned */
134    public const NEW_CONNECTED = 1;
135
136    /** No errors occurred during the query */
137    protected const ERR_NONE = 0;
138    /** Retry query due to a connection loss detected while sending the query (session intact) */
139    protected const ERR_RETRY_QUERY = 1;
140    /** Abort query (no retries) due to a statement rollback (session/transaction intact) */
141    protected const ERR_ABORT_QUERY = 2;
142    /** Abort any current transaction, by rolling it back, due to an error during the query */
143    protected const ERR_ABORT_TRX = 4;
144    /** Abort and reset session due to server-side session-level state loss (locks, temp tables) */
145    protected const ERR_ABORT_SESSION = 8;
146
147    /** Assume that queries taking this long to yield connection loss errors are at fault */
148    protected const DROPPED_CONN_BLAME_THRESHOLD_SEC = 3.0;
149
150    /** @var string Idiom used when a cancelable atomic section started the transaction */
151    private const NOT_APPLICABLE = 'n/a';
152
153    /** How long before it is worth doing a dummy query to test the connection */
154    private const PING_TTL = 1.0;
155    /** Dummy SQL query */
156    private const PING_QUERY = 'SELECT 1 AS ping';
157
158    /** Hostname/address and optional port to use on all connections */
159    protected const CONN_SERVER = 'server';
160    /** Database server username to use on all connections */
161    protected const CONN_USER = 'user';
162    /** Database server password to use on all connections */
163    protected const CONN_PASSWORD = 'password';
164    /** Database name to use on initial connection */
165    protected const CONN_INITIAL_DB = 'dbname';
166    /** Schema name to use on initial connection */
167    protected const CONN_INITIAL_SCHEMA = 'schema';
168    /** Table prefix to use on initial connection */
169    protected const CONN_INITIAL_TABLE_PREFIX = 'tablePrefix';
170
171    /** @deprecated Since 1.46 */
172    protected const CONN_HOST = self::CONN_SERVER;
173
174    /** @var SQLPlatform */
175    protected $platform;
176
177    /** @var ReplicationReporter */
178    protected $replicationReporter;
179
180    /**
181     * @note exceptions for missing libraries/drivers should be thrown in initConnection()
182     * @param array $params Parameters passed from Database::factory()
183     */
184    public function __construct( array $params ) {
185        $this->logger = $params['logger'] ?? new NullLogger();
186        $this->transactionManager = new TransactionManager(
187            $this->logger,
188            $params['trxProfiler']
189        );
190        $this->connectionParams = [
191            self::CONN_SERVER => ( isset( $params['host'] ) && $params['host'] !== '' )
192                ? $params['host']
193                : null,
194            self::CONN_USER => ( isset( $params['user'] ) && $params['user'] !== '' )
195                ? $params['user']
196                : null,
197            self::CONN_INITIAL_DB => ( isset( $params['dbname'] ) && $params['dbname'] !== '' )
198                ? $params['dbname']
199                : null,
200            self::CONN_INITIAL_SCHEMA => ( isset( $params['schema'] ) && $params['schema'] !== '' )
201                ? $params['schema']
202                : null,
203            self::CONN_PASSWORD => is_string( $params['password'] ) ? $params['password'] : null,
204            self::CONN_INITIAL_TABLE_PREFIX => (string)$params['tablePrefix']
205        ];
206
207        $this->lbInfo = $params['lbInfo'] ?? [];
208        $this->connectionVariables = $params['variables'] ?? [];
209        // Set SQL mode, default is turning them all off, can be overridden or skipped with null
210        if ( is_string( $params['sqlMode'] ?? null ) ) {
211            $this->connectionVariables['sql_mode'] = $params['sqlMode'];
212        }
213        $flags = (int)$params['flags'];
214        $this->flagsHolder = new DatabaseFlags( $flags );
215        $this->ssl = $params['ssl'] ?? (bool)( $flags & self::DBO_SSL );
216        $this->connectTimeout = $params['connectTimeout'] ?? null;
217        $this->receiveTimeout = $params['receiveTimeout'] ?? null;
218        $this->cliMode = (bool)$params['cliMode'];
219        $this->agent = (string)$params['agent'];
220        $this->serverName = $params['serverName'];
221        $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'] ?? 10000;
222        $this->strictWarnings = !empty( $params['strictWarnings'] );
223
224        $this->profiler = is_callable( $params['profiler'] ) ? $params['profiler'] : null;
225        $this->errorLogger = $params['errorLogger'];
226        $this->deprecationLogger = $params['deprecationLogger'];
227
228        $this->csProvider = $params['criticalSectionProvider'] ?? null;
229
230        // Set initial dummy domain until open() sets the final DB/prefix
231        $this->currentDomain = new DatabaseDomain(
232            $params['dbname'] != '' ? $params['dbname'] : null,
233            $params['schema'] != '' ? $params['schema'] : null,
234            $params['tablePrefix']
235        );
236        $this->platform = new SQLPlatform(
237            $this,
238            $this->logger,
239            $this->currentDomain,
240            $this->errorLogger
241        );
242        $this->tracer = $params['tracer'] ?? new NoopTracer();
243        // Children classes must set $this->replicationReporter.
244    }
245
246    /**
247     * Initialize the connection to the database over the wire (or to local files)
248     *
249     * @throws LogicException
250     * @throws InvalidArgumentException
251     * @throws DBConnectionError
252     * @since 1.31
253     */
254    final public function initConnection() {
255        if ( $this->isOpen() ) {
256            throw new LogicException( __METHOD__ . ': already connected' );
257        }
258        // Establish the connection
259        $this->open(
260            $this->connectionParams[self::CONN_SERVER],
261            $this->connectionParams[self::CONN_USER],
262            $this->connectionParams[self::CONN_PASSWORD],
263            $this->connectionParams[self::CONN_INITIAL_DB],
264            $this->connectionParams[self::CONN_INITIAL_SCHEMA],
265            $this->connectionParams[self::CONN_INITIAL_TABLE_PREFIX]
266        );
267        $this->lastPing = microtime( true );
268    }
269
270    /**
271     * Open a new connection to the database (closing any existing one)
272     *
273     * @param string|null $server Server host/address and optional port {@see connectionParams}
274     * @param string|null $user User name {@see connectionParams}
275     * @param string|null $password User password {@see connectionParams}
276     * @param string|null $db Database name
277     * @param string|null $schema Database schema name
278     * @param string $tablePrefix
279     * @throws DBConnectionError
280     */
281    abstract protected function open( $server, $user, $password, $db, $schema, $tablePrefix );
282
283    /**
284     * @return array Map of (Database::ATTR_* constant => value)
285     * @since 1.31
286     */
287    public static function getAttributes() {
288        return [];
289    }
290
291    /**
292     * Set the PSR-3 logger interface to use.
293     */
294    public function setLogger( LoggerInterface $logger ): void {
295        $this->logger = $logger;
296    }
297
298    /** @inheritDoc */
299    public function getServerInfo() {
300        return $this->getServerVersion();
301    }
302
303    /** @inheritDoc */
304    public function tablePrefix( $prefix = null ) {
305        $old = $this->currentDomain->getTablePrefix();
306
307        if ( $prefix !== null ) {
308            $this->currentDomain = new DatabaseDomain(
309                $this->currentDomain->getDatabase(),
310                $this->currentDomain->getSchema(),
311                $prefix
312            );
313            $this->platform->setCurrentDomain( $this->currentDomain );
314        }
315
316        return $old;
317    }
318
319    /** @inheritDoc */
320    public function dbSchema( $schema = null ) {
321        $old = $this->currentDomain->getSchema();
322
323        if ( $schema !== null ) {
324            if ( $schema !== '' && $this->getDBname() === null ) {
325                throw new DBUnexpectedError(
326                    $this,
327                    "Cannot set schema to '$schema'; no database set"
328                );
329            }
330
331            $this->currentDomain = new DatabaseDomain(
332                $this->currentDomain->getDatabase(),
333                // DatabaseDomain uses null for unspecified schemas
334                ( $schema !== '' ) ? $schema : null,
335                $this->currentDomain->getTablePrefix()
336            );
337            $this->platform->setCurrentDomain( $this->currentDomain );
338        }
339
340        return (string)$old;
341    }
342
343    /** @inheritDoc */
344    public function getLBInfo( $name = null ) {
345        if ( $name === null ) {
346            return $this->lbInfo;
347        }
348
349        if ( array_key_exists( $name, $this->lbInfo ) ) {
350            return $this->lbInfo[$name];
351        }
352
353        return null;
354    }
355
356    /** @inheritDoc */
357    public function setLBInfo( $nameOrArray, $value = null ) {
358        if ( is_array( $nameOrArray ) ) {
359            $this->lbInfo = $nameOrArray;
360        } elseif ( is_string( $nameOrArray ) ) {
361            if ( $value !== null ) {
362                $this->lbInfo[$nameOrArray] = $value;
363            } else {
364                unset( $this->lbInfo[$nameOrArray] );
365            }
366        } else {
367            throw new InvalidArgumentException( "Got non-string key" );
368        }
369    }
370
371    /** @inheritDoc */
372    public function lastDoneWrites() {
373        return $this->lastWriteTime;
374    }
375
376    /**
377     * @return bool
378     * @since 1.39
379     * @internal For use by Database/LoadBalancer only
380     */
381    public function sessionLocksPending() {
382        return (bool)$this->sessionNamedLocks;
383    }
384
385    /**
386     * @return ?string Owner name of explicit transaction round being participating in; null if none
387     */
388    final protected function getTransactionRoundFname() {
389        if ( $this->flagsHolder->hasImplicitTrxFlag() ) {
390            // LoadBalancer transaction round participation is enabled for this DB handle;
391            // get the owner of the active explicit transaction round (if any)
392            return $this->getLBInfo( self::LB_TRX_ROUND_FNAME );
393        }
394
395        return null;
396    }
397
398    /** @inheritDoc */
399    public function isOpen() {
400        return (bool)$this->conn;
401    }
402
403    /** @inheritDoc */
404    public function getDomainID() {
405        return $this->currentDomain->getId();
406    }
407
408    /**
409     * Wrapper for addslashes()
410     *
411     * @param string $s String to be slashed.
412     * @return string Slashed string.
413     */
414    abstract public function strencode( $s );
415
416    /**
417     * Set a custom error handler for logging errors during database connection
418     */
419    protected function installErrorHandler() {
420        $this->lastPhpError = false;
421        $this->htmlErrors = ini_set( 'html_errors', '0' );
422        set_error_handler( $this->connectionErrorLogger( ... ) );
423    }
424
425    /**
426     * Restore the previous error handler and return the last PHP error for this DB
427     *
428     * @return string|false
429     */
430    protected function restoreErrorHandler() {
431        restore_error_handler();
432        if ( $this->htmlErrors !== false ) {
433            ini_set( 'html_errors', $this->htmlErrors );
434        }
435
436        return $this->getLastPHPError();
437    }
438
439    /**
440     * @return string|false Last PHP error for this DB (typically connection errors)
441     */
442    protected function getLastPHPError() {
443        if ( $this->lastPhpError ) {
444            $error = preg_replace( '!\[<a.*</a>\]!', '', $this->lastPhpError );
445            $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
446
447            return $error;
448        }
449
450        return false;
451    }
452
453    /**
454     * Error handler for logging errors during database connection
455     *
456     * @internal This method should not be used outside of Database classes
457     *
458     * @param int|string $errno
459     * @param string $errstr
460     */
461    public function connectionErrorLogger( $errno, $errstr ) {
462        $this->lastPhpError = $errstr;
463    }
464
465    /**
466     * Create a log context to pass to PSR-3 logger functions.
467     *
468     * @param array $extras Additional data to add to context
469     * @return array
470     */
471    protected function getLogContext( array $extras = [] ) {
472        return $extras + [
473            'db_server' => $this->getServerName(),
474            'db_name' => $this->getDBname(),
475            'db_user' => $this->connectionParams[self::CONN_USER] ?? null,
476        ];
477    }
478
479    /** @inheritDoc */
480    final public function close( $fname = __METHOD__ ) {
481        $error = null; // error to throw after disconnecting
482
483        $wasOpen = (bool)$this->conn;
484        // This should mostly do nothing if the connection is already closed
485        if ( $this->conn ) {
486            // Roll back any dangling transaction first
487            if ( $this->trxLevel() ) {
488                $error = $this->transactionManager->trxCheckBeforeClose( $this, $fname );
489                // Rollback the changes and run any callbacks as needed
490                $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
491                $this->runTransactionPostRollbackCallbacks();
492            }
493
494            // Close the actual connection in the binding handle
495            $closed = $this->closeConnection();
496        } else {
497            $closed = true; // already closed; nothing to do
498        }
499
500        $this->conn = null;
501
502        // Log any unexpected errors after having disconnected
503        if ( $error !== null ) {
504            // T217819, T231443: this is probably just LoadBalancer trying to recover from
505            // errors and shutdown. Log any problems and move on since the request has to
506            // end one way or another. Throwing errors is not very useful at some point.
507            $this->logger->error( $error, [ 'db_log_category' => 'query' ] );
508        }
509
510        // Note that various subclasses call close() at the start of open(), which itself is
511        // called by replaceLostConnection(). In that case, just because onTransactionResolution()
512        // callbacks are pending does not mean that an exception should be thrown. Rather, they
513        // will be executed after the reconnection step.
514        if ( $wasOpen ) {
515            // Double check that no callbacks are dangling
516            $fnames = $this->pendingWriteAndCallbackCallers();
517            if ( $fnames ) {
518                throw new RuntimeException(
519                    "Transaction callbacks are still pending: " . implode( ', ', $fnames )
520                );
521            }
522        }
523
524        return $closed;
525    }
526
527    /**
528     * Make sure there is an open connection handle (alive or not)
529     *
530     * This guards against fatal errors to the binding handle not being defined in cases
531     * where open() was never called or close() was already called.
532     *
533     * @throws DBUnexpectedError
534     */
535    final protected function assertHasConnectionHandle() {
536        if ( !$this->isOpen() ) {
537            throw new DBUnexpectedError( $this, "DB connection was already closed" );
538        }
539    }
540
541    /**
542     * Closes underlying database connection
543     * @return bool Whether connection was closed successfully
544     * @since 1.20
545     */
546    abstract protected function closeConnection();
547
548    /**
549     * Run a query and return a QueryStatus instance with the query result information
550     *
551     * This is meant to handle the basic command of actually sending a query to the
552     * server via the driver. No implicit transaction, reconnection, nor retry logic
553     * should happen here. The higher level query() method is designed to handle those
554     * sorts of concerns. This method should not trigger such higher level methods.
555     *
556     * The lastError() and lastErrno() methods should meaningfully reflect what error,
557     * if any, occurred during the last call to this method. Methods like executeQuery(),
558     * query(), select(), insert(), update(), delete(), and upsert() implement their calls
559     * to doQuery() such that an immediately subsequent call to lastError()/lastErrno()
560     * meaningfully reflects any error that occurred during that public query method call.
561     *
562     * For SELECT queries, the result field contains either:
563     *   - a) A driver-specific IResultWrapper describing the query results
564     *   - b) False, on any query failure
565     *
566     * For non-SELECT queries, the result field contains either:
567     *   - a) A driver-specific IResultWrapper, only on success
568     *   - b) True, only on success (e.g. no meaningful result other than "OK")
569     *   - c) False, on any query failure
570     *
571     * @param string $sql Single-statement SQL query
572     * @return QueryStatus
573     * @since 1.39
574     */
575    abstract protected function doSingleStatementQuery( string $sql ): QueryStatus;
576
577    /**
578     * Determine whether a write query affects a permanent table.
579     * This includes pseudo-permanent tables.
580     *
581     * @param Query $query
582     * @return bool
583     */
584    private function hasPermanentTable( Query $query ) {
585        if ( $query->getVerb() === 'CREATE TEMPORARY' ) {
586            // Temporary table creation is allowed
587            return false;
588        }
589        $table = $query->getWriteTable();
590        if ( $table === null ) {
591            // Parse error? Assume permanent.
592            return true;
593        }
594        [ $db, $pt ] = $this->platform->getDatabaseAndTableIdentifier( $table );
595        $tempInfo = $this->sessionTempTables[$db][$pt] ?? null;
596        return !$tempInfo || $tempInfo->pseudoPermanent;
597    }
598
599    /**
600     * Register creation and dropping of temporary tables
601     */
602    protected function registerTempTables( Query $query ) {
603        $table = $query->getWriteTable();
604        if ( $table === null ) {
605            return;
606        }
607        switch ( $query->getVerb() ) {
608            case 'CREATE TEMPORARY':
609                [ $db, $pt ] = $this->platform->getDatabaseAndTableIdentifier( $table );
610                $this->sessionTempTables[$db][$pt] = new TempTableInfo(
611                    $this->transactionManager->getTrxId(),
612                    (bool)( $query->getFlags() & self::QUERY_PSEUDO_PERMANENT )
613                );
614                break;
615
616            case 'DROP':
617                [ $db, $pt ] = $this->platform->getDatabaseAndTableIdentifier( $table );
618                unset( $this->sessionTempTables[$db][$pt] );
619        }
620    }
621
622    /** @inheritDoc */
623    public function query( $sql, $fname = __METHOD__, $flags = 0 ) {
624        if ( !( $sql instanceof Query ) ) {
625            $flags = (int)$flags; // b/c; this field used to be a bool
626            $sql = QueryBuilderFromRawSql::buildQuery( $sql, $flags, $this->currentDomain->getTablePrefix() );
627        } else {
628            $flags = $sql->getFlags();
629        }
630
631        // Make sure that this caller is allowed to issue this query statement
632        $this->assertQueryIsCurrentlyAllowed( $sql->getVerb(), $fname );
633
634        // Send the query to the server and fetch any corresponding errors
635        $status = $this->executeQuery( $sql, $fname, $flags );
636        if ( $status->res === false ) {
637            // An error occurred; log, and, if needed, report an exception.
638            // Errors that corrupt the transaction/session state cannot be silenced.
639            $ignore = (
640                $this->flagsHolder::contains( $flags, self::QUERY_SILENCE_ERRORS ) &&
641                !$this->flagsHolder::contains( $status->flags, self::ERR_ABORT_SESSION ) &&
642                !$this->flagsHolder::contains( $status->flags, self::ERR_ABORT_TRX )
643            );
644            $this->reportQueryError( $status->message, $status->code, $sql->getSQL(), $fname, $ignore );
645        }
646
647        return $status->res;
648    }
649
650    /**
651     * Execute a query without enforcing public (non-Database) caller restrictions.
652     *
653     * Retry it if there is a recoverable connection loss (e.g. no important state lost).
654     *
655     * This does not precheck for transaction/session state errors or critical section errors.
656     *
657     * @see Database::query()
658     *
659     * @param Query $sql SQL statement
660     * @param string $fname Name of the calling function
661     * @param int $flags Bit field of ISQLPlatform::QUERY_* constants
662     * @return QueryStatus
663     * @throws DBUnexpectedError
664     * @since 1.34
665     */
666    final protected function executeQuery( $sql, $fname, $flags ) {
667        $this->assertHasConnectionHandle();
668
669        $isPermWrite = false;
670        $isWrite = $sql->isWriteQuery();
671        if ( $isWrite ) {
672            ChangedTablesTracker::recordQuery( $this->currentDomain, $sql );
673            // Permit temporary table writes on replica connections, but require a writable
674            // master connection for writes to persistent tables.
675            if ( $this->hasPermanentTable( $sql ) ) {
676                $isPermWrite = true;
677                $info = $this->getReadOnlyReason();
678                if ( $info ) {
679                    [ $reason, $source ] = $info;
680                    if ( $source === 'role' ) {
681                        throw new DBReadOnlyRoleError( $this, "Database is read-only: $reason" );
682                    } else {
683                        throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
684                    }
685                }
686                // DBConnRef uses QUERY_REPLICA_ROLE to enforce replica roles during query()
687                if ( $this->flagsHolder::contains( $sql->getFlags(), self::QUERY_REPLICA_ROLE ) ) {
688                    throw new DBReadOnlyRoleError(
689                        $this,
690                        "Cannot write; target role is DB_REPLICA"
691                    );
692                }
693            }
694        }
695
696        // Whether a silent retry attempt is left for recoverable connection loss errors
697        $retryLeft = !$this->flagsHolder::contains( $flags, self::QUERY_NO_RETRY );
698
699        $cs = $this->commenceCriticalSection( __METHOD__ );
700
701        do {
702            // Start a DBO_TRX wrapper transaction as needed (throw an error on failure)
703            if ( $this->beginIfImplied( $sql, $fname, $flags ) ) {
704                // Since begin() was called, any connection loss was already handled
705                $retryLeft = false;
706            }
707            // Send the query statement to the server and fetch any results.
708            $status = $this->attemptQuery( $sql, $fname, $isPermWrite );
709        } while (
710            // An error occurred that can be recovered from via query retry
711            $this->flagsHolder::contains( $status->flags, self::ERR_RETRY_QUERY ) &&
712            // The retry has not been exhausted (consume it now)
713            // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
714            $retryLeft && !( $retryLeft = false )
715        );
716
717        // Register creation and dropping of temporary tables
718        if ( $status->res ) {
719            $this->registerTempTables( $sql );
720        }
721        $this->completeCriticalSection( __METHOD__, $cs );
722
723        return $status;
724    }
725
726    /**
727     * Query method wrapper handling profiling, logging, affected row count tracking, and
728     * automatic reconnections (without retry) on query failure due to connection loss
729     *
730     * Note that this does not handle DBO_TRX logic.
731     *
732     * This method handles profiling, debug logging, reconnection and the tracking of:
733     *   - write callers
734     *   - last write time
735     *   - affected row count of the last write
736     *   - whether writes occurred in a transaction
737     *   - last successful query time (confirming that the connection was not dropped)
738     *
739     * @see doSingleStatementQuery()
740     *
741     * @param Query $sql SQL statement
742     * @param string $fname Name of the calling function
743     * @param bool $isPermWrite Whether it's a query writing to permanent tables
744     * @return QueryStatus statement result
745     * @throws DBUnexpectedError
746     */
747    private function attemptQuery(
748        $sql,
749        string $fname,
750        bool $isPermWrite
751    ) {
752        // Transaction attributes before issuing this query
753        $priorSessInfo = new CriticalSessionInfo(
754            $this->transactionManager->getTrxId(),
755            $this->transactionManager->explicitTrxActive(),
756            $this->transactionManager->pendingWriteCallers(),
757            $this->transactionManager->pendingPreCommitCallbackCallers(),
758            $this->sessionNamedLocks,
759            $this->sessionTempTables
760        );
761        // Get the transaction-aware SQL string used for profiling
762        $generalizedSql = GeneralizedSql::newFromQuery(
763            $sql,
764            ( $this->replicationReporter->getTopologyRole() === self::ROLE_STREAMING_MASTER )
765                ? 'role-primary: '
766                : ''
767        );
768        // Add agent and calling method comments to the SQL
769        $cStatement = $this->makeCommentedSql( $sql->getSQL(), $fname );
770        // Start profile section
771        $ps = $this->profiler ? ( $this->profiler )( $generalizedSql->stringify() ) : null;
772        $startTime = microtime( true );
773
774        // Clear any overrides from a prior "query method". Note that this does not affect
775        // any such methods that are currently invoking query() itself since those query
776        // methods set these fields before returning.
777        $this->lastEmulatedAffectedRows = null;
778        $this->lastEmulatedInsertId = null;
779
780        // Record an OTEL span for this query.
781        $writeTableName = $sql->getWriteTable();
782        $spanName = $writeTableName ?
783            "Database {$sql->getVerb()} {$this->getDBname()}.{$writeTableName}" :
784            "Database {$sql->getVerb()} {$this->getDBname()}";
785        $span = $this->tracer->createSpan( $spanName )
786            ->setSpanKind( SpanInterface::SPAN_KIND_CLIENT )
787            ->start();
788        if ( $span->getContext()->isSampled() ) {
789            $span->setAttributes( [
790                'code.function' => $fname,
791                'db.namespace' => $this->getDBname(),
792                'db.operation.name' => $sql->getVerb(),
793                'db.query.text' => $generalizedSql->stringify(),
794                'db.system' => $this->getType(),
795                'server.address' => $this->getServerName(),
796                'db.collection.name' => $writeTableName, # nulls filtered out
797            ] );
798        }
799
800        $status = $this->doSingleStatementQuery( $cStatement );
801
802        // End profile section
803        $endTime = microtime( true );
804        $queryRuntime = max( $endTime - $startTime, 0.0 );
805        unset( $ps );
806        $span->end();
807
808        if ( $status->res !== false ) {
809            $this->lastPing = $endTime;
810            $span->setSpanStatus( SpanInterface::SPAN_STATUS_OK );
811        } else {
812            $span->setSpanStatus( SpanInterface::SPAN_STATUS_ERROR )
813                ->setAttributes( [
814                'db.response.status_code' => $status->code,
815                'exception.message' => $status->message,
816            ] );
817        }
818
819        $affectedRowCount = $status->rowsAffected;
820        $returnedRowCount = $status->rowsReturned;
821        $this->lastQueryAffectedRows = $affectedRowCount;
822
823        if ( $span->getContext()->isSampled() ) {
824            $span->setAttributes( [
825                'db.response.affected_rows' => $affectedRowCount,
826                'db.response.returned_rows' => $returnedRowCount,
827            ] );
828        }
829
830        if ( $status->res !== false ) {
831            if ( $isPermWrite ) {
832                if ( $this->trxLevel() ) {
833                    $this->transactionManager->transactionWritingIn(
834                        $this->getServerName(),
835                        $this->getDomainID(),
836                        $startTime
837                    );
838                    $this->transactionManager->updateTrxWriteQueryReport(
839                        $sql->getSQL(),
840                        $queryRuntime,
841                        $affectedRowCount,
842                        $fname
843                    );
844                } else {
845                    $this->lastWriteTime = $endTime;
846                }
847            }
848        }
849
850        $this->transactionManager->recordQueryCompletion(
851            $generalizedSql,
852            $startTime,
853            $isPermWrite,
854            $isPermWrite ? $affectedRowCount : $returnedRowCount,
855            $this->getServerName(),
856            $fname
857        );
858
859        // Check if the query failed...
860        $status->flags = $this->handleErroredQuery( $status, $sql, $fname, $queryRuntime, $priorSessInfo );
861        // Avoid the overhead of logging calls unless debug mode is enabled
862        if ( $this->flagsHolder->getFlag( self::DBO_DEBUG ) ) {
863            $this->logger->debug(
864                "{method} [{runtime_ms}ms] [{rows} rows] {db_server}: {sql}",
865                $this->getLogContext( [
866                    'method' => $fname,
867                    'sql' => $sql->getSQL(),
868                    'domain' => $this->getDomainID(),
869                    'runtime_ms' => round( $queryRuntime * 1000, 3 ),
870                    'rows' => $isPermWrite ? $affectedRowCount : $returnedRowCount,
871                    'db_log_category' => 'query'
872                ] )
873            );
874        }
875
876        return $status;
877    }
878
879    private function handleErroredQuery(
880        QueryStatus $status, Query $sql, string $fname, float $queryRuntime, CriticalSessionInfo $priorSessInfo
881    ): int {
882        $errflags = self::ERR_NONE;
883        $error = $status->message;
884        $errno = $status->code;
885        if ( $status->res !== false ) {
886            // Statement succeeded
887            return $errflags;
888        }
889        if ( $this->isConnectionError( $errno ) ) {
890            // Connection lost before or during the query...
891            // Determine how to proceed given the lost session state
892            $connLossFlag = $this->assessConnectionLoss(
893                $sql->getVerb(),
894                $queryRuntime,
895                $priorSessInfo
896            );
897            // Update session state tracking and try to reestablish a connection
898            $reconnected = $this->replaceLostConnection( $errno, __METHOD__ );
899            // Check if important server-side session-level state was lost
900            if ( $connLossFlag >= self::ERR_ABORT_SESSION ) {
901                $ex = $this->getQueryException( $error, $errno, $sql->getSQL(), $fname );
902                $this->transactionManager->setSessionError( $ex );
903            }
904            // Check if important server-side transaction-level state was lost
905            if ( $connLossFlag >= self::ERR_ABORT_TRX ) {
906                $ex = $this->getQueryException( $error, $errno, $sql->getSQL(), $fname );
907                $this->transactionManager->setTransactionError( $ex );
908            }
909            // Check if the query should be retried (having made the reconnection attempt)
910            if ( $connLossFlag === self::ERR_RETRY_QUERY ) {
911                $errflags |= ( $reconnected ? self::ERR_RETRY_QUERY : self::ERR_ABORT_QUERY );
912            } else {
913                $errflags |= $connLossFlag;
914            }
915        } elseif ( $this->isKnownStatementRollbackError( $errno ) ) {
916            // Query error triggered a server-side statement-only rollback...
917            $errflags |= self::ERR_ABORT_QUERY;
918            if ( $this->trxLevel() ) {
919                // Allow legacy callers to ignore such errors via QUERY_IGNORE_DBO_TRX and
920                // try/catch. However, a deprecation notice will be logged on the next query.
921                $cause = [ $error, $errno, $fname ];
922                $this->transactionManager->setTrxStatusIgnoredCause( $cause );
923            }
924        } elseif ( $this->trxLevel() ) {
925            // Some other error occurred during the query, within a transaction...
926            // Server-side handling of errors during transactions varies widely depending on
927            // the RDBMS type and configuration. There are several possible results: (a) the
928            // whole transaction is rolled back, (b) only the queries after BEGIN are rolled
929            // back, (c) the transaction is marked as "aborted" and a ROLLBACK is required
930            // before other queries are permitted. For compatibility reasons, pessimistically
931            // require a ROLLBACK query (not using SAVEPOINT) before allowing other queries.
932            $ex = $this->getQueryException( $error, $errno, $sql->getSQL(), $fname );
933            $this->transactionManager->setTransactionError( $ex );
934            $errflags |= self::ERR_ABORT_TRX;
935        } else {
936            // Some other error occurred during the query, without a transaction...
937            $errflags |= self::ERR_ABORT_QUERY;
938        }
939
940        return $errflags;
941    }
942
943    /**
944     * @param string $sql
945     * @param string $fname
946     * @return string
947     */
948    private function makeCommentedSql( $sql, $fname ): string {
949        // Add trace comment to the begin of the sql string, right after the operator.
950        // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598).
951        // NOTE: Don't add varying ids such as request id or session id to the comment.
952        // It would break aggregation of similar queries in analysis tools (see T193050#7512149)
953        $encName = preg_replace( '/[\x00-\x1F\/]/', '-', "$fname {$this->agent}" );
954        return preg_replace( '/\s|$/', " /* $encName */ ", $sql, 1 );
955    }
956
957    /**
958     * Start an implicit transaction if DBO_TRX is enabled and no transaction is active
959     *
960     * @param Query $sql SQL statement
961     * @param string $fname
962     * @param int $flags Bit field of ISQLPlatform::QUERY_* constants
963     * @return bool Whether an implicit transaction was started
964     * @throws DBError
965     */
966    private function beginIfImplied( $sql, $fname, $flags ) {
967        if ( !$this->trxLevel() && $this->flagsHolder->hasApplicableImplicitTrxFlag( $flags ) ) {
968            if ( $this->platform->isTransactableQuery( $sql ) ) {
969                $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
970                $this->transactionManager->turnOnAutomatic();
971
972                return true;
973            }
974        }
975
976        return false;
977    }
978
979    /**
980     * Check if callers outside of Database can run the given query given the session state
981     *
982     * In order to keep the DB handle's session state tracking in sync, certain queries
983     * like "USE", "BEGIN", "COMMIT", and "ROLLBACK" must not be issued directly from
984     * outside callers. Such commands should only be issued through dedicated methods
985     * like selectDomain(), begin(), commit(), and rollback(), respectively.
986     *
987     * This also checks if the session state tracking was corrupted by a prior exception.
988     *
989     * @param string $verb
990     * @param string $fname
991     * @throws DBUnexpectedError
992     * @throws DBTransactionStateError
993     */
994    private function assertQueryIsCurrentlyAllowed( string $verb, string $fname ) {
995        if ( $verb === 'USE' ) {
996            throw new DBUnexpectedError( $this, "Got USE query; use selectDomain() instead" );
997        }
998
999        if ( $verb === 'ROLLBACK' ) {
1000            // Whole transaction rollback is used for recovery
1001            // @TODO: T269161; prevent "BEGIN"/"COMMIT"/"ROLLBACK" from outside callers
1002            return;
1003        }
1004
1005        if ( $this->csmError ) {
1006            throw new DBTransactionStateError(
1007                $this,
1008                "Cannot execute query from $fname while session state is out of sync",
1009                [],
1010                $this->csmError
1011            );
1012        }
1013
1014        $this->transactionManager->assertSessionStatus( $this, $fname );
1015
1016        if ( $verb !== 'ROLLBACK TO SAVEPOINT' ) {
1017            $this->transactionManager->assertTransactionStatus(
1018                $this,
1019                $this->deprecationLogger,
1020                $fname
1021            );
1022        }
1023    }
1024
1025    /**
1026     * Determine how to handle a connection lost discovered during a query attempt
1027     *
1028     * This checks if explicit transactions, pending transaction writes, and important
1029     * session-level state (locks, temp tables) was lost. Point-in-time read snapshot loss
1030     * is considered acceptable for DBO_TRX logic.
1031     *
1032     * If state was lost, but that loss was discovered during a ROLLBACK that would have
1033     * destroyed that state anyway, treat the error as recoverable.
1034     *
1035     * @param string $verb SQL query verb
1036     * @param float $walltime How many seconds passes while attempting the query
1037     * @param CriticalSessionInfo $priorSessInfo Session state just before the query
1038     * @return int Recovery approach. One of the following ERR_* class constants:
1039     *   - Database::ERR_RETRY_QUERY: reconnect silently, retry query
1040     *   - Database::ERR_ABORT_QUERY: reconnect silently, do not retry query
1041     *   - Database::ERR_ABORT_TRX: reconnect, throw error, enforce transaction rollback
1042     *   - Database::ERR_ABORT_SESSION: reconnect, throw error, enforce session rollback
1043     */
1044    private function assessConnectionLoss(
1045        string $verb,
1046        float $walltime,
1047        CriticalSessionInfo $priorSessInfo
1048    ) {
1049        if ( $walltime < self::DROPPED_CONN_BLAME_THRESHOLD_SEC ) {
1050            // Query failed quickly; the connection was probably lost before the query was sent
1051            $res = self::ERR_RETRY_QUERY;
1052        } else {
1053            // Query took a long time; the connection was probably lost during query execution
1054            $res = self::ERR_ABORT_QUERY;
1055        }
1056
1057        // List of problems causing session/transaction state corruption
1058        $blockers = [];
1059        // Loss of named locks breaks future callers relying on those locks for critical sections
1060        foreach ( $priorSessInfo->namedLocks as $lockName => $lockInfo ) {
1061            if ( $lockInfo['trxId'] && $lockInfo['trxId'] === $priorSessInfo->trxId ) {
1062                // Treat lost locks acquired during the lost transaction as a transaction state
1063                // problem. Connection loss on ROLLBACK (non-SAVEPOINT) is tolerable since
1064                // rollback automatically triggered server-side.
1065                if ( $verb !== 'ROLLBACK' ) {
1066                    $res = max( $res, self::ERR_ABORT_TRX );
1067                    $blockers[] = "named lock '$lockName'";
1068                }
1069            } else {
1070                // Treat lost locks acquired either during prior transactions or during no
1071                // transaction as a session state problem.
1072                $res = max( $res, self::ERR_ABORT_SESSION );
1073                $blockers[] = "named lock '$lockName'";
1074            }
1075        }
1076        // Loss of temp tables breaks future callers relying on those tables for queries
1077        foreach ( $priorSessInfo->tempTables as $domainTempTables ) {
1078            foreach ( $domainTempTables as $tableName => $tableInfo ) {
1079                if ( $tableInfo->trxId && $tableInfo->trxId === $priorSessInfo->trxId ) {
1080                    // Treat lost temp tables created during the lost transaction as a
1081                    // transaction state problem. Connection loss on ROLLBACK (non-SAVEPOINT)
1082                    // is tolerable since rollback automatically triggered server-side.
1083                    if ( $verb !== 'ROLLBACK' ) {
1084                        $res = max( $res, self::ERR_ABORT_TRX );
1085                        $blockers[] = "temp table '$tableName'";
1086                    }
1087                } else {
1088                    // Treat lost temp tables created either during prior transactions or during
1089                    // no transaction as a session state problem.
1090                    $res = max( $res, self::ERR_ABORT_SESSION );
1091                    $blockers[] = "temp table '$tableName'";
1092                }
1093            }
1094        }
1095        // Loss of transaction writes breaks future callers and DBO_TRX logic relying on those
1096        // writes to be atomic and still pending. Connection loss on ROLLBACK (non-SAVEPOINT) is
1097        // tolerable since rollback automatically triggered server-side.
1098        if ( $priorSessInfo->trxWriteCallers && $verb !== 'ROLLBACK' ) {
1099            $res = max( $res, self::ERR_ABORT_TRX );
1100            $blockers[] = 'uncommitted writes';
1101        }
1102        if ( $priorSessInfo->trxPreCommitCbCallers && $verb !== 'ROLLBACK' ) {
1103            $res = max( $res, self::ERR_ABORT_TRX );
1104            $blockers[] = 'pre-commit callbacks';
1105        }
1106        if ( $priorSessInfo->trxExplicit && $verb !== 'ROLLBACK' && $verb !== 'COMMIT' ) {
1107            // Transaction automatically rolled back, breaking the expectations of callers
1108            // relying on the continued existence of that transaction for things like atomic
1109            // writes, serializability, or reads from the same point-in-time snapshot. If the
1110            // connection loss occurred on ROLLBACK (non-SAVEPOINT) or COMMIT, then we do not
1111            // need to mark the transaction state as corrupt, since no transaction would still
1112            // be open even if the query did succeed (T127428).
1113            $res = max( $res, self::ERR_ABORT_TRX );
1114            $blockers[] = 'explicit transaction';
1115        }
1116
1117        if ( $blockers ) {
1118            $this->logger->warning(
1119                "cannot reconnect to {db_server} silently: {error}",
1120                $this->getLogContext( [
1121                    'error' => 'session state loss (' . implode( ', ', $blockers ) . ')',
1122                    'exception' => new RuntimeException(),
1123                    'db_log_category' => 'connection'
1124                ] )
1125            );
1126        }
1127
1128        return $res;
1129    }
1130
1131    /**
1132     * Clean things up after session (and thus transaction) loss before reconnect
1133     */
1134    private function handleSessionLossPreconnect() {
1135        // Clean up tracking of session-level things...
1136        // https://mariadb.com/kb/en/create-table/#create-temporary-table
1137        // https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
1138        $this->sessionTempTables = [];
1139        // https://mariadb.com/kb/en/get_lock/
1140        // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
1141        $this->sessionNamedLocks = [];
1142        // Session loss implies transaction loss (T67263)
1143        $this->transactionManager->onSessionLoss( $this );
1144        // Clear additional subclass fields
1145        $this->doHandleSessionLossPreconnect();
1146    }
1147
1148    /**
1149     * Reset any additional subclass trx* and session* fields
1150     */
1151    protected function doHandleSessionLossPreconnect() {
1152        // no-op
1153    }
1154
1155    /**
1156     * Checks whether the cause of the error is detected to be a timeout.
1157     *
1158     * It returns false by default, and not all engines support detecting this yet.
1159     * If this returns false, it will be treated as a generic query error.
1160     *
1161     * @param int|string $errno Error number
1162     * @return bool
1163     * @since 1.39
1164     */
1165    protected function isQueryTimeoutError( $errno ) {
1166        return false;
1167    }
1168
1169    /**
1170     * Report a query error
1171     *
1172     * If $ignore is set, emit a DEBUG level log entry and continue,
1173     * otherwise, emit an ERROR level log entry and throw an exception.
1174     *
1175     * @param string $error
1176     * @param int|string $errno
1177     * @param string $sql
1178     * @param string $fname
1179     * @param bool $ignore Whether to just log an error rather than throw an exception
1180     * @throws DBQueryError
1181     */
1182    public function reportQueryError( $error, $errno, $sql, $fname, $ignore = false ) {
1183        if ( $ignore ) {
1184            $this->logger->debug(
1185                "SQL ERROR (ignored): $error",
1186                [ 'db_log_category' => 'query' ]
1187            );
1188        } else {
1189            throw $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
1190        }
1191    }
1192
1193    /**
1194     * @param string $error
1195     * @param string|int $errno
1196     * @param string $sql
1197     * @param string $fname
1198     * @return DBError
1199     */
1200    private function getQueryExceptionAndLog( $error, $errno, $sql, $fname ) {
1201        // Information that instances of the same problem have in common should
1202        // not be normalized (T255202).
1203        $this->logger->error(
1204            "Error $errno from $fname, {error} {sql1line} {db_server}",
1205            $this->getLogContext( [
1206                'method' => __METHOD__,
1207                'errno' => $errno,
1208                'error' => $error,
1209                'sql1line' => mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 ),
1210                'fname' => $fname,
1211                'db_log_category' => 'query',
1212                'exception' => new RuntimeException()
1213            ] )
1214        );
1215        return $this->getQueryException( $error, $errno, $sql, $fname );
1216    }
1217
1218    /**
1219     * @param string $error
1220     * @param string|int $errno
1221     * @param string $sql
1222     * @param string $fname
1223     * @return DBError
1224     */
1225    private function getQueryException( $error, $errno, $sql, $fname ) {
1226        if ( $this->isQueryTimeoutError( $errno ) ) {
1227            return new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
1228        } elseif ( $this->isConnectionError( $errno ) ) {
1229            return new DBQueryDisconnectedError( $this, $error, $errno, $sql, $fname );
1230        } else {
1231            return new DBQueryError( $this, $error, $errno, $sql, $fname );
1232        }
1233    }
1234
1235    /**
1236     * @param string $error
1237     * @return DBConnectionError
1238     */
1239    final protected function newExceptionAfterConnectError( $error ) {
1240        // Connection was not fully initialized and is not safe for use.
1241        // Stash any error associated with the handle before destroying it.
1242        $this->lastConnectError = $error;
1243        $this->conn = null;
1244
1245        $this->logger->error(
1246            "Error connecting to {db_server} as user {db_user}: {error}",
1247            $this->getLogContext( [
1248                'error' => $error,
1249                'exception' => new RuntimeException(),
1250                'db_log_category' => 'connection',
1251            ] )
1252        );
1253
1254        return new DBConnectionError( $this, $error );
1255    }
1256
1257    /**
1258     * Get a SelectQueryBuilder bound to this connection. This is overridden by
1259     * DBConnRef.
1260     */
1261    public function newSelectQueryBuilder(): SelectQueryBuilder {
1262        return new SelectQueryBuilder( $this );
1263    }
1264
1265    /**
1266     * Get a UnionQueryBuilder bound to this connection. This is overridden by
1267     * DBConnRef.
1268     */
1269    public function newUnionQueryBuilder(): UnionQueryBuilder {
1270        return new UnionQueryBuilder( $this );
1271    }
1272
1273    /**
1274     * Get an UpdateQueryBuilder bound to this connection. This is overridden by
1275     * DBConnRef.
1276     */
1277    public function newUpdateQueryBuilder(): UpdateQueryBuilder {
1278        return new UpdateQueryBuilder( $this );
1279    }
1280
1281    /**
1282     * Get a DeleteQueryBuilder bound to this connection. This is overridden by
1283     * DBConnRef.
1284     */
1285    public function newDeleteQueryBuilder(): DeleteQueryBuilder {
1286        return new DeleteQueryBuilder( $this );
1287    }
1288
1289    /**
1290     * Get a InsertQueryBuilder bound to this connection. This is overridden by
1291     * DBConnRef.
1292     */
1293    public function newInsertQueryBuilder(): InsertQueryBuilder {
1294        return new InsertQueryBuilder( $this );
1295    }
1296
1297    /**
1298     * Get a ReplaceQueryBuilder bound to this connection. This is overridden by
1299     * DBConnRef.
1300     */
1301    public function newReplaceQueryBuilder(): ReplaceQueryBuilder {
1302        return new ReplaceQueryBuilder( $this );
1303    }
1304
1305    /** @inheritDoc */
1306    public function selectField(
1307        $tables, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1308    ) {
1309        if ( $var === '*' ) {
1310            throw new DBUnexpectedError( $this, "Cannot use a * field" );
1311        } elseif ( is_array( $var ) && count( $var ) !== 1 ) {
1312            throw new DBUnexpectedError( $this, 'Cannot use more than one field' );
1313        }
1314
1315        $options = $this->platform->normalizeOptions( $options );
1316        $options['LIMIT'] = 1;
1317
1318        $res = $this->select( $tables, $var, $cond, $fname, $options, $join_conds );
1319        if ( $res === false ) {
1320            throw new DBUnexpectedError( $this, "Got false from select()" );
1321        }
1322
1323        $row = $res->fetchRow();
1324        if ( $row === false ) {
1325            return false;
1326        }
1327
1328        return reset( $row );
1329    }
1330
1331    /** @inheritDoc */
1332    public function selectFieldValues(
1333        $tables, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1334    ): array {
1335        if ( $var === '*' ) {
1336            throw new DBUnexpectedError( $this, "Cannot use a * field" );
1337        } elseif ( !is_string( $var ) ) {
1338            throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1339        }
1340
1341        $options = $this->platform->normalizeOptions( $options );
1342        $res = $this->select( $tables, [ 'value' => $var ], $cond, $fname, $options, $join_conds );
1343        if ( $res === false ) {
1344            throw new DBUnexpectedError( $this, "Got false from select()" );
1345        }
1346
1347        $values = [];
1348        foreach ( $res as $row ) {
1349            $values[] = $row->value;
1350        }
1351
1352        return $values;
1353    }
1354
1355    /** @inheritDoc */
1356    public function select(
1357        $tables, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1358    ) {
1359        $options = (array)$options;
1360        // Don't turn this into using platform directly, DatabaseMySQL overrides this.
1361        $sql = $this->selectSQLText( $tables, $vars, $conds, $fname, $options, $join_conds );
1362        // Treat SELECT queries with FOR UPDATE as writes. This matches
1363        // how MySQL enforces read_only (FOR SHARE and LOCK IN SHADE MODE are allowed).
1364        $flags = in_array( 'FOR UPDATE', $options, true )
1365            ? self::QUERY_CHANGE_ROWS
1366            : self::QUERY_CHANGE_NONE;
1367
1368        $query = new Query( $sql, $flags, 'SELECT' );
1369        return $this->query( $query, $fname );
1370    }
1371
1372    /** @inheritDoc */
1373    public function selectRow( $tables, $vars, $conds, $fname = __METHOD__,
1374        $options = [], $join_conds = []
1375    ) {
1376        $options = (array)$options;
1377        $options['LIMIT'] = 1;
1378
1379        $res = $this->select( $tables, $vars, $conds, $fname, $options, $join_conds );
1380        if ( $res === false ) {
1381            throw new DBUnexpectedError( $this, "Got false from select()" );
1382        }
1383
1384        if ( !$res->numRows() ) {
1385            return false;
1386        }
1387
1388        return $res->fetchObject();
1389    }
1390
1391    /**
1392     * @inheritDoc
1393     */
1394    public function estimateRowCount(
1395        $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1396    ): int {
1397        $conds = $this->platform->normalizeConditions( $conds, $fname );
1398        $column = $this->platform->extractSingleFieldFromList( $var );
1399        if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1400            $conds[] = "$column IS NOT NULL";
1401        }
1402
1403        $res = $this->select(
1404            $tables, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options, $join_conds
1405        );
1406        $row = $res ? $res->fetchRow() : [];
1407
1408        return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
1409    }
1410
1411    /** @inheritDoc */
1412    public function selectRowCount(
1413        $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1414    ): int {
1415        $conds = $this->platform->normalizeConditions( $conds, $fname );
1416        $column = $this->platform->extractSingleFieldFromList( $var );
1417        if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1418            $conds[] = "$column IS NOT NULL";
1419        }
1420        if ( in_array( 'DISTINCT', (array)$options ) ) {
1421            if ( $column === null ) {
1422                throw new DBUnexpectedError( $this,
1423                    '$var cannot be empty when the DISTINCT option is given' );
1424            }
1425            $innerVar = $column;
1426        } else {
1427            $innerVar = '1';
1428        }
1429
1430        $res = $this->select(
1431            [
1432                'tmp_count' => $this->platform->buildSelectSubquery(
1433                    $tables,
1434                    $innerVar,
1435                    $conds,
1436                    $fname,
1437                    $options,
1438                    $join_conds
1439                )
1440            ],
1441            [ 'rowcount' => 'COUNT(*)' ],
1442            [],
1443            $fname
1444        );
1445        $row = $res ? $res->fetchRow() : [];
1446
1447        return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
1448    }
1449
1450    /** @inheritDoc */
1451    public function lockForUpdate(
1452        $table, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1453    ) {
1454        if ( !$this->trxLevel() && !$this->flagsHolder->hasImplicitTrxFlag() ) {
1455            throw new DBUnexpectedError(
1456                $this,
1457                __METHOD__ . ': no transaction is active nor is DBO_TRX set'
1458            );
1459        }
1460
1461        $options = (array)$options;
1462        $options[] = 'FOR UPDATE';
1463
1464        return $this->selectRowCount( $table, '*', $conds, $fname, $options, $join_conds );
1465    }
1466
1467    /** @inheritDoc */
1468    public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1469        $info = $this->fieldInfo( $table, $field );
1470
1471        return (bool)$info;
1472    }
1473
1474    /** @inheritDoc */
1475    abstract public function tableExists( $table, $fname = __METHOD__ );
1476
1477    /** @inheritDoc */
1478    public function indexExists( $table, $index, $fname = __METHOD__ ) {
1479        $info = $this->indexInfo( $table, $index, $fname );
1480
1481        return (bool)$info;
1482    }
1483
1484    /** @inheritDoc */
1485    public function indexUnique( $table, $index, $fname = __METHOD__ ) {
1486        $info = $this->indexInfo( $table, $index, $fname );
1487
1488        return $info ? $info['unique'] : null;
1489    }
1490
1491    /** @inheritDoc */
1492    abstract public function getPrimaryKeyColumns( $table, $fname = __METHOD__ );
1493
1494    /**
1495     * Get information about an index into an object
1496     *
1497     * @param string $table The unqualified name of a table
1498     * @param string $index Index name
1499     * @param string $fname Calling function name
1500     * @return array<string,mixed>|false Index info map; false if it does not exist
1501     * @phan-return array{unique:bool}|false
1502     */
1503    abstract public function indexInfo( $table, $index, $fname = __METHOD__ );
1504
1505    /** @inheritDoc */
1506    public function insert( $table, $rows, $fname = __METHOD__, $options = [] ) {
1507        $query = $this->platform->dispatchingInsertSqlText( $table, $rows, $options );
1508        if ( !$query ) {
1509            return true;
1510        }
1511        $this->query( $query, $fname );
1512        if ( $this->strictWarnings ) {
1513            $this->checkInsertWarnings( $query, $fname );
1514        }
1515        return true;
1516    }
1517
1518    /**
1519     * Check for warnings after performing an INSERT query, and throw exceptions
1520     * if necessary.
1521     *
1522     * @param Query $query
1523     * @param string $fname
1524     * @return void
1525     */
1526    protected function checkInsertWarnings( Query $query, $fname ) {
1527    }
1528
1529    /** @inheritDoc */
1530    public function update( $table, $set, $conds, $fname = __METHOD__, $options = [] ) {
1531        $query = $this->platform->updateSqlText( $table, $set, $conds, $options );
1532        $this->query( $query, $fname );
1533
1534        return true;
1535    }
1536
1537    /** @inheritDoc */
1538    public function databasesAreIndependent() {
1539        return false;
1540    }
1541
1542    /** @inheritDoc */
1543    final public function selectDomain( $domain ) {
1544        $cs = $this->commenceCriticalSection( __METHOD__ );
1545
1546        try {
1547            $this->doSelectDomain( DatabaseDomain::newFromId( $domain ) );
1548        } catch ( DBError $e ) {
1549            $this->completeCriticalSection( __METHOD__, $cs );
1550            throw $e;
1551        }
1552
1553        $this->completeCriticalSection( __METHOD__, $cs );
1554    }
1555
1556    /**
1557     * @param DatabaseDomain $domain
1558     * @throws DBConnectionError
1559     * @throws DBError
1560     * @since 1.32
1561     */
1562    protected function doSelectDomain( DatabaseDomain $domain ) {
1563        $this->currentDomain = $domain;
1564        $this->platform->setCurrentDomain( $this->currentDomain );
1565    }
1566
1567    /** @inheritDoc */
1568    public function getDBname() {
1569        return $this->currentDomain->getDatabase();
1570    }
1571
1572    /** @inheritDoc */
1573    public function getServer() {
1574        return $this->connectionParams[self::CONN_SERVER] ?? null;
1575    }
1576
1577    /** @inheritDoc */
1578    public function getServerName() {
1579        return $this->serverName ?? $this->getServer() ?? 'unknown';
1580    }
1581
1582    /** @inheritDoc */
1583    public function addQuotes( $s ) {
1584        if ( $s instanceof RawSQLValue ) {
1585            return $s->toSql();
1586        }
1587        if ( $s instanceof Blob ) {
1588            $s = $s->fetch();
1589        }
1590        if ( $s === null ) {
1591            return 'NULL';
1592        } elseif ( is_bool( $s ) ) {
1593            return (string)(int)$s;
1594        } elseif ( is_int( $s ) ) {
1595            return (string)$s;
1596        } else {
1597            return "'" . $this->strencode( $s ) . "'";
1598        }
1599    }
1600
1601    /** @inheritDoc */
1602    public function expr( string $field, string $op, $value ): Expression {
1603        return new Expression( $field, $op, $value );
1604    }
1605
1606    /** @inheritDoc */
1607    public function andExpr( array $conds ): AndExpressionGroup {
1608        return AndExpressionGroup::newFromArray( $conds );
1609    }
1610
1611    /** @inheritDoc */
1612    public function orExpr( array $conds ): OrExpressionGroup {
1613        return OrExpressionGroup::newFromArray( $conds );
1614    }
1615
1616    /** @inheritDoc */
1617    public function replace( $table, $uniqueKeys, $rows, $fname = __METHOD__ ) {
1618        $uniqueKey = $this->platform->normalizeUpsertParams( $uniqueKeys, $rows );
1619        if ( !$rows ) {
1620            return;
1621        }
1622        $affectedRowCount = 0;
1623        $insertId = null;
1624        $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
1625        try {
1626            foreach ( $rows as $row ) {
1627                // Delete any conflicting rows (including ones inserted from $rows)
1628                $query = $this->platform->deleteSqlText(
1629                    $table,
1630                    [ $this->platform->makeKeyCollisionCondition( [ $row ], $uniqueKey ) ]
1631                );
1632                $this->query( $query, $fname );
1633                // Insert the new row
1634                $query = $this->platform->dispatchingInsertSqlText( $table, $row, [] );
1635                $this->query( $query, $fname );
1636                $affectedRowCount += $this->lastQueryAffectedRows;
1637                $insertId = $insertId ?: $this->lastQueryInsertId;
1638            }
1639            $this->endAtomic( $fname );
1640        } catch ( DBError $e ) {
1641            $this->cancelAtomic( $fname );
1642            throw $e;
1643        }
1644        $this->lastEmulatedAffectedRows = $affectedRowCount;
1645        $this->lastEmulatedInsertId = $insertId;
1646    }
1647
1648    /** @inheritDoc */
1649    public function upsert( $table, array $rows, $uniqueKeys, array $set, $fname = __METHOD__ ) {
1650        $uniqueKey = $this->platform->normalizeUpsertParams( $uniqueKeys, $rows );
1651        if ( !$rows ) {
1652            return true;
1653        }
1654        $this->platform->assertValidUpsertSetArray( $set, $uniqueKey, $rows );
1655
1656        $encTable = $this->tableName( $table );
1657        $sqlColumnAssignments = $this->makeList( $set, self::LIST_SET );
1658        // Get any AUTO_INCREMENT/SERIAL column for this table so we can set insertId()
1659        $autoIncrementColumn = $this->getInsertIdColumnForUpsert( $table );
1660        // Check if there is a SQL assignment expression in $set (as generated by SQLPlatform::buildExcludedValue)
1661        $useWith = array_any(
1662            $set,
1663            static fn ( $v, $k ) => ( $v instanceof RawSQLValue || is_int( $k ) )
1664        );
1665        // Subclasses might need explicit type casting within "WITH...AS (VALUES ...)"
1666        // so that these CTE rows can be referenced within the SET clause assignments.
1667        $typeByColumn = $useWith ? $this->getValueTypesForWithClause( $table ) : [];
1668
1669        $first = true;
1670        $affectedRowCount = 0;
1671        $insertId = null;
1672        $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
1673        try {
1674            foreach ( $rows as $row ) {
1675                // Update any existing conflicting row (including ones inserted from $rows)
1676                [ $sqlColumns, $sqlTuples, $sqlVals ] = $this->platform->makeInsertLists(
1677                    [ $row ],
1678                    '__',
1679                    $typeByColumn
1680                );
1681                $sqlConditions = $this->platform->makeKeyCollisionCondition(
1682                    [ $row ],
1683                    $uniqueKey
1684                );
1685                $query = new Query(
1686                    ( $useWith ? "WITH __VALS ($sqlVals) AS (VALUES $sqlTuples" : "" ) .
1687                        "UPDATE $encTable SET $sqlColumnAssignments " .
1688                        "WHERE ($sqlConditions)",
1689                    self::QUERY_CHANGE_ROWS,
1690                    'UPDATE',
1691                    $table
1692                );
1693                $this->query( $query, $fname );
1694                $rowsUpdated = $this->lastQueryAffectedRows;
1695                $affectedRowCount += $rowsUpdated;
1696                if ( $rowsUpdated > 0 ) {
1697                    // Conflicting row found and updated
1698                    if ( $first && $autoIncrementColumn !== null ) {
1699                        // @TODO: use "RETURNING" instead (when supported by SQLite)
1700                        $query = new Query(
1701                            "SELECT $autoIncrementColumn AS id FROM $encTable " .
1702                            "WHERE ($sqlConditions)",
1703                            self::QUERY_CHANGE_NONE,
1704                            'SELECT'
1705                        );
1706                        $sRes = $this->query( $query, $fname, self::QUERY_CHANGE_ROWS );
1707                        $insertId = (int)$sRes->fetchRow()['id'];
1708                    }
1709                } else {
1710                    // No conflicting row found
1711                    $query = new Query(
1712                        "INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples",
1713                        self::QUERY_CHANGE_ROWS,
1714                        'INSERT',
1715                        $table
1716                    );
1717                    $this->query( $query, $fname );
1718                    $affectedRowCount += $this->lastQueryAffectedRows;
1719                }
1720                $first = false;
1721            }
1722            $this->endAtomic( $fname );
1723        } catch ( DBError $e ) {
1724            $this->cancelAtomic( $fname );
1725            throw $e;
1726        }
1727        $this->lastEmulatedAffectedRows = $affectedRowCount;
1728        $this->lastEmulatedInsertId = $insertId;
1729        return true;
1730    }
1731
1732    /**
1733     * @param string $table The unqualified name of a table
1734     * @return string|null The AUTO_INCREMENT/SERIAL column; null if not needed
1735     */
1736    protected function getInsertIdColumnForUpsert( $table ) {
1737        return null;
1738    }
1739
1740    /**
1741     * @param string $table The unqualified name of a table
1742     * @return array<string,string> Map of (column => type); [] if not needed
1743     */
1744    protected function getValueTypesForWithClause( $table ) {
1745        return [];
1746    }
1747
1748    /** @inheritDoc */
1749    public function deleteJoin(
1750        $delTable,
1751        $joinTable,
1752        $delVar,
1753        $joinVar,
1754        $conds,
1755        $fname = __METHOD__
1756    ) {
1757        $sql = $this->platform->deleteJoinSqlText( $delTable, $joinTable, $delVar, $joinVar, $conds );
1758        $query = new Query( $sql, self::QUERY_CHANGE_ROWS, 'DELETE', $delTable );
1759        $this->query( $query, $fname );
1760    }
1761
1762    /** @inheritDoc */
1763    public function delete( $table, $conds, $fname = __METHOD__ ) {
1764        $this->query( $this->platform->deleteSqlText( $table, $conds ), $fname );
1765
1766        return true;
1767    }
1768
1769    /** @inheritDoc */
1770    final public function insertSelect(
1771        $destTable,
1772        $srcTable,
1773        $varMap,
1774        $conds,
1775        $fname = __METHOD__,
1776        $insertOptions = [],
1777        $selectOptions = [],
1778        $selectJoinConds = []
1779    ) {
1780        static $hints = [ 'NO_AUTO_COLUMNS' ];
1781
1782        $insertOptions = $this->platform->normalizeOptions( $insertOptions );
1783        $selectOptions = $this->platform->normalizeOptions( $selectOptions );
1784
1785        if ( $this->cliMode && $this->isInsertSelectSafe( $insertOptions, $selectOptions, $fname ) ) {
1786            // For massive migrations with downtime, we don't want to select everything
1787            // into memory and OOM, so do all this native on the server side if possible.
1788            $this->doInsertSelectNative(
1789                $destTable,
1790                $srcTable,
1791                $varMap,
1792                $conds,
1793                $fname,
1794                array_diff( $insertOptions, $hints ),
1795                $selectOptions,
1796                $selectJoinConds
1797            );
1798        } else {
1799            $this->doInsertSelectGeneric(
1800                $destTable,
1801                $srcTable,
1802                $varMap,
1803                $conds,
1804                $fname,
1805                array_diff( $insertOptions, $hints ),
1806                $selectOptions,
1807                $selectJoinConds
1808            );
1809        }
1810
1811        return true;
1812    }
1813
1814    /**
1815     * @param array $insertOptions
1816     * @param array $selectOptions
1817     * @param string $fname
1818     * @return bool Whether an INSERT SELECT with these options will be replication safe
1819     * @since 1.31
1820     */
1821    protected function isInsertSelectSafe( array $insertOptions, array $selectOptions, $fname ) {
1822        return true;
1823    }
1824
1825    /**
1826     * Implementation of insertSelect() based on select() and insert()
1827     *
1828     * @see IDatabase::insertSelect()
1829     * @param string $destTable Unqualified name of destination table
1830     * @param string|array $srcTable Unqualified name of source table
1831     * @param array $varMap
1832     * @param array $conds
1833     * @param string $fname
1834     * @param array $insertOptions
1835     * @param array $selectOptions
1836     * @param array $selectJoinConds
1837     * @since 1.35
1838     */
1839    private function doInsertSelectGeneric(
1840        $destTable,
1841        $srcTable,
1842        array $varMap,
1843        $conds,
1844        $fname,
1845        array $insertOptions,
1846        array $selectOptions,
1847        $selectJoinConds
1848    ) {
1849        // For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
1850        // on only the primary DB (without needing row-based-replication). It also makes it easy to
1851        // know how big the INSERT is going to be.
1852        $fields = [];
1853        foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
1854            $fields[] = $this->platform->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
1855        }
1856        $res = $this->select(
1857            $srcTable,
1858            implode( ',', $fields ),
1859            $conds,
1860            $fname,
1861            array_merge( $selectOptions, [ 'FOR UPDATE' ] ),
1862            $selectJoinConds
1863        );
1864
1865        $affectedRowCount = 0;
1866        $insertId = null;
1867        if ( $res ) {
1868            $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
1869            try {
1870                $rows = [];
1871                foreach ( $res as $row ) {
1872                    $rows[] = (array)$row;
1873                }
1874                // Avoid inserts that are too huge
1875                $rowBatches = array_chunk( $rows, $this->nonNativeInsertSelectBatchSize );
1876                foreach ( $rowBatches as $rows ) {
1877                    $query = $this->platform->dispatchingInsertSqlText( $destTable, $rows, $insertOptions );
1878                    $this->query( $query, $fname );
1879                    $affectedRowCount += $this->lastQueryAffectedRows;
1880                    $insertId = $insertId ?: $this->lastQueryInsertId;
1881                }
1882                $this->endAtomic( $fname );
1883            } catch ( DBError $e ) {
1884                $this->cancelAtomic( $fname );
1885                throw $e;
1886            }
1887        }
1888        $this->lastEmulatedAffectedRows = $affectedRowCount;
1889        $this->lastEmulatedInsertId = $insertId;
1890    }
1891
1892    /**
1893     * Native server-side implementation of insertSelect() for situations where
1894     * we don't want to select everything into memory
1895     *
1896     * @see IDatabase::insertSelect()
1897     * @param string $destTable The unqualified name of destination table
1898     * @param string|array $srcTable The unqualified name of source table
1899     * @param array $varMap
1900     * @param array $conds
1901     * @param string $fname
1902     * @param array $insertOptions
1903     * @param array $selectOptions
1904     * @param array $selectJoinConds
1905     * @since 1.35
1906     */
1907    protected function doInsertSelectNative(
1908        $destTable,
1909        $srcTable,
1910        array $varMap,
1911        $conds,
1912        $fname,
1913        array $insertOptions,
1914        array $selectOptions,
1915        $selectJoinConds
1916    ) {
1917        $sql = $this->platform->insertSelectNativeSqlText(
1918            $destTable,
1919            $srcTable,
1920            $varMap,
1921            $conds,
1922            $fname,
1923            $insertOptions,
1924            $selectOptions,
1925            $selectJoinConds
1926        );
1927        $query = new Query(
1928            $sql,
1929            self::QUERY_CHANGE_ROWS,
1930            'INSERT',
1931            $destTable
1932        );
1933        $this->query( $query, $fname );
1934    }
1935
1936    /**
1937     * Do not use this method outside of Database/DBError classes
1938     *
1939     * @param int|string $errno
1940     * @return bool Whether the given query error was a connection drop
1941     * @since 1.38
1942     */
1943    protected function isConnectionError( $errno ) {
1944        return false;
1945    }
1946
1947    /**
1948     * @param int|string $errno
1949     * @return bool Whether it is known that the last query error only caused statement rollback
1950     * @note This is for backwards compatibility for callers catching DBError exceptions in
1951     *   order to ignore problems like duplicate key errors or foreign key violations
1952     * @since 1.39
1953     */
1954    protected function isKnownStatementRollbackError( $errno ) {
1955        return false; // don't know; it could have caused a transaction rollback
1956    }
1957
1958    /**
1959     * @inheritDoc
1960     */
1961    public function serverIsReadOnly() {
1962        return false;
1963    }
1964
1965    /** @inheritDoc */
1966    final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
1967        $this->transactionManager->onTransactionResolution( $this, $callback, $fname );
1968    }
1969
1970    /** @inheritDoc */
1971    final public function onTransactionCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
1972        if ( !$this->trxLevel() && $this->getTransactionRoundFname() !== null ) {
1973            // This DB handle is set to participate in LoadBalancer transaction rounds and
1974            // an explicit transaction round is active. Start an implicit transaction on this
1975            // DB handle (setting trxAutomatic) similar to how query() does in such situations.
1976            $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
1977        }
1978
1979        $this->transactionManager->addPostCommitOrIdleCallback( $callback, $fname );
1980        if ( !$this->trxLevel() ) {
1981            $dbErrors = [];
1982            $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE, $dbErrors );
1983            if ( $dbErrors ) {
1984                throw $dbErrors[0];
1985            }
1986        }
1987    }
1988
1989    /** @inheritDoc */
1990    final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
1991        if ( !$this->trxLevel() && $this->getTransactionRoundFname() !== null ) {
1992            // This DB handle is set to participate in LoadBalancer transaction rounds and
1993            // an explicit transaction round is active. Start an implicit transaction on this
1994            // DB handle (setting trxAutomatic) similar to how query() does in such situations.
1995            $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
1996        }
1997
1998        if ( $this->trxLevel() ) {
1999            $this->transactionManager->addPreCommitOrIdleCallback(
2000                $callback,
2001                $fname
2002            );
2003        } else {
2004            // No transaction is active nor will start implicitly, so make one for this callback
2005            $this->startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
2006            try {
2007                $callback( $this );
2008            } catch ( Throwable $e ) {
2009                // Avoid confusing error reporting during critical section errors
2010                if ( !$this->csmError ) {
2011                    $this->cancelAtomic( __METHOD__ );
2012                }
2013                throw $e;
2014            }
2015            $this->endAtomic( __METHOD__ );
2016        }
2017    }
2018
2019    /** @inheritDoc */
2020    final public function setTransactionListener( $name, ?callable $callback = null ) {
2021        $this->transactionManager->setTransactionListener( $name, $callback );
2022    }
2023
2024    /**
2025     * Whether to disable running of post-COMMIT/ROLLBACK callbacks
2026     *
2027     * @internal This method should not be used outside of Database/LoadBalancer
2028     *
2029     * @since 1.28
2030     * @param bool $suppress
2031     */
2032    final public function setTrxEndCallbackSuppression( $suppress ) {
2033        $this->transactionManager->setTrxEndCallbackSuppression( $suppress );
2034    }
2035
2036    /**
2037     * Consume and run any "on transaction idle/resolution" callbacks
2038     *
2039     * @internal This method should not be used outside of Database/LoadBalancer
2040     *
2041     * @since 1.20
2042     * @param int $trigger IDatabase::TRIGGER_* constant
2043     * @param DBError[] &$errors DB exceptions caught [returned]
2044     * @return int Number of callbacks attempted
2045     * @throws DBUnexpectedError
2046     * @throws Throwable Any non-DBError exception thrown by a callback
2047     */
2048    public function runOnTransactionIdleCallbacks( $trigger, array &$errors = [] ) {
2049        if ( $this->trxLevel() ) {
2050            throw new DBUnexpectedError( $this, __METHOD__ . ': a transaction is still open' );
2051        }
2052
2053        if ( $this->transactionManager->isEndCallbacksSuppressed() ) {
2054            // Execution deferred by LoadBalancer for explicit execution later
2055            return 0;
2056        }
2057
2058        $cs = $this->commenceCriticalSection( __METHOD__ );
2059
2060        $count = 0;
2061        $autoTrx = $this->flagsHolder->hasImplicitTrxFlag(); // automatic begin() enabled?
2062        // Drain the queues of transaction "idle" and "end" callbacks until they are empty
2063        do {
2064            $callbackEntries = $this->transactionManager->consumeEndCallbacks();
2065            $count += count( $callbackEntries );
2066            foreach ( $callbackEntries as $entry ) {
2067                $this->flagsHolder->clearFlag( self::DBO_TRX ); // make each query its own transaction
2068                try {
2069                    $entry[0]( $trigger );
2070                } catch ( DBError $ex ) {
2071                    ( $this->errorLogger )( $ex );
2072                    $errors[] = $ex;
2073                    // Some callbacks may use startAtomic/endAtomic, so make sure
2074                    // their transactions are ended so other callbacks don't fail
2075                    if ( $this->trxLevel() ) {
2076                        $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
2077                    }
2078                } finally {
2079                    if ( $autoTrx ) {
2080                        $this->flagsHolder->setFlag( self::DBO_TRX ); // restore automatic begin()
2081                    } else {
2082                        $this->flagsHolder->clearFlag( self::DBO_TRX ); // restore auto-commit
2083                    }
2084                }
2085            }
2086        } while ( $this->transactionManager->countPostCommitOrIdleCallbacks() );
2087
2088        $this->completeCriticalSection( __METHOD__, $cs );
2089
2090        return $count;
2091    }
2092
2093    /**
2094     * Actually run any "transaction listener" callbacks
2095     *
2096     * @internal This method should not be used outside of Database/LoadBalancer
2097     *
2098     * @since 1.20
2099     * @param int $trigger IDatabase::TRIGGER_* constant
2100     * @param DBError[] &$errors DB exceptions caught [returned]
2101     * @throws Throwable Any non-DBError exception thrown by a callback
2102     */
2103    public function runTransactionListenerCallbacks( $trigger, array &$errors = [] ) {
2104        if ( $this->transactionManager->isEndCallbacksSuppressed() ) {
2105            // Execution deferred by LoadBalancer for explicit execution later
2106            return;
2107        }
2108
2109        // These callbacks should only be registered in setup, thus no iteration is needed
2110        foreach ( $this->transactionManager->getRecurringCallbacks() as $callback ) {
2111            try {
2112                $callback( $trigger, $this );
2113            } catch ( DBError $ex ) {
2114                ( $this->errorLogger )( $ex );
2115                $errors[] = $ex;
2116            }
2117        }
2118    }
2119
2120    /**
2121     * Handle "on transaction idle/resolution" and "transaction listener" callbacks post-COMMIT
2122     *
2123     * @throws DBError The first DBError exception thrown by a callback
2124     * @throws Throwable Any non-DBError exception thrown by a callback
2125     */
2126    private function runTransactionPostCommitCallbacks() {
2127        $dbErrors = [];
2128        $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT, $dbErrors );
2129        $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT, $dbErrors );
2130        $this->lastEmulatedAffectedRows = 0; // for the sake of consistency
2131        if ( $dbErrors ) {
2132            throw $dbErrors[0];
2133        }
2134    }
2135
2136    /**
2137     * Handle "on transaction idle/resolution" and "transaction listener" callbacks post-ROLLBACK
2138     *
2139     * This will suppress and log any DBError exceptions
2140     *
2141     * @throws Throwable Any non-DBError exception thrown by a callback
2142     */
2143    private function runTransactionPostRollbackCallbacks() {
2144        $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
2145        $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
2146        $this->lastEmulatedAffectedRows = 0; // for the sake of consistency
2147    }
2148
2149    /** @inheritDoc */
2150    final public function startAtomic(
2151        $fname = __METHOD__,
2152        $cancelable = self::ATOMIC_NOT_CANCELABLE
2153    ) {
2154        $cs = $this->commenceCriticalSection( __METHOD__ );
2155
2156        if ( $this->trxLevel() ) {
2157            // This atomic section is only one part of a larger transaction
2158            $sectionOwnsTrx = false;
2159        } else {
2160            // Start an implicit transaction (sets trxAutomatic)
2161            try {
2162                $this->begin( $fname, self::TRANSACTION_INTERNAL );
2163            } catch ( DBError $e ) {
2164                $this->completeCriticalSection( __METHOD__, $cs );
2165                throw $e;
2166            }
2167            if ( $this->flagsHolder->hasImplicitTrxFlag() ) {
2168                // This DB handle participates in LoadBalancer transaction rounds; all atomic
2169                // sections should be buffered into one transaction (e.g. to keep web requests
2170                // transactional). Note that an implicit transaction round is considered to be
2171                // active when no there is no explicit transaction round.
2172                $sectionOwnsTrx = false;
2173            } else {
2174                // This DB handle does not participate in LoadBalancer transaction rounds;
2175                // each topmost atomic section will use its own transaction.
2176                $sectionOwnsTrx = true;
2177            }
2178            $this->transactionManager->setAutomaticAtomic( $sectionOwnsTrx );
2179        }
2180
2181        if ( $cancelable === self::ATOMIC_CANCELABLE ) {
2182            if ( $sectionOwnsTrx ) {
2183                // This atomic section is synonymous with the whole transaction; just
2184                // use full COMMIT/ROLLBACK in endAtomic()/cancelAtomic(), respectively
2185                $savepointId = self::NOT_APPLICABLE;
2186            } else {
2187                // This atomic section is only part of the whole transaction; use a SAVEPOINT
2188                // query so that its changes can be cancelled without losing the rest of the
2189                // transaction (e.g. changes from other sections or from outside of sections)
2190                try {
2191                    $savepointId = $this->transactionManager->nextSavePointId( $this, $fname );
2192                    $sql = $this->platform->savepointSqlText( $savepointId );
2193                    $query = new Query( $sql, self::QUERY_CHANGE_TRX, 'SAVEPOINT' );
2194                    $this->query( $query, $fname );
2195                } catch ( DBError $e ) {
2196                    $this->completeCriticalSection( __METHOD__, $cs, $e );
2197                    throw $e;
2198                }
2199            }
2200        } else {
2201            $savepointId = null;
2202        }
2203
2204        $sectionId = new AtomicSectionIdentifier;
2205        $this->transactionManager->addToAtomicLevels( $fname, $sectionId, $savepointId );
2206
2207        $this->completeCriticalSection( __METHOD__, $cs );
2208
2209        return $sectionId;
2210    }
2211
2212    /** @inheritDoc */
2213    final public function endAtomic( $fname = __METHOD__ ) {
2214        [ $savepointId, $sectionId ] = $this->transactionManager->onEndAtomic( $this, $fname );
2215
2216        $runPostCommitCallbacks = false;
2217
2218        $cs = $this->commenceCriticalSection( __METHOD__ );
2219
2220        // Remove the last section (no need to re-index the array)
2221        $finalLevelOfImplicitTrxPopped = $this->transactionManager->popAtomicLevel();
2222
2223        try {
2224            if ( $finalLevelOfImplicitTrxPopped ) {
2225                $this->commit( $fname, self::FLUSHING_INTERNAL );
2226                $runPostCommitCallbacks = true;
2227            } elseif ( $savepointId !== null && $savepointId !== self::NOT_APPLICABLE ) {
2228                $sql = $this->platform->releaseSavepointSqlText( $savepointId );
2229                $query = new Query( $sql, self::QUERY_CHANGE_TRX, 'RELEASE SAVEPOINT' );
2230                $this->query( $query, $fname );
2231            }
2232        } catch ( DBError $e ) {
2233            $this->completeCriticalSection( __METHOD__, $cs, $e );
2234            throw $e;
2235        }
2236
2237        $this->transactionManager->onEndAtomicInCriticalSection( $sectionId );
2238
2239        $this->completeCriticalSection( __METHOD__, $cs );
2240
2241        if ( $runPostCommitCallbacks ) {
2242            $this->runTransactionPostCommitCallbacks();
2243        }
2244    }
2245
2246    /** @inheritDoc */
2247    final public function cancelAtomic(
2248        $fname = __METHOD__,
2249        ?AtomicSectionIdentifier $sectionId = null
2250    ) {
2251        $this->transactionManager->onCancelAtomicBeforeCriticalSection( $this, $fname );
2252        $pos = $this->transactionManager->getPositionFromSectionId( $sectionId );
2253        if ( $pos < 0 ) {
2254            throw new DBUnexpectedError( $this, "Atomic section not found (for $fname)" );
2255        }
2256
2257        $cs = $this->commenceCriticalSection( __METHOD__ );
2258        $runPostRollbackCallbacks = false;
2259        [ $savedFname, $excisedSectionIds, $newTopSectionId, $savedSectionId, $savepointId ] =
2260            $this->transactionManager->cancelAtomic( $pos );
2261
2262        try {
2263            if ( $savedFname !== $fname ) {
2264                $e = new DBUnexpectedError(
2265                    $this,
2266                    "Invalid atomic section ended (got $fname but expected $savedFname)"
2267                );
2268                $this->completeCriticalSection( __METHOD__, $cs, $e );
2269                throw $e;
2270            }
2271
2272            // Remove the last section (no need to re-index the array)
2273            $this->transactionManager->popAtomicLevel();
2274            $excisedSectionIds[] = $savedSectionId;
2275            $newTopSectionId = $this->transactionManager->currentAtomicSectionId();
2276
2277            if ( $savepointId !== null ) {
2278                // Rollback the transaction changes proposed within this atomic section
2279                if ( $savepointId === self::NOT_APPLICABLE ) {
2280                    // Atomic section started the transaction; rollback the whole transaction
2281                    // and trigger cancellation callbacks for all active atomic sections
2282                    $this->rollback( $fname, self::FLUSHING_INTERNAL );
2283                    $runPostRollbackCallbacks = true;
2284                } else {
2285                    // Atomic section nested within the transaction; rollback the transaction
2286                    // to the state prior to this section and trigger its cancellation callbacks
2287                    $sql = $this->platform->rollbackToSavepointSqlText( $savepointId );
2288                    $query = new Query( $sql, self::QUERY_CHANGE_TRX, 'ROLLBACK TO SAVEPOINT' );
2289                    $this->query( $query, $fname );
2290                    $this->transactionManager->setTrxStatusToOk(); // no exception; recovered
2291                }
2292            } else {
2293                // Put the transaction into an error state if it's not already in one
2294                $trxError = new DBUnexpectedError(
2295                    $this,
2296                    "Uncancelable atomic section canceled (got $fname)"
2297                );
2298                $this->transactionManager->setTransactionError( $trxError );
2299            }
2300        } finally {
2301            // Fix up callbacks owned by the sections that were just cancelled.
2302            // All callbacks should have an owner that is present in trxAtomicLevels.
2303            $this->transactionManager->modifyCallbacksForCancel(
2304                $excisedSectionIds,
2305                $newTopSectionId
2306            );
2307        }
2308
2309        $this->lastEmulatedAffectedRows = 0; // for the sake of consistency
2310
2311        $this->completeCriticalSection( __METHOD__, $cs );
2312
2313        if ( $runPostRollbackCallbacks ) {
2314            $this->runTransactionPostRollbackCallbacks();
2315        }
2316    }
2317
2318    /** @inheritDoc */
2319    final public function doAtomicSection(
2320        $fname,
2321        callable $callback,
2322        $cancelable = self::ATOMIC_NOT_CANCELABLE
2323    ) {
2324        $sectionId = $this->startAtomic( $fname, $cancelable );
2325        try {
2326            $res = $callback( $this, $fname );
2327        } catch ( Throwable $e ) {
2328            // Avoid confusing error reporting during critical section errors
2329            if ( !$this->csmError ) {
2330                $this->cancelAtomic( $fname, $sectionId );
2331            }
2332
2333            throw $e;
2334        }
2335        $this->endAtomic( $fname );
2336
2337        return $res;
2338    }
2339
2340    /** @inheritDoc */
2341    final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
2342        static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
2343        if ( !in_array( $mode, $modes, true ) ) {
2344            throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'" );
2345        }
2346
2347        $this->transactionManager->onBegin( $this, $fname );
2348
2349        if ( $this->flagsHolder->hasImplicitTrxFlag() && $mode !== self::TRANSACTION_INTERNAL ) {
2350            $msg = "$fname: implicit transaction expected (DBO_TRX set)";
2351            throw new DBUnexpectedError( $this, $msg );
2352        }
2353
2354        $this->assertHasConnectionHandle();
2355
2356        $cs = $this->commenceCriticalSection( __METHOD__ );
2357        $timeStart = microtime( true );
2358        try {
2359            $this->doBegin( $fname );
2360        } catch ( DBError $e ) {
2361            $this->completeCriticalSection( __METHOD__, $cs );
2362            throw $e;
2363        }
2364        $timeEnd = microtime( true );
2365        // Treat "BEGIN" as a trivial query to gauge the RTT delay
2366        $rtt = max( $timeEnd - $timeStart, 0.0 );
2367        $this->transactionManager->onBeginInCriticalSection( $mode, $fname, $rtt );
2368        $this->replicationReporter->resetReplicationLagStatus( $this );
2369        $this->completeCriticalSection( __METHOD__, $cs );
2370    }
2371
2372    /**
2373     * Issues the BEGIN command to the database server.
2374     *
2375     * @see Database::begin()
2376     * @param string $fname
2377     * @throws DBError
2378     */
2379    protected function doBegin( $fname ) {
2380        $query = new Query( 'BEGIN', self::QUERY_CHANGE_TRX, 'BEGIN' );
2381        $this->query( $query, $fname );
2382    }
2383
2384    /** @inheritDoc */
2385    final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
2386        static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
2387        if ( !in_array( $flush, $modes, true ) ) {
2388            throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'" );
2389        }
2390
2391        if ( !$this->transactionManager->onCommit( $this, $fname, $flush ) ) {
2392            return;
2393        }
2394
2395        $this->assertHasConnectionHandle();
2396
2397        $this->runOnTransactionPreCommitCallbacks();
2398
2399        $cs = $this->commenceCriticalSection( __METHOD__ );
2400        try {
2401            if ( $this->trxLevel() ) {
2402                $query = new Query( 'COMMIT', self::QUERY_CHANGE_TRX, 'COMMIT' );
2403                $this->query( $query, $fname );
2404            }
2405        } catch ( DBError $e ) {
2406            $this->completeCriticalSection( __METHOD__, $cs );
2407            throw $e;
2408        }
2409        $lastWriteTime = $this->transactionManager->onCommitInCriticalSection( $this );
2410        if ( $lastWriteTime ) {
2411            $this->lastWriteTime = $lastWriteTime;
2412        }
2413        // With FLUSHING_ALL_PEERS, callbacks will run when requested by a dedicated phase
2414        // within LoadBalancer. With FLUSHING_INTERNAL, callbacks will run when requested by
2415        // the Database caller during a safe point. This avoids isolation and recursion issues.
2416        if ( $flush === self::FLUSHING_ONE ) {
2417            $this->runTransactionPostCommitCallbacks();
2418        }
2419        $this->completeCriticalSection( __METHOD__, $cs );
2420    }
2421
2422    /** @inheritDoc */
2423    final public function rollback( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
2424        if (
2425            $flush !== self::FLUSHING_INTERNAL &&
2426            $flush !== self::FLUSHING_ALL_PEERS &&
2427            $this->flagsHolder->hasImplicitTrxFlag()
2428        ) {
2429            throw new DBUnexpectedError(
2430                $this,
2431                "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)"
2432            );
2433        }
2434
2435        if ( !$this->trxLevel() ) {
2436            $this->transactionManager->setTrxStatusToNone();
2437            $this->transactionManager->clearPreEndCallbacks();
2438            if ( $this->transactionManager->trxLevel() === TransactionManager::STATUS_TRX_ERROR ) {
2439                $this->logger->info(
2440                    "$fname: acknowledged server-side transaction loss on {db_server}",
2441                    $this->getLogContext()
2442                );
2443            }
2444
2445            return;
2446        }
2447
2448        $this->assertHasConnectionHandle();
2449
2450        if ( $this->csmError ) {
2451            // Since the session state is corrupt, we cannot just rollback the transaction
2452            // while preserving the non-transaction session state. The handle will remain
2453            // marked as corrupt until flushSession() is called to reset the connection
2454            // and deal with any remaining callbacks.
2455            $this->logger->info(
2456                "$fname: acknowledged client-side transaction loss on {db_server}",
2457                $this->getLogContext()
2458            );
2459
2460            return;
2461        }
2462
2463        $cs = $this->commenceCriticalSection( __METHOD__ );
2464        if ( $this->trxLevel() ) {
2465            // Disconnects cause rollback anyway, so ignore those errors
2466            $query = new Query(
2467                $this->platform->rollbackSqlText(),
2468                self::QUERY_SILENCE_ERRORS | self::QUERY_CHANGE_TRX,
2469                'ROLLBACK'
2470            );
2471            $this->query( $query, $fname );
2472        }
2473        $this->transactionManager->onRollbackInCriticalSection( $this );
2474        // With FLUSHING_ALL_PEERS, callbacks will run when requested by a dedicated phase
2475        // within LoadBalancer. With FLUSHING_INTERNAL, callbacks will run when requested by
2476        // the Database caller during a safe point. This avoids isolation and recursion issues.
2477        if ( $flush === self::FLUSHING_ONE ) {
2478            $this->runTransactionPostRollbackCallbacks();
2479        }
2480        $this->completeCriticalSection( __METHOD__, $cs );
2481    }
2482
2483    /**
2484     * @internal Only for tests and highly discouraged
2485     * @param TransactionManager $transactionManager
2486     */
2487    public function setTransactionManager( TransactionManager $transactionManager ) {
2488        $this->transactionManager = $transactionManager;
2489    }
2490
2491    /** @inheritDoc */
2492    public function flushSession( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
2493        if (
2494            $flush !== self::FLUSHING_INTERNAL &&
2495            $flush !== self::FLUSHING_ALL_PEERS &&
2496            $this->flagsHolder->hasImplicitTrxFlag()
2497        ) {
2498            throw new DBUnexpectedError(
2499                $this,
2500                "$fname: Expected mass flush of all peer connections (DBO_TRX set)"
2501            );
2502        }
2503
2504        if ( $this->csmError ) {
2505            // If a critical section error occurred, such as Excimer timeout exceptions raised
2506            // before a query response was marshalled, destroy the connection handle and reset
2507            // the session state tracking variables. The value of trxLevel() is irrelevant here,
2508            // and, in fact, might be 1 due to rollback() deferring critical section recovery.
2509            $this->logger->info(
2510                "$fname: acknowledged client-side session loss on {db_server}",
2511                $this->getLogContext()
2512            );
2513            $this->csmError = null;
2514            $this->csmFname = null;
2515            $this->replaceLostConnection( 2048, __METHOD__ );
2516
2517            return;
2518        }
2519
2520        if ( $this->trxLevel() ) {
2521            // Any existing transaction should have been rolled back already
2522            throw new DBUnexpectedError(
2523                $this,
2524                "$fname: transaction still in progress (not yet rolled back)"
2525            );
2526        }
2527
2528        if ( $this->transactionManager->sessionStatus() === TransactionManager::STATUS_SESS_ERROR ) {
2529            // If the session state was already lost due to either an unacknowledged session
2530            // state loss error (e.g. dropped connection) or an explicit connection close call,
2531            // then there is nothing to do here. Note that in such cases, even temporary tables
2532            // and server-side config variables are lost (invocation of this method is assumed
2533            // to imply that such losses are tolerable).
2534            $this->logger->info(
2535                "$fname: acknowledged server-side session loss on {db_server}",
2536                $this->getLogContext()
2537            );
2538        } elseif ( $this->isOpen() ) {
2539            // Connection handle exists; server-side session state must be flushed
2540            $this->doFlushSession( $fname );
2541            $this->sessionNamedLocks = [];
2542        }
2543
2544        $this->transactionManager->clearSessionError();
2545    }
2546
2547    /**
2548     * Reset the server-side session state for named locks and table locks
2549     *
2550     * Connection and query errors will be suppressed and logged
2551     *
2552     * @param string $fname
2553     * @since 1.38
2554     */
2555    protected function doFlushSession( $fname ) {
2556        // no-op
2557    }
2558
2559    /** @inheritDoc */
2560    public function flushSnapshot( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
2561        $this->transactionManager->onFlushSnapshot(
2562            $this,
2563            $fname,
2564            $flush,
2565            $this->getTransactionRoundFname()
2566        );
2567        if (
2568            $this->transactionManager->sessionStatus() === TransactionManager::STATUS_SESS_ERROR ||
2569            $this->transactionManager->trxStatus() === TransactionManager::STATUS_TRX_ERROR
2570        ) {
2571            $this->rollback( $fname, self::FLUSHING_INTERNAL );
2572        } else {
2573            $this->commit( $fname, self::FLUSHING_INTERNAL );
2574        }
2575    }
2576
2577    /** @inheritDoc */
2578    public function duplicateTableStructure(
2579        $oldName,
2580        $newName,
2581        $temporary = false,
2582        $fname = __METHOD__
2583    ) {
2584        throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
2585    }
2586
2587    /** @inheritDoc */
2588    public function listTables( $prefix = null, $fname = __METHOD__ ) {
2589        throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
2590    }
2591
2592    /** @inheritDoc */
2593    public function affectedRows() {
2594        $this->lastEmulatedAffectedRows ??= $this->lastQueryAffectedRows;
2595
2596        return $this->lastEmulatedAffectedRows;
2597    }
2598
2599    /** @inheritDoc */
2600    public function insertId() {
2601        if ( $this->lastEmulatedInsertId === null ) {
2602            // Guard against misuse of this method by checking affectedRows(). Note that calls
2603            // to insert() with "IGNORE" and calls to insertSelect() might not add any rows.
2604            if ( $this->affectedRows() ) {
2605                $this->lastEmulatedInsertId = $this->lastInsertId();
2606            } else {
2607                $this->lastEmulatedInsertId = 0;
2608            }
2609        }
2610
2611        return $this->lastEmulatedInsertId;
2612    }
2613
2614    /**
2615     * Get a row ID from the last insert statement to implicitly assign one within the session
2616     *
2617     * If the statement involved assigning sequence IDs to multiple rows, then the return value
2618     * will be any one of those values (database-specific). If the statement was an "UPSERT" and
2619     * some existing rows were updated, then the result will either reflect only IDs of created
2620     * rows or it will reflect IDs of both created and updated rows (this is database-specific).
2621     *
2622     * The result is unspecified if the statement gave an error.
2623     *
2624     * @return int Sequence ID, 0 (if none)
2625     * @throws DBError
2626     */
2627    abstract protected function lastInsertId();
2628
2629    /** @inheritDoc */
2630    public function ping() {
2631        if ( $this->isOpen() ) {
2632            // If the connection was recently used, assume that it is still good
2633            if ( ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
2634                return true;
2635            }
2636            // Send a trivial query to test the connection, triggering an automatic
2637            // reconnection attempt if the connection was lost
2638            $query = new Query(
2639                self::PING_QUERY,
2640                self::QUERY_IGNORE_DBO_TRX | self::QUERY_SILENCE_ERRORS | self::QUERY_CHANGE_NONE,
2641                'SELECT'
2642            );
2643            $res = $this->query( $query, __METHOD__ );
2644            $ok = ( $res !== false );
2645        } else {
2646            // Try to re-establish a connection
2647            $ok = $this->replaceLostConnection( null, __METHOD__ );
2648        }
2649
2650        return $ok;
2651    }
2652
2653    /**
2654     * Close any existing (dead) database connection and open a new connection
2655     *
2656     * @param int|null $lastErrno
2657     * @param string $fname
2658     * @return bool True if new connection is opened successfully, false if error
2659     */
2660    protected function replaceLostConnection( $lastErrno, $fname ) {
2661        if ( $this->conn ) {
2662            $this->closeConnection();
2663            $this->conn = null;
2664            $this->handleSessionLossPreconnect();
2665        }
2666
2667        try {
2668            $this->open(
2669                $this->connectionParams[self::CONN_SERVER],
2670                $this->connectionParams[self::CONN_USER],
2671                $this->connectionParams[self::CONN_PASSWORD],
2672                $this->currentDomain->getDatabase(),
2673                $this->currentDomain->getSchema(),
2674                $this->tablePrefix()
2675            );
2676            $this->lastPing = microtime( true );
2677            $ok = true;
2678
2679            $this->logger->warning(
2680                $fname . ': lost connection to {db_server} with error {errno}; reconnected',
2681                $this->getLogContext( [
2682                    'exception' => new RuntimeException(),
2683                    'db_log_category' => 'connection',
2684                    'errno' => $lastErrno
2685                ] )
2686            );
2687        } catch ( DBConnectionError $e ) {
2688            $ok = false;
2689
2690            $this->logger->error(
2691                $fname . ': lost connection to {db_server} with error {errno}; reconnection failed: {connect_msg}',
2692                $this->getLogContext( [
2693                    'exception' => new RuntimeException(),
2694                    'db_log_category' => 'connection',
2695                    'errno' => $lastErrno,
2696                    'connect_msg' => $e->getMessage()
2697                ] )
2698            );
2699        }
2700
2701        // Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
2702        // If callback suppression is set then the array will remain unhandled.
2703        $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
2704        // Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener().
2705        // If callback suppression is set then the array will remain unhandled.
2706        $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
2707
2708        return $ok;
2709    }
2710
2711    /**
2712     * Merge the result of getSessionLagStatus() for several DBs
2713     * using the most pessimistic values to estimate the lag of
2714     * any data derived from them in combination
2715     *
2716     * This is information is useful for caching modules
2717     *
2718     * @see WANObjectCache::set()
2719     * @see WANObjectCache::getWithSetCallback()
2720     *
2721     * @param IReadableDatabase|null ...$dbs
2722     * Note: For backward compatibility, it is allowed for null values
2723     * to be passed among the parameters. This is deprecated since 1.36,
2724     * only IReadableDatabase objects should be passed.
2725     *
2726     * @return array Map of values:
2727     *   - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
2728     *   - since: oldest UNIX timestamp of any of the DB lag estimates
2729     *   - pending: whether any of the DBs have uncommitted changes
2730     * @throws DBError
2731     * @since 1.27
2732     * @deprecated since 1.47, has no effect and should be removed
2733     */
2734    public static function getCacheSetOptions( ?IReadableDatabase ...$dbs ) {
2735        $res = [ 'pending' => false ];
2736
2737        foreach ( $dbs as $db ) {
2738            if ( $db instanceof IDatabaseForOwner ) {
2739                $res['pending'] = $res['pending'] ?: $db->writesPending();
2740            }
2741        }
2742
2743        return $res;
2744    }
2745
2746    /** @inheritDoc */
2747    public function encodeBlob( $b ) {
2748        return $b;
2749    }
2750
2751    /** @inheritDoc */
2752    public function decodeBlob( $b ) {
2753        if ( $b instanceof Blob ) {
2754            $b = $b->fetch();
2755        }
2756        return $b;
2757    }
2758
2759    public function setSessionOptions( array $options ) {
2760    }
2761
2762    /** @inheritDoc */
2763    public function sourceFile(
2764        $filename,
2765        ?callable $lineCallback = null,
2766        ?callable $resultCallback = null,
2767        $fname = false,
2768        ?callable $inputCallback = null
2769    ) {
2770        // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
2771        $fp = @fopen( $filename, 'r' );
2772
2773        if ( $fp === false ) {
2774            throw new RuntimeException( "Could not open \"{$filename}\"" );
2775        }
2776
2777        if ( !$fname ) {
2778            $fname = __METHOD__ . "$filename )";
2779        }
2780
2781        try {
2782            return $this->sourceStream(
2783                $fp,
2784                $lineCallback,
2785                $resultCallback,
2786                $fname,
2787                $inputCallback
2788            );
2789        } finally {
2790            fclose( $fp );
2791        }
2792    }
2793
2794    /** @inheritDoc */
2795    public function sourceStream(
2796        $fp,
2797        ?callable $lineCallback = null,
2798        ?callable $resultCallback = null,
2799        $fname = __METHOD__,
2800        ?callable $inputCallback = null
2801    ) {
2802        $delimiterReset = new ScopedCallback(
2803            function ( $delimiter ) {
2804                $this->delimiter = $delimiter;
2805            },
2806            [ $this->delimiter ]
2807        );
2808        $cmd = '';
2809
2810        while ( !feof( $fp ) ) {
2811            if ( $lineCallback ) {
2812                $lineCallback();
2813            }
2814
2815            $line = trim( fgets( $fp ) );
2816
2817            if ( $line == '' ) {
2818                continue;
2819            }
2820
2821            if ( $line[0] == '-' && $line[1] == '-' ) {
2822                continue;
2823            }
2824
2825            if ( $cmd != '' ) {
2826                $cmd .= ' ';
2827            }
2828
2829            $done = $this->streamStatementEnd( $cmd, $line );
2830
2831            $cmd .= "$line\n";
2832
2833            if ( $done || feof( $fp ) ) {
2834                $cmd = $this->platform->replaceVars( $cmd );
2835
2836                if ( $inputCallback ) {
2837                    $callbackResult = $inputCallback( $cmd );
2838
2839                    if ( is_string( $callbackResult ) || !$callbackResult ) {
2840                        $cmd = $callbackResult;
2841                    }
2842                }
2843
2844                if ( $cmd ) {
2845                    $res = $this->query( $cmd, $fname );
2846
2847                    if ( $resultCallback ) {
2848                        $resultCallback( $res, $this );
2849                    }
2850
2851                    if ( $res === false ) {
2852                        $err = $this->lastError();
2853
2854                        return "Query \"{$cmd}\" failed with error code \"$err\".\n";
2855                    }
2856                }
2857                $cmd = '';
2858            }
2859        }
2860
2861        ScopedCallback::consume( $delimiterReset );
2862        return true;
2863    }
2864
2865    /**
2866     * Called by sourceStream() to check if we've reached a statement end
2867     *
2868     * @param string &$sql SQL assembled so far
2869     * @param string &$newLine New line about to be added to $sql
2870     * @return bool Whether $newLine contains end of the statement
2871     */
2872    public function streamStatementEnd( &$sql, &$newLine ) {
2873        if ( $this->delimiter ) {
2874            $prev = $newLine;
2875            $newLine = preg_replace(
2876                '/' . preg_quote( $this->delimiter, '/' ) . '$/',
2877                '',
2878                $newLine
2879            );
2880            if ( $newLine != $prev ) {
2881                return true;
2882            }
2883        }
2884
2885        return false;
2886    }
2887
2888    /**
2889     * @inheritDoc
2890     */
2891    public function lock( $lockName, $method, $timeout = 5, $flags = 0 ) {
2892        $logContext = [
2893            'lockname' => $lockName,
2894            'db_log_category' => 'locking'
2895        ];
2896        $lockTsUnix = $this->doLock( $lockName, $method, $timeout );
2897        if ( $lockTsUnix !== null ) {
2898            $locked = true;
2899            $this->sessionNamedLocks[$lockName] = [
2900                'ts' => $lockTsUnix,
2901                'trxId' => $this->transactionManager->getTrxId()
2902            ];
2903            $this->logger->debug(
2904                __METHOD__ . ": acquired lock '{lockname}'",
2905                $logContext
2906            );
2907        } else {
2908            $locked = false;
2909            $this->logger->info(
2910                __METHOD__ . ": failed to acquire lock '{lockname}'",
2911                $logContext
2912            );
2913        }
2914
2915        return $this->flagsHolder::contains( $flags, self::LOCK_TIMESTAMP ) ? $lockTsUnix : $locked;
2916    }
2917
2918    /**
2919     * @see lock()
2920     *
2921     * @param string $lockName
2922     * @param string $method
2923     * @param int $timeout
2924     * @return float|null UNIX timestamp of lock acquisition; null on failure
2925     * @throws DBError
2926     */
2927    protected function doLock( string $lockName, string $method, int $timeout ) {
2928        return microtime( true ); // not implemented
2929    }
2930
2931    /**
2932     * @inheritDoc
2933     */
2934    public function unlock( $lockName, $method ) {
2935        $logContext = [
2936            'lockname' => $lockName,
2937            'db_log_category' => 'locking'
2938        ];
2939        if ( !isset( $this->sessionNamedLocks[$lockName] ) ) {
2940            $released = false;
2941            $this->logger->warning(
2942                __METHOD__ . ": trying to release unheld lock '{lockname}'\n",
2943                $logContext
2944            );
2945        } else {
2946            $released = $this->doUnlock( $lockName, $method );
2947            if ( $released ) {
2948                unset( $this->sessionNamedLocks[$lockName] );
2949                $this->logger->debug(
2950                    __METHOD__ . ": released lock '{lockname}'",
2951                    $logContext
2952                );
2953            } else {
2954                $this->logger->warning(
2955                    __METHOD__ . ": failed to release lock '{lockname}'\n",
2956                    $logContext
2957                );
2958            }
2959        }
2960
2961        return $released;
2962    }
2963
2964    /**
2965     * @see unlock()
2966     *
2967     * @param string $lockName
2968     * @param string $method
2969     * @return bool Success
2970     * @throws DBError
2971     */
2972    protected function doUnlock( string $lockName, string $method ) {
2973        return true; // not implemented
2974    }
2975
2976    /** @inheritDoc */
2977    #[\NoDiscard]
2978    public function getScopedLockAndFlush( $lockKey, $fname, $timeout ): ?ScopedCallback {
2979        $this->transactionManager->onGetScopedLockAndFlush( $this, $fname );
2980
2981        if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
2982            return null;
2983        }
2984
2985        $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
2986            // Note that the callback can be reached due to an exception making the calling
2987            // function end early. If the transaction/session is in an error state, avoid log
2988            // spam and confusing replacement of an original DBError with one about unlock().
2989            // Unlock query will fail anyway; avoid possibly triggering errors in rollback()
2990            if (
2991                $this->transactionManager->sessionStatus() === TransactionManager::STATUS_SESS_ERROR ||
2992                $this->transactionManager->trxStatus() === TransactionManager::STATUS_TRX_ERROR
2993            ) {
2994                return;
2995            }
2996            if ( $this->trxLevel() ) {
2997                $this->onTransactionResolution(
2998                    function () use ( $lockKey, $fname ) {
2999                        $this->unlock( $lockKey, $fname );
3000                    },
3001                    $fname
3002                );
3003            } else {
3004                $this->unlock( $lockKey, $fname );
3005            }
3006        } );
3007
3008        $this->commit( $fname, self::FLUSHING_INTERNAL );
3009
3010        return $unlocker;
3011    }
3012
3013    /** @inheritDoc */
3014    public function dropTable( $table, $fname = __METHOD__ ) {
3015        if ( !$this->tableExists( $table, $fname ) ) {
3016            return false;
3017        }
3018
3019        $query = new Query(
3020            $this->platform->dropTableSqlText( $table ),
3021            self::QUERY_CHANGE_SCHEMA,
3022            'DROP',
3023            $table
3024        );
3025        $this->query( $query, $fname );
3026
3027        return true;
3028    }
3029
3030    /** @inheritDoc */
3031    public function truncateTable( $table, $fname = __METHOD__ ) {
3032        $sql = "TRUNCATE TABLE " . $this->tableName( $table );
3033        $query = new Query( $sql, self::QUERY_CHANGE_SCHEMA, 'TRUNCATE', $table );
3034        $this->query( $query, $fname );
3035    }
3036
3037    /** @inheritDoc */
3038    public function isReadOnly() {
3039        return ( $this->getReadOnlyReason() !== null );
3040    }
3041
3042    /**
3043     * @return array|null Tuple of (reason string, "role" or "lb") if read-only; null otherwise
3044     */
3045    protected function getReadOnlyReason() {
3046        $reason = $this->replicationReporter->getTopologyBasedReadOnlyReason();
3047        if ( $reason ) {
3048            return $reason;
3049        }
3050
3051        $reason = $this->getLBInfo( self::LB_READ_ONLY_REASON );
3052        if ( is_string( $reason ) ) {
3053            return [ $reason, 'lb' ];
3054        }
3055
3056        return null;
3057    }
3058
3059    /**
3060     * Get the underlying binding connection handle
3061     *
3062     * Makes sure the connection resource is set (disconnects and ping() failure can unset it).
3063     * This catches broken callers than catch and ignore disconnection exceptions.
3064     * Unlike checking isOpen(), this is safe to call inside of open().
3065     *
3066     * @return mixed
3067     * @throws DBUnexpectedError
3068     * @since 1.26
3069     */
3070    protected function getBindingHandle() {
3071        if ( !$this->conn ) {
3072            throw new DBUnexpectedError(
3073                $this,
3074                'DB connection was already closed or the connection dropped'
3075            );
3076        }
3077
3078        return $this->conn;
3079    }
3080
3081    /**
3082     * Demark the start of a critical section of session/transaction state changes
3083     *
3084     * Use this to disable potentially DB handles due to corruption from highly unexpected
3085     * exceptions (e.g. from zend timers or coding errors) preempting execution of methods.
3086     *
3087     * Callers must demark completion of the critical section with completeCriticalSection().
3088     * Callers should handle DBError exceptions that do not cause object state corruption by
3089     * catching them, calling completeCriticalSection(), and then rethrowing them.
3090     *
3091     * @code
3092     *     $cs = $this->commenceCriticalSection( __METHOD__ );
3093     *     try {
3094     *         //...send a query that changes the session/transaction state...
3095     *     } catch ( DBError $e ) {
3096     *         $this->completeCriticalSection( __METHOD__, $cs );
3097     *         throw $expectedException;
3098     *     }
3099     *     try {
3100     *         //...send another query that changes the session/transaction state...
3101     *     } catch ( DBError $trxError ) {
3102     *         // Require ROLLBACK before allowing any other queries from outside callers
3103     *         $this->completeCriticalSection( __METHOD__, $cs, $trxError );
3104     *         throw $expectedException;
3105     *     }
3106     *     // ...update session state fields of $this...
3107     *     $this->completeCriticalSection( __METHOD__, $cs );
3108     * @endcode
3109     *
3110     * @see Database::completeCriticalSection()
3111     *
3112     * @since 1.36
3113     * @param string $fname Caller name
3114     * @return CriticalSectionScope|null RAII-style monitor (topmost sections only)
3115     * @throws DBUnexpectedError If an unresolved critical section error already exists
3116     */
3117    protected function commenceCriticalSection( string $fname ) {
3118        if ( $this->csmError ) {
3119            throw new DBUnexpectedError(
3120                $this,
3121                "Cannot execute $fname critical section while session state is out of sync.\n\n" .
3122                $this->csmError->getMessage() . "\n" .
3123                $this->csmError->getTraceAsString()
3124            );
3125        }
3126
3127        if ( $this->csmId ) {
3128            $csm = null; // fold into the outer critical section
3129        } elseif ( $this->csProvider ) {
3130            $csm = $this->csProvider->scopedEnter(
3131                $fname,
3132                null, // emergency limit (default)
3133                null, // emergency callback (default)
3134                function () use ( $fname ) {
3135                    // Mark a critical section as having been aborted by an error
3136                    $e = new RuntimeException( "A critical section from {$fname} has failed" );
3137                    $this->csmError = $e;
3138                    $this->csmId = null;
3139                }
3140            );
3141            $this->csmId = $csm->getId();
3142            $this->csmFname = $fname;
3143        } else {
3144            $csm = null; // not supported
3145        }
3146
3147        return $csm;
3148    }
3149
3150    /**
3151     * Demark the completion of a critical section of session/transaction state changes
3152     *
3153     * @see Database::commenceCriticalSection()
3154     *
3155     * @since 1.36
3156     * @param string $fname Caller name
3157     * @param CriticalSectionScope|null $csm RAII-style monitor (topmost sections only)
3158     * @param Throwable|null $trxError Error that requires setting STATUS_TRX_ERROR (if any)
3159     */
3160    protected function completeCriticalSection(
3161        string $fname,
3162        ?CriticalSectionScope $csm,
3163        ?Throwable $trxError = null
3164    ) {
3165        if ( $csm !== null ) {
3166            if ( $this->csmId === null ) {
3167                throw new LogicException( "$fname critical section is not active" );
3168            } elseif ( $csm->getId() !== $this->csmId ) {
3169                throw new LogicException(
3170                    "$fname critical section is not the active ({$this->csmFname}) one"
3171                );
3172            }
3173
3174            $csm->exit();
3175            $this->csmId = null;
3176        }
3177
3178        if ( $trxError ) {
3179            $this->transactionManager->setTransactionError( $trxError );
3180        }
3181    }
3182
3183    public function __toString() {
3184        $id = spl_object_id( $this );
3185
3186        $description = $this->getType() . ' object #' . $id;
3187        // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.is_resource
3188        if ( is_resource( $this->conn ) ) {
3189            $description .= ' (' . (string)$this->conn . ')'; // "resource id #<ID>"
3190        } elseif ( is_object( $this->conn ) ) {
3191            $handleId = spl_object_id( $this->conn );
3192            $description .= " (handle id #$handleId)";
3193        }
3194
3195        return $description;
3196    }
3197
3198    /**
3199     * Make sure that copies do not share the same client binding handle
3200     * @throws DBConnectionError
3201     */
3202    public function __clone() {
3203        $this->logger->warning(
3204            "Cloning " . static::class . " is not recommended; forking connection",
3205            [
3206                'exception' => new RuntimeException(),
3207                'db_log_category' => 'connection'
3208            ]
3209        );
3210
3211        if ( $this->isOpen() ) {
3212            // Open a new connection resource without messing with the old one
3213            $this->conn = null;
3214            $this->transactionManager->clearEndCallbacks();
3215            $this->handleSessionLossPreconnect(); // no trx or locks anymore
3216            $this->open(
3217                $this->connectionParams[self::CONN_SERVER],
3218                $this->connectionParams[self::CONN_USER],
3219                $this->connectionParams[self::CONN_PASSWORD],
3220                $this->currentDomain->getDatabase(),
3221                $this->currentDomain->getSchema(),
3222                $this->tablePrefix()
3223            );
3224            $this->lastPing = microtime( true );
3225        }
3226    }
3227
3228    /**
3229     * Called by serialize. Throw an exception when DB connection is serialized.
3230     * This causes problems on some database engines because the connection is
3231     * not restored on unserialize.
3232     * @return never
3233     */
3234    public function __sleep(): never {
3235        throw new RuntimeException( 'Database serialization may cause problems, since ' .
3236            'the connection is not restored on wakeup' );
3237    }
3238
3239    /**
3240     * Run a few simple checks and close dangling connections
3241     */
3242    public function __destruct() {
3243        if ( $this->transactionManager ) {
3244            // Tests mock this class and disable constructor.
3245            $this->transactionManager->onDestruct();
3246        }
3247
3248        $danglingWriters = $this->pendingWriteAndCallbackCallers();
3249        if ( $danglingWriters ) {
3250            $fnames = implode( ', ', $danglingWriters );
3251            trigger_error( "DB transaction writes or callbacks still pending ($fnames)" );
3252        }
3253
3254        if ( $this->conn ) {
3255            // Avoid connection leaks. Normally, resources close at script completion.
3256            // The connection might already be closed in PHP by now, so suppress warnings.
3257            // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3258            @$this->closeConnection();
3259            $this->conn = null;
3260        }
3261    }
3262
3263    /* Start of methods delegated to DatabaseFlags. Avoid using them outside of rdbms library */
3264
3265    /** @inheritDoc */
3266    public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
3267        $this->flagsHolder->setFlag( $flag, $remember );
3268    }
3269
3270    /** @inheritDoc */
3271    public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
3272        $this->flagsHolder->clearFlag( $flag, $remember );
3273    }
3274
3275    /** @inheritDoc */
3276    public function restoreFlags( $state = self::RESTORE_PRIOR ) {
3277        $this->flagsHolder->restoreFlags( $state );
3278    }
3279
3280    /** @inheritDoc */
3281    public function getFlag( $flag ) {
3282        return $this->flagsHolder->getFlag( $flag );
3283    }
3284
3285    /* End of methods delegated to DatabaseFlags. */
3286
3287    /* Start of methods delegated to TransactionManager. Avoid using them outside of rdbms library */
3288
3289    /** @inheritDoc */
3290    final public function trxLevel() {
3291        // FIXME: A lot of tests disable constructor leading to trx manager being
3292        // null and breaking, this is unacceptable but hopefully this should
3293        // happen less by moving these functions to the transaction manager class.
3294        if ( !$this->transactionManager ) {
3295            $this->transactionManager = new TransactionManager( new NullLogger() );
3296        }
3297        return $this->transactionManager->trxLevel();
3298    }
3299
3300    /** @inheritDoc */
3301    public function trxTimestamp() {
3302        return $this->transactionManager->trxTimestamp();
3303    }
3304
3305    /** @return int */
3306    public function trxStatus() {
3307        return $this->transactionManager->trxStatus();
3308    }
3309
3310    /** @inheritDoc */
3311    public function writesPending() {
3312        return $this->transactionManager->writesPending();
3313    }
3314
3315    /** @inheritDoc */
3316    public function writesOrCallbacksPending() {
3317        return $this->transactionManager->writesOrCallbacksPending();
3318    }
3319
3320    /** @inheritDoc */
3321    public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
3322        return $this->transactionManager->pendingWriteQueryDuration( $type );
3323    }
3324
3325    /** @inheritDoc */
3326    public function pendingWriteCallers() {
3327        if ( !$this->transactionManager ) {
3328            return [];
3329        }
3330        return $this->transactionManager->pendingWriteCallers();
3331    }
3332
3333    /** @return string[] */
3334    public function pendingWriteAndCallbackCallers() {
3335        if ( !$this->transactionManager ) {
3336            return [];
3337        }
3338        return $this->transactionManager->pendingWriteAndCallbackCallers();
3339    }
3340
3341    /**
3342     * @return int Number of callbacks attempted
3343     * @throws Throwable Any exception thrown by a callback
3344     */
3345    public function runOnTransactionPreCommitCallbacks() {
3346        return $this->transactionManager->runOnTransactionPreCommitCallbacks();
3347    }
3348
3349    /** @inheritDoc */
3350    public function explicitTrxActive() {
3351        return $this->transactionManager->explicitTrxActive();
3352    }
3353
3354    /* End of methods delegated to TransactionManager. */
3355
3356    /* Start of methods delegated to SQLPlatform. Avoid using them outside of rdbms library */
3357
3358    /** @inheritDoc */
3359    public function implicitOrderby() {
3360        return $this->platform->implicitOrderby();
3361    }
3362
3363    /** @inheritDoc */
3364    public function selectSQLText(
3365        $tables, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
3366    ) {
3367        return $this->platform->selectSQLText( $tables, $vars, $conds, $fname, $options, $join_conds );
3368    }
3369
3370    /** @inheritDoc */
3371    public function buildComparison( string $op, array $conds ): string {
3372        return $this->platform->buildComparison( $op, $conds );
3373    }
3374
3375    /** @inheritDoc */
3376    public function makeList( array $a, $mode = self::LIST_COMMA ) {
3377        return $this->platform->makeList( $a, $mode );
3378    }
3379
3380    /** @inheritDoc */
3381    public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
3382        return $this->platform->makeWhereFrom2d( $data, $baseKey, $subKey );
3383    }
3384
3385    /** @inheritDoc */
3386    public function factorConds( $condsArray ) {
3387        return $this->platform->factorConds( $condsArray );
3388    }
3389
3390    /** @inheritDoc */
3391    public function bitNot( $field ) {
3392        return $this->platform->bitNot( $field );
3393    }
3394
3395    /** @inheritDoc */
3396    public function bitAnd( $fieldLeft, $fieldRight ) {
3397        return $this->platform->bitAnd( $fieldLeft, $fieldRight );
3398    }
3399
3400    /** @inheritDoc */
3401    public function bitOr( $fieldLeft, $fieldRight ) {
3402        return $this->platform->bitOr( $fieldLeft, $fieldRight );
3403    }
3404
3405    /** @inheritDoc */
3406    public function buildConcat( $stringList ) {
3407        return $this->platform->buildConcat( $stringList );
3408    }
3409
3410    /** @inheritDoc */
3411    public function buildGroupConcat( $field, $delim ): string {
3412        return $this->platform->buildGroupConcat( $field, $delim );
3413    }
3414
3415    /** @inheritDoc */
3416    public function buildGreatest( $fields, $values ) {
3417        return $this->platform->buildGreatest( $fields, $values );
3418    }
3419
3420    /** @inheritDoc */
3421    public function buildLeast( $fields, $values ) {
3422        return $this->platform->buildLeast( $fields, $values );
3423    }
3424
3425    /** @inheritDoc */
3426    public function buildSubstring( $input, $startPosition, $length = null ) {
3427        return $this->platform->buildSubstring( $input, $startPosition, $length );
3428    }
3429
3430    /** @inheritDoc */
3431    public function buildStringCast( $field ) {
3432        return $this->platform->buildStringCast( $field );
3433    }
3434
3435    /** @inheritDoc */
3436    public function buildIntegerCast( $field ) {
3437        return $this->platform->buildIntegerCast( $field );
3438    }
3439
3440    /** @inheritDoc */
3441    public function tableName( string $name, $format = 'quoted' ) {
3442        return $this->platform->tableName( $name, $format );
3443    }
3444
3445    /** @inheritDoc */
3446    public function tableNamesN( ...$tables ) {
3447        return $this->platform->tableNamesN( ...$tables );
3448    }
3449
3450    /** @inheritDoc */
3451    public function addIdentifierQuotes( $s ) {
3452        return $this->platform->addIdentifierQuotes( $s );
3453    }
3454
3455    /**
3456     * @param string $name
3457     * @return bool
3458     */
3459    public function isQuotedIdentifier( $name ) {
3460        return $this->platform->isQuotedIdentifier( $name );
3461    }
3462
3463    /** @inheritDoc */
3464    public function buildLike( $param, ...$params ) {
3465        return $this->platform->buildLike( $param, ...$params );
3466    }
3467
3468    /** @inheritDoc */
3469    public function anyChar() {
3470        return $this->platform->anyChar();
3471    }
3472
3473    /** @inheritDoc */
3474    public function anyString() {
3475        return $this->platform->anyString();
3476    }
3477
3478    /** @inheritDoc */
3479    public function limitResult( $sql, $limit, $offset = false ) {
3480        return $this->platform->limitResult( $sql, $limit, $offset );
3481    }
3482
3483    /** @inheritDoc */
3484    public function unionSupportsOrderAndLimit() {
3485        return $this->platform->unionSupportsOrderAndLimit();
3486    }
3487
3488    /** @inheritDoc */
3489    public function unionQueries( $sqls, $all, $options = [] ) {
3490        return $this->platform->unionQueries( $sqls, $all, $options );
3491    }
3492
3493    /** @inheritDoc */
3494    public function conditional( $cond, $caseTrueExpression, $caseFalseExpression ) {
3495        return $this->platform->conditional( $cond, $caseTrueExpression, $caseFalseExpression );
3496    }
3497
3498    /** @inheritDoc */
3499    public function strreplace( $orig, $old, $new ) {
3500        return $this->platform->strreplace( $orig, $old, $new );
3501    }
3502
3503    /** @inheritDoc */
3504    public function timestamp( $ts = 0 ) {
3505        return $this->platform->timestamp( $ts );
3506    }
3507
3508    /** @inheritDoc */
3509    public function timestampOrNull( $ts = null ) {
3510        return $this->platform->timestampOrNull( $ts );
3511    }
3512
3513    /** @inheritDoc */
3514    public function getInfinity() {
3515        return $this->platform->getInfinity();
3516    }
3517
3518    /** @inheritDoc */
3519    public function encodeExpiry( $expiry ) {
3520        return $this->platform->encodeExpiry( $expiry );
3521    }
3522
3523    /** @inheritDoc */
3524    public function decodeExpiry( $expiry, $format = TS::MW ) {
3525        return $this->platform->decodeExpiry( $expiry, $format );
3526    }
3527
3528    /** @inheritDoc */
3529    public function setTableAliases( array $aliases ) {
3530        $this->platform->setTableAliases( $aliases );
3531    }
3532
3533    /** @inheritDoc */
3534    public function getTableAliases() {
3535        return $this->platform->getTableAliases();
3536    }
3537
3538    /** @inheritDoc */
3539    public function buildGroupConcatField(
3540        $delim, $tables, $field, $conds = '', $join_conds = []
3541    ) {
3542        return $this->platform->buildGroupConcatField( $delim, $tables, $field, $conds, $join_conds );
3543    }
3544
3545    /** @inheritDoc */
3546    public function buildSelectSubquery(
3547        $tables, $vars, $conds = '', $fname = __METHOD__,
3548        $options = [], $join_conds = []
3549    ) {
3550        return $this->platform->buildSelectSubquery( $tables, $vars, $conds, $fname, $options, $join_conds );
3551    }
3552
3553    /** @inheritDoc */
3554    public function buildExcludedValue( $column ) {
3555        return $this->platform->buildExcludedValue( $column );
3556    }
3557
3558    /** @inheritDoc */
3559    public function setSchemaVars( $vars ) {
3560        $this->platform->setSchemaVars( $vars );
3561    }
3562
3563    /* End of methods delegated to SQLPlatform. */
3564
3565    /* Start of methods delegated to ReplicationReporter. */
3566
3567    /** @inheritDoc */
3568    public function primaryPosWait( DBPrimaryPos $pos, $timeout ) {
3569        return $this->replicationReporter->primaryPosWait( $this, $pos, $timeout );
3570    }
3571
3572    /** @inheritDoc */
3573    public function getPrimaryPos() {
3574        return $this->replicationReporter->getPrimaryPos( $this );
3575    }
3576
3577    /** @inheritDoc */
3578    public function getLag() {
3579        return $this->replicationReporter->getLag( $this );
3580    }
3581
3582    /** @inheritDoc */
3583    public function getSessionLagStatus() {
3584        return $this->replicationReporter->getSessionLagStatus( $this );
3585    }
3586
3587    /* End of methods delegated to ReplicationReporter. */
3588}