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