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