Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
69.52% |
935 / 1345 |
|
38.74% |
74 / 191 |
CRAP | |
0.00% |
0 / 1 |
Database | |
69.52% |
935 / 1345 |
|
38.74% |
74 / 191 |
6699.61 | |
0.00% |
0 / 1 |
__construct | |
95.65% |
44 / 46 |
|
0.00% |
0 / 1 |
14 | |||
initConnection | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
2.00 | |||
open | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getAttributes | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setLogger | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getServerInfo | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
tablePrefix | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
dbSchema | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
5 | |||
getLBInfo | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
setLBInfo | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
lastDoneWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
sessionLocksPending | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTransactionRoundId | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
isOpen | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDomainID | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
strencode | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
installErrorHandler | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
restoreErrorHandler | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getLastPHPError | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
connectionErrorLogger | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLogContext | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
close | |
84.21% |
16 / 19 |
|
0.00% |
0 / 1 |
6.14 | |||
assertHasConnectionHandle | |
50.00% |
1 / 2 |
|
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% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
registerTempTables | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
4 | |||
query | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
5 | |||
executeQuery | |
96.55% |
28 / 29 |
|
0.00% |
0 / 1 |
10 | |||
attemptQuery | |
84.38% |
54 / 64 |
|
0.00% |
0 / 1 |
11.46 | |||
handleErroredQuery | |
50.00% |
16 / 32 |
|
0.00% |
0 / 1 |
22.50 | |||
makeCommentedSql | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
beginIfImplied | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
assertQueryIsCurrentlyAllowed | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
5.00 | |||
assessConnectionLoss | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
380 | |||
handleSessionLossPreconnect | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
doHandleSessionLossPreconnect | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isQueryTimeoutError | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
reportQueryError | |
33.33% |
2 / 6 |
|
0.00% |
0 / 1 |
3.19 | |||
getQueryExceptionAndLog | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
1 | |||
getQueryException | |
60.00% |
3 / 5 |
|
0.00% |
0 / 1 |
3.58 | |||
newExceptionAfterConnectError | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
newSelectQueryBuilder | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newUnionQueryBuilder | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
newUpdateQueryBuilder | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
newDeleteQueryBuilder | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
newInsertQueryBuilder | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
newReplaceQueryBuilder | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
selectField | |
69.23% |
9 / 13 |
|
0.00% |
0 / 1 |
7.05 | |||
selectFieldValues | |
75.00% |
9 / 12 |
|
0.00% |
0 / 1 |
5.39 | |||
select | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
selectRow | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
estimateRowCount | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
selectRowCount | |
92.59% |
25 / 27 |
|
0.00% |
0 / 1 |
7.02 | |||
lockForUpdate | |
50.00% |
4 / 8 |
|
0.00% |
0 / 1 |
4.12 | |||
fieldExists | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
tableExists | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
indexExists | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
indexUnique | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
indexInfo | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
insert | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
3.21 | |||
checkInsertWarnings | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
update | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
databasesAreIndependent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
selectDomain | |
50.00% |
3 / 6 |
|
0.00% |
0 / 1 |
2.50 | |||
doSelectDomain | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getDBname | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getServer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getServerName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addQuotes | |
72.73% |
8 / 11 |
|
0.00% |
0 / 1 |
6.73 | |||
expr | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
andExpr | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
orExpr | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
replace | |
81.82% |
18 / 22 |
|
0.00% |
0 / 1 |
5.15 | |||
upsert | |
93.94% |
62 / 66 |
|
0.00% |
0 / 1 |
10.02 | |||
getInsertIdColumnForUpsert | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getValueTypesForWithClause | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
deleteJoin | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
delete | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
insertSelect | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
3 | |||
isInsertSelectSafe | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
doInsertSelectGeneric | |
90.00% |
27 / 30 |
|
0.00% |
0 / 1 |
7.05 | |||
doInsertSelectNative | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
1 | |||
isConnectionError | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isKnownStatementRollbackError | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
serverIsReadOnly | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
onTransactionResolution | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
onTransactionCommitOrIdle | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
5.05 | |||
onTransactionPreCommitOrIdle | |
71.43% |
10 / 14 |
|
0.00% |
0 / 1 |
6.84 | |||
onAtomicSectionCancel | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setTransactionListener | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setTrxEndCallbackSuppression | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
runOnTransactionIdleCallbacks | |
73.91% |
17 / 23 |
|
0.00% |
0 / 1 |
7.87 | |||
runTransactionListenerCallbacks | |
42.86% |
3 / 7 |
|
0.00% |
0 / 1 |
6.99 | |||
runTransactionPostCommitCallbacks | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
2.02 | |||
runTransactionPostRollbackCallbacks | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
startAtomic | |
80.77% |
21 / 26 |
|
0.00% |
0 / 1 |
7.35 | |||
endAtomic | |
83.33% |
15 / 18 |
|
0.00% |
0 / 1 |
6.17 | |||
cancelAtomic | |
97.73% |
43 / 44 |
|
0.00% |
0 / 1 |
6 | |||
doAtomicSection | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
begin | |
65.00% |
13 / 20 |
|
0.00% |
0 / 1 |
7.54 | |||
doBegin | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
commit | |
95.00% |
19 / 20 |
|
0.00% |
0 / 1 |
7 | |||
rollback | |
88.57% |
31 / 35 |
|
0.00% |
0 / 1 |
9.12 | |||
setTransactionManager | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
flushSession | |
60.00% |
18 / 30 |
|
0.00% |
0 / 1 |
12.10 | |||
doFlushSession | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
flushSnapshot | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
duplicateTableStructure | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
listTables | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
affectedRows | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
insertId | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
lastInsertId | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
ping | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
replaceLostConnection | |
69.44% |
25 / 36 |
|
0.00% |
0 / 1 |
3.26 | |||
getCacheSetOptions | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
56 | |||
encodeBlob | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
decodeBlob | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
setSessionOptions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
sourceFile | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 | |||
sourceStream | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
240 | |||
streamStatementEnd | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
lockIsFree | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
doLockIsFree | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
lock | |
50.00% |
8 / 16 |
|
0.00% |
0 / 1 |
4.12 | |||
doLock | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
unlock | |
35.71% |
5 / 14 |
|
0.00% |
0 / 1 |
5.39 | |||
doUnlock | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getScopedLockAndFlush | |
88.89% |
16 / 18 |
|
0.00% |
0 / 1 |
5.03 | |||
dropTable | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
truncateTable | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
isReadOnly | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getReadOnlyReason | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
getBindingHandle | |
33.33% |
2 / 6 |
|
0.00% |
0 / 1 |
3.19 | |||
commenceCriticalSection | |
75.00% |
18 / 24 |
|
0.00% |
0 / 1 |
4.25 | |||
completeCriticalSection | |
63.64% |
7 / 11 |
|
0.00% |
0 / 1 |
6.20 | |||
__toString | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
3.02 | |||
__clone | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
6 | |||
__sleep | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
__destruct | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
4.10 | |||
setFlag | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
clearFlag | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
restoreFlags | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFlag | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
trxLevel | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
trxTimestamp | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
trxStatus | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
writesPending | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
writesOrCallbacksPending | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
pendingWriteQueryDuration | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
pendingWriteCallers | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
pendingWriteAndCallbackCallers | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
runOnTransactionPreCommitCallbacks | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
explicitTrxActive | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
implicitOrderby | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
selectSQLText | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
buildComparison | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
makeList | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
makeWhereFrom2d | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
factorConds | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
bitNot | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
bitAnd | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
bitOr | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
buildConcat | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
buildGreatest | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
buildLeast | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
buildSubstring | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
buildStringCast | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
buildIntegerCast | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
tableName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
tableNames | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
tableNamesN | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addIdentifierQuotes | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isQuotedIdentifier | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
buildLike | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
anyChar | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
anyString | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
limitResult | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
unionSupportsOrderAndLimit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
unionQueries | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
conditional | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
strreplace | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
timestamp | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
timestampOrNull | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getInfinity | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
encodeExpiry | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
decodeExpiry | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setTableAliases | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTableAliases | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setIndexAliases | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
buildGroupConcatField | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
buildSelectSubquery | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
buildExcludedValue | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setSchemaVars | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
primaryPosWait | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPrimaryPos | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLag | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSessionLagStatus | |
0.00% |
0 / 1 |
|
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 | */ |
20 | namespace Wikimedia\Rdbms; |
21 | |
22 | use InvalidArgumentException; |
23 | use LogicException; |
24 | use Psr\Log\LoggerAwareInterface; |
25 | use Psr\Log\LoggerInterface; |
26 | use Psr\Log\NullLogger; |
27 | use RuntimeException; |
28 | use Stringable; |
29 | use Throwable; |
30 | use Wikimedia\AtEase\AtEase; |
31 | use Wikimedia\Rdbms\Database\DatabaseFlags; |
32 | use Wikimedia\Rdbms\Platform\SQLPlatform; |
33 | use Wikimedia\Rdbms\Replication\ReplicationReporter; |
34 | use Wikimedia\RequestTimeout\CriticalSectionProvider; |
35 | use Wikimedia\RequestTimeout\CriticalSectionScope; |
36 | use Wikimedia\ScopedCallback; |
37 | |
38 | /** |
39 | * Relational database abstraction object |
40 | * |
41 | * @ingroup Database |
42 | * @since 1.28 |
43 | */ |
44 | abstract 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 | } |