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