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