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