Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
16.48% covered (danger)
16.48%
43 / 261
CRAP
39.94% covered (danger)
39.94%
796 / 1993
Database
0.00% covered (danger)
0.00%
0 / 1
16.48% covered (danger)
16.48%
43 / 261
165609.90
39.94% covered (danger)
39.94%
796 / 1993
 __construct
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 30
 initConnection
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 doInitConnection
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 8
 open
n/a
0 / 0
1
n/a
0 / 0
 factory
0.00% covered (danger)
0.00%
0 / 1
5.06
86.96% covered (warning)
86.96%
20 / 23
 attributesFromType
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 getClass
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 21
 getAttributes
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 setLogger
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getServerInfo
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getTopologyRole
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getTopologyRootMaster
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 trxLevel
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 trxTimestamp
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 trxStatus
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 tablePrefix
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
7 / 7
 dbSchema
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
9 / 9
 relationSchemaQualifier
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getLBInfo
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
5 / 5
 setLBInfo
0.00% covered (danger)
0.00%
0 / 1
4.03
87.50% covered (warning)
87.50%
7 / 8
 getLazyMasterHandle
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 implicitOrderby
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 lastQuery
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 lastDoneWrites
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 writesPending
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 writesOrCallbacksPending
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 6
 preCommitCallbacksPending
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 getTransactionRoundId
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 4
 pendingWriteQueryDuration
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 7
 pingAndCalculateLastTrxApplyTime
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 6
 pendingWriteCallers
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 pendingWriteRowsAffected
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 pendingWriteAndCallbackCallers
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 8
 flatAtomicSectionList
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 2
 isOpen
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 setFlag
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
8 / 8
 clearFlag
0.00% covered (danger)
0.00%
0 / 1
4.12
50.00% covered (danger)
50.00%
4 / 8
 restoreFlags
0.00% covered (danger)
0.00%
0 / 1
3.03
85.71% covered (warning)
85.71%
6 / 7
 getFlag
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getDomainID
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 indexInfo
n/a
0 / 0
1
n/a
0 / 0
 strencode
n/a
0 / 0
1
n/a
0 / 0
 installErrorHandler
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 4
 restoreErrorHandler
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 getLastPHPError
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 5
 connectionErrorLogger
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getLogContext
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 5
 close
0.00% covered (danger)
0.00%
0 / 1
13.96
82.14% covered (warning)
82.14%
23 / 28
 assertHasConnectionHandle
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 assertIsWritableMaster
0.00% covered (danger)
0.00%
0 / 1
3.03
85.71% covered (warning)
85.71%
6 / 7
 closeConnection
n/a
0 / 0
1
n/a
0 / 0
 doQuery
n/a
0 / 0
1
n/a
0 / 0
 isWriteQuery
0.00% covered (danger)
0.00%
0 / 1
4.25
75.00% covered (warning)
75.00%
6 / 8
 getQueryVerb
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 isTransactableQuery
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 4
 getTempTableWrites
0.00% covered (danger)
0.00%
0 / 1
11.11
70.37% covered (warning)
70.37%
19 / 27
 registerTempWrites
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 17
 isPristineTemporaryTable
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 query
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
7 / 7
 executeQuery
0.00% covered (danger)
0.00%
0 / 1
21.96
65.62% covered (warning)
65.62%
21 / 32
 executeQueryAttempt
0.00% covered (danger)
0.00%
0 / 1
210
0.00% covered (danger)
0.00%
0 / 51
 beginIfImplied
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 6
 updateTrxWriteQueryTime
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 14
 assertQueryIsCurrentlyAllowed
0.00% covered (danger)
0.00%
0 / 1
8.12
61.11% covered (warning)
61.11%
11 / 18
 assertNoOpenTransactions
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 5
 canRecoverFromDisconnect
0.00% covered (danger)
0.00%
0 / 1
56
0.00% covered (danger)
0.00%
0 / 13
 handleSessionLossPreconnect
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 16
 doHandleSessionLossPreconnect
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 handleSessionLossPostconnect
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 5
 consumeTrxShortId
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 wasQueryTimeout
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 reportQueryError
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 getQueryExceptionAndLog
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 10
 getQueryException
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 5
 newExceptionAfterConnectError
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 7
 freeResult
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 newSelectQueryBuilder
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 selectField
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 11
 selectFieldValues
0.00% covered (danger)
0.00%
0 / 1
5.39
75.00% covered (warning)
75.00%
9 / 12
 makeSelectOptions
0.00% covered (danger)
0.00%
0 / 1
18.95
77.42% covered (warning)
77.42%
24 / 31
 makeGroupByWithHaving
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
12 / 12
 makeOrderBy
0.00% covered (danger)
0.00%
0 / 1
3.04
83.33% covered (warning)
83.33%
5 / 6
 select
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 selectSQLText
0.00% covered (danger)
0.00%
0 / 1
21.70
80.43% covered (warning)
80.43%
37 / 46
 selectRow
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 8
 estimateRowCount
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 8
 selectRowCount
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 17
 selectOptionsIncludeLocking
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
5 / 5
 selectFieldsOrOptionsAggregate
0.00% covered (danger)
0.00%
0 / 1
12.00
66.67% covered (warning)
66.67%
8 / 12
 normalizeRowArray
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 11
 normalizeConditions
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 8
 normalizeUpsertKeys
0.00% covered (danger)
0.00%
0 / 1
132
0.00% covered (danger)
0.00%
0 / 24
 normalizeOptions
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 5
 isFlagInOptions
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 4
 extractSingleFieldFromList
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 8
 lockForUpdate
0.00% covered (danger)
0.00%
0 / 1
3.71
57.14% covered (warning)
57.14%
4 / 7
 fieldExists
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 indexExists
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 6
 tableExists
n/a
0 / 0
1
n/a
0 / 0
 indexUnique
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 insert
0.00% covered (danger)
0.00%
0 / 1
3.02
87.50% covered (warning)
87.50%
7 / 8
 doInsert
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 5
 doInsertNonConflicting
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 6
 makeInsertNonConflictingVerbAndOptions
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 makeInsertLists
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 15
 makeUpdateOptionsArray
0.00% covered (danger)
0.00%
0 / 1
2.03
80.00% covered (warning)
80.00%
4 / 5
 makeUpdateOptions
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 update
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
10 / 10
 makeList
0.00% covered (danger)
0.00%
0 / 1
28
95.24% covered (success)
95.24%
40 / 42
 makeWhereFrom2d
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 9
 aggregateValue
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 bitNot
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 bitAnd
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 bitOr
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 buildConcat
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 buildGroupConcatField
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 buildGreatest
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 buildLeast
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 buildSuperlative
0.00% covered (danger)
0.00%
0 / 1
110
0.00% covered (danger)
0.00%
0 / 16
 buildSubstring
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
 assertBuildSubstringParams
0.00% covered (danger)
0.00%
0 / 1
6.10
85.71% covered (warning)
85.71%
6 / 7
 assertConditionIsNotEmpty
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
6 / 6
 buildStringCast
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 buildIntegerCast
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 buildSelectSubquery
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 databasesAreIndependent
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 selectDB
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 5
 selectDomain
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 doSelectDomain
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getDBname
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getServer
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 tableName
0.00% covered (danger)
0.00%
0 / 1
10.14
60.00% covered (warning)
60.00%
12 / 20
 qualifiedTableComponents
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 21
 prependDatabaseOrSchema
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 5
 tableNames
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 tableNamesN
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 tableNameWithAlias
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 10
 fieldNameWithAlias
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 3
 fieldNamesWithAlias
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 6
 tableNamesWithIndexClauseOrJOIN
0.00% covered (danger)
0.00%
0 / 1
14.54
86.00% covered (warning)
86.00%
43 / 50
 indexName
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 addQuotes
0.00% covered (danger)
0.00%
0 / 1
5.27
77.78% covered (warning)
77.78%
7 / 9
 addIdentifierQuotes
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 isQuotedIdentifier
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 escapeLikeInternal
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 buildLike
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
10 / 10
 anyChar
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 anyString
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 nextSequenceValue
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 useIndexClause
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 ignoreIndexClause
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 replace
0.00% covered (danger)
0.00%
0 / 1
3.01
90.91% covered (success)
90.91%
10 / 11
 doReplace
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 14
 makeConditionCollidesUponKey
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 21
 makeConditionCollidesUponKeys
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 8
 upsert
0.00% covered (danger)
0.00%
0 / 1
3.85
54.55% covered (warning)
54.55%
6 / 11
 doUpsert
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 16
 deleteJoin
0.00% covered (danger)
0.00%
0 / 1
3.01
90.00% covered (success)
90.00%
9 / 10
 textFieldSize
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 9
 delete
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
9 / 9
 insertSelect
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
23 / 23
 isInsertSelectSafe
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 doInsertSelectGeneric
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 27
 doInsertSelectNative
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
15 / 15
 limitResult
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 7
 unionSupportsOrderAndLimit
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 unionQueries
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
2 / 2
 unionConditionPermutations
100.00% covered (success)
100.00%
1 / 1
17
100.00% covered (success)
100.00%
35 / 35
 conditional
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 strreplace
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getServerUptime
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 wasDeadlock
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 wasLockTimeout
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 wasConnectionLoss
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 wasReadOnlyError
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 wasErrorReissuable
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 3
 wasConnectionError
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 wasKnownStatementRollbackError
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 deadlockLoop
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 17
 masterPosWait
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getReplicaPos
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getMasterPos
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 serverIsReadOnly
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 onTransactionResolution
0.00% covered (danger)
0.00%
0 / 1
2.06
75.00% covered (warning)
75.00%
3 / 4
 onTransactionCommitOrIdle
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
7 / 7
 onTransactionIdle
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 onTransactionPreCommitOrIdle
0.00% covered (danger)
0.00%
0 / 1
5.39
75.00% covered (warning)
75.00%
9 / 12
 onAtomicSectionCancel
0.00% covered (danger)
0.00%
0 / 1
4.12
50.00% covered (danger)
50.00%
2 / 4
 currentAtomicSectionId
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 4
 reassignCallbacksForSection
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 13
 modifyCallbacksForCancel
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 17
 setTransactionListener
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 setTrxEndCallbackSuppression
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 runOnTransactionIdleCallbacks
0.00% covered (danger)
0.00%
0 / 1
11.73
81.82% covered (warning)
81.82%
27 / 33
 runOnTransactionPreCommitCallbacks
0.00% covered (danger)
0.00%
0 / 1
6.68
73.33% covered (warning)
73.33%
11 / 15
 runOnAtomicSectionCancelCallbacks
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 16
 runTransactionListenerCallbacks
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 11
 doSavepoint
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 doReleaseSavepoint
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 doRollbackToSavepoint
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 nextSavepointId
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 6
 startAtomic
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
15 / 15
 endAtomic
100.00% covered (success)
100.00%
1 / 1
9
100.00% covered (success)
100.00%
18 / 18
 cancelAtomic
0.00% covered (danger)
0.00%
0 / 1
13
97.87% covered (success)
97.87%
46 / 47
 doAtomicSection
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
7 / 7
 begin
0.00% covered (danger)
0.00%
0 / 1
56
0.00% covered (danger)
0.00%
0 / 37
 doBegin
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 commit
0.00% covered (danger)
0.00%
0 / 1
42.37
40.48% covered (danger)
40.48%
17 / 42
 doCommit
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 rollback
0.00% covered (danger)
0.00%
0 / 1
14.93
63.33% covered (warning)
63.33%
19 / 30
 doRollback
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 flushSnapshot
0.00% covered (danger)
0.00%
0 / 1
18.62
38.10% covered (danger)
38.10%
8 / 21
 explicitTrxActive
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 1
 duplicateTableStructure
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 listTables
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 listViews
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 timestamp
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 timestampOrNull
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 affectedRows
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 fetchAffectedRowCount
n/a
0 / 0
1
n/a
0 / 0
 resultObject
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 7
 ping
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 9
 replaceLostConnection
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 23
 getSessionLagStatus
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 getRecordedTransactionLagStatus
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 3
 getApproximateLagStatus
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 2
 getCacheSetOptions
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 10
 getLag
0.00% covered (danger)
0.00%
0 / 1
3.58
60.00% covered (warning)
60.00%
3 / 5
 doGetLag
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 maxListLen
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 encodeBlob
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 decodeBlob
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 setSessionOptions
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 sourceFile
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 14
 setSchemaVars
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 2
 sourceStream
0.00% covered (danger)
0.00%
0 / 1
240
0.00% covered (danger)
0.00%
0 / 33
 streamStatementEnd
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
7 / 7
 replaceVars
0.00% covered (danger)
0.00%
0 / 1
182
0.00% covered (danger)
0.00%
0 / 16
 getSchemaVars
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getDefaultSchemaVars
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 lockIsFree
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 lock
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 unlock
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 getScopedLockAndFlush
0.00% covered (danger)
0.00%
0 / 1
4.03
88.24% covered (warning)
88.24%
15 / 17
 namedLocksEnqueue
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 tableLocksHaveTransactionScope
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 lockTables
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 5
 doLockTables
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 unlockTables
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 doUnlockTables
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 dropTable
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 doDropTable
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 truncate
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 8
 doTruncate
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 getInfinity
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 encodeExpiry
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 3
 decodeExpiry
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 3
 setBigSelects
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 isReadOnly
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getReadOnlyReason
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 8
 setTableAliases
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setIndexAliases
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 fieldHasBit
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getBindingHandle
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 5
 __toString
0.00% covered (danger)
0.00%
0 / 1
5.01
91.67% covered (success)
91.67%
11 / 12
 __clone
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 17
 __sleep
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 __destruct
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 12
<?php
/**
 * @defgroup Database Database
 *
 * This file deals with database interface functions
 * and query specifics/optimisations.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 *
 * @file
 * @ingroup Database
 */
namespace Wikimedia\Rdbms;
use BagOStuff;
use Exception;
use HashBagOStuff;
use InvalidArgumentException;
use LogicException;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use RuntimeException;
use Throwable;
use UnexpectedValueException;
use Wikimedia\AtEase\AtEase;
use Wikimedia\ScopedCallback;
use Wikimedia\Timestamp\ConvertibleTimestamp;
/**
 * Relational database abstraction object
 *
 * @stable to extend
 * @ingroup Database
 * @since 1.28
 */
abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface {
    /** @var BagOStuff APC cache */
    protected $srvCache;
    /** @var LoggerInterface */
    protected $connLogger;
    /** @var LoggerInterface */
    protected $queryLogger;
    /** @var LoggerInterface */
    protected $replLogger;
    /** @var callable Error logging callback */
    protected $errorLogger;
    /** @var callable Deprecation logging callback */
    protected $deprecationLogger;
    /** @var callable|null */
    protected $profiler;
    /** @var TransactionProfiler */
    protected $trxProfiler;
    /** @var DatabaseDomain */
    protected $currentDomain;
    /** @var object|resource|null Database connection */
    protected $conn;
    /** @var IDatabase|null Lazy handle to the master DB this server replicates from */
    private $lazyMasterHandle;
    /** @var string Server that this instance is currently connected to */
    protected $server;
    /** @var string User that this instance is currently connected under the name of */
    protected $user;
    /** @var string Password used to establish the current connection */
    protected $password;
    /** @var bool Whether this PHP instance is for a CLI script */
    protected $cliMode;
    /** @var string Agent name for query profiling */
    protected $agent;
    /** @var string Replication topology role of the server; one of the class ROLE_* constants */
    protected $topologyRole;
    /** @var string|null Host (or address) of the root master server for the replication topology */
    protected $topologyRootMaster;
    /** @var array Parameters used by initConnection() to establish a connection */
    protected $connectionParams;
    /** @var string[]|int[]|float[] SQL variables values to use for all new connections */
    protected $connectionVariables;
    /** @var int Row batch size to use for emulated INSERT SELECT queries */
    protected $nonNativeInsertSelectBatchSize;
    /** @var int Current bit field of class DBO_* constants */
    protected $flags;
    /** @var array Current LoadBalancer tracking information */
    protected $lbInfo = [];
    /** @var string Current SQL query delimiter */
    protected $delimiter = ';';
    /** @var array[] Current map of (table => (dbname, schema, prefix) map) */
    protected $tableAliases = [];
    /** @var string[] Current map of (index alias => index) */
    protected $indexAliases = [];
    /** @var array|null Current variables use for schema element placeholders */
    protected $schemaVars;
    /** @var string|bool|null Stashed value of html_errors INI setting */
    private $htmlErrors;
    /** @var int[] Prior flags member variable values */
    private $priorFlags = [];
    /** @var array Map of (name => 1) for locks obtained via lock() */
    protected $sessionNamedLocks = [];
    /** @var array Map of (table name => 1) for current TEMPORARY tables */
    protected $sessionTempTables = [];
    /** @var array Map of (table name => 1) for current TEMPORARY tables */
    protected $sessionDirtyTempTables = [];
    /** @var string ID of the active transaction or the empty string otherwise */
    private $trxShortId = '';
    /** @var int Transaction status */
    private $trxStatus = self::STATUS_TRX_NONE;
    /** @var Exception|null The last error that caused the status to become STATUS_TRX_ERROR */
    private $trxStatusCause;
    /** @var array|null Error details of the last statement-only rollback */
    private $trxStatusIgnoredCause;
    /** @var float|null UNIX timestamp at the time of BEGIN for the last transaction */
    private $trxTimestamp = null;
    /** @var float Replication lag estimate at the time of BEGIN for the last transaction */
    private $trxReplicaLag = null;
    /** @var string Name of the function that start the last transaction */
    private $trxFname = null;
    /** @var bool Whether possible write queries were done in the last transaction started */
    private $trxDoneWrites = false;
    /** @var bool Whether the current transaction was started implicitly due to DBO_TRX */
    private $trxAutomatic = false;
    /** @var int Counter for atomic savepoint identifiers (reset with each transaction) */
    private $trxAtomicCounter = 0;
    /** @var array List of (name, unique ID, savepoint ID) for each active atomic section level */
    private $trxAtomicLevels = [];
    /** @var bool Whether the current transaction was started implicitly by startAtomic() */
    private $trxAutomaticAtomic = false;
    /** @var string[] Write query callers of the current transaction */
    private $trxWriteCallers = [];
    /** @var float Seconds spent in write queries for the current transaction */
    private $trxWriteDuration = 0.0;
    /** @var int Number of write queries for the current transaction */
    private $trxWriteQueryCount = 0;
    /** @var int Number of rows affected by write queries for the current transaction */
    private $trxWriteAffectedRows = 0;
    /** @var float Like trxWriteQueryCount but excludes lock-bound, easy to replicate, queries */
    private $trxWriteAdjDuration = 0.0;
    /** @var int Number of write queries counted in trxWriteAdjDuration */
    private $trxWriteAdjQueryCount = 0;
    /** @var array[] List of (callable, method name, atomic section id) */
    private $trxIdleCallbacks = [];
    /** @var array[] List of (callable, method name, atomic section id) */
    private $trxPreCommitCallbacks = [];
    /**
     * @var array[] List of (callable, method name, atomic section id)
     * @phan-var array<array{0:callable,1:string,2:AtomicSectionIdentifier|null}>
     */
    private $trxEndCallbacks = [];
    /** @var array[] List of (callable, method name, atomic section id) */
    private $trxSectionCancelCallbacks = [];
    /** @var callable[] Map of (name => callable) */
    private $trxRecurringCallbacks = [];
    /** @var bool Whether to suppress triggering of transaction end callbacks */
    private $trxEndCallbacksSuppressed = false;
    /** @var integer|null Rows affected by the last query to query() or its CRUD wrappers */
    protected $affectedRowCount;
    /** @var float UNIX timestamp */
    private $lastPing = 0.0;
    /** @var string The last SQL query attempted */
    private $lastQuery = '';
    /** @var float|bool UNIX timestamp of last write query */
    private $lastWriteTime = false;
    /** @var string|bool */
    private $lastPhpError = false;
    /** @var float Query round trip time estimate */
    private $lastRoundTripEstimate = 0.0;
    /** @var int|null Integer ID of the managing LBFactory instance or null if none */
    private $ownerId;
    /** @var string Whether the database is a file on disk */
    public const ATTR_DB_IS_FILE = 'db-is-file';
    /** @var string Lock granularity is on the level of the entire database */
    public const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
    /** @var string The SCHEMA keyword refers to a grouping of tables in a database */
    public const ATTR_SCHEMAS_AS_TABLE_GROUPS = 'supports-schemas';
    /** @var int New Database instance will not be connected yet when returned */
    public const NEW_UNCONNECTED = 0;
    /** @var int New Database instance will already be connected when returned */
    public const NEW_CONNECTED = 1;
    /** @var int Transaction is in a error state requiring a full or savepoint rollback */
    public const STATUS_TRX_ERROR = 1;
    /** @var int Transaction is active and in a normal state */
    public const STATUS_TRX_OK = 2;
    /** @var int No transaction is active */
    public const STATUS_TRX_NONE = 3;
    /** @var string Idiom used when a cancelable atomic section started the transaction */
    private static $NOT_APPLICABLE = 'n/a';
    /** @var string Prefix to the atomic section counter used to make savepoint IDs */
    private static $SAVEPOINT_PREFIX = 'wikimedia_rdbms_atomic';
    /** @var int Writes to this temporary table do not affect lastDoneWrites() */
    private static $TEMP_NORMAL = 1;
    /** @var int Writes to this temporary table effect lastDoneWrites() */
    private static $TEMP_PSEUDO_PERMANENT = 2;
    /** @var int Number of times to re-try an operation in case of deadlock */
    private static $DEADLOCK_TRIES = 4;
    /** @var int Minimum time to wait before retry, in microseconds */
    private static $DEADLOCK_DELAY_MIN = 500000;
    /** @var int Maximum time to wait before retry */
    private static $DEADLOCK_DELAY_MAX = 1500000;
    /** @var int How long before it is worth doing a dummy query to test the connection */
    private static $PING_TTL = 1.0;
    /** @var string Dummy SQL query */
    private static $PING_QUERY = 'SELECT 1 AS ping';
    /** @var float Guess of how many seconds it takes to replicate a small insert */
    private static $TINY_WRITE_SEC = 0.010;
    /** @var float Consider a write slow if it took more than this many seconds */
    private static $SLOW_WRITE_SEC = 0.500;
    /** @var float Assume an insert of this many rows or less should be fast to replicate */
    private static $SMALL_WRITE_ROWS = 100;
    /** @var string[] List of DBO_* flags that can be changed after connection */
    protected static $MUTABLE_FLAGS = [
        'DBO_DEBUG',
        'DBO_NOBUFFER',
        'DBO_TRX',
        'DBO_DDLMODE',
    ];
    /** @var int Bit field of all DBO_* flags that can be changed after connection */
    protected static $DBO_MUTABLE = (
        self::DBO_DEBUG | self::DBO_NOBUFFER | self::DBO_TRX | self::DBO_DDLMODE
    );
    /**
     * @note exceptions for missing libraries/drivers should be thrown in initConnection()
     * @stable to call
     * @param array $params Parameters passed from Database::factory()
     */
    public function __construct( array $params ) {
        $this->connectionParams = [
            'host' => strlen( $params['host'] ) ? $params['host'] : null,
            'user' => strlen( $params['user'] ) ? $params['user'] : null,
            'dbname' => strlen( $params['dbname'] ) ? $params['dbname'] : null,
            'schema' => strlen( $params['schema'] ) ? $params['schema'] : null,
            'password' => is_string( $params['password'] ) ? $params['password'] : null,
            'tablePrefix' => (string)$params['tablePrefix']
        ];
        $this->lbInfo = $params['lbInfo'] ?? [];
        $this->lazyMasterHandle = $params['lazyMasterHandle'] ?? null;
        $this->connectionVariables = $params['variables'] ?? [];
        $this->flags = (int)$params['flags'];
        $this->cliMode = (bool)$params['cliMode'];
        $this->agent = (string)$params['agent'];
        $this->topologyRole = (string)$params['topologyRole'];
        $this->topologyRootMaster = (string)$params['topologicalMaster'];
        $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'] ?? 10000;
        $this->srvCache = $params['srvCache'];
        $this->profiler = is_callable( $params['profiler'] ) ? $params['profiler'] : null;
        $this->trxProfiler = $params['trxProfiler'];
        $this->connLogger = $params['connLogger'];
        $this->queryLogger = $params['queryLogger'];
        $this->replLogger = $params['replLogger'];
        $this->errorLogger = $params['errorLogger'];
        $this->deprecationLogger = $params['deprecationLogger'];
        // Set initial dummy domain until open() sets the final DB/prefix
        $this->currentDomain = new DatabaseDomain(
            $params['dbname'] != '' ? $params['dbname'] : null,
            $params['schema'] != '' ? $params['schema'] : null,
            $params['tablePrefix']
        );
        $this->ownerId = $params['ownerId'] ?? null;
    }
    /**
     * Initialize the connection to the database over the wire (or to local files)
     *
     * @throws LogicException
     * @throws InvalidArgumentException
     * @throws DBConnectionError
     * @since 1.31
     */
    final public function initConnection() {
        if ( $this->isOpen() ) {
            throw new LogicException( __METHOD__ . ': already connected' );
        }
        // Establish the connection
        $this->doInitConnection();
    }
    /**
     * Actually connect to the database over the wire (or to local files)
     *
     * @throws DBConnectionError
     * @since 1.31
     */
    protected function doInitConnection() {
        $this->open(
            $this->connectionParams['host'],
            $this->connectionParams['user'],
            $this->connectionParams['password'],
            $this->connectionParams['dbname'],
            $this->connectionParams['schema'],
            $this->connectionParams['tablePrefix']
        );
    }
    /**
     * Open a new connection to the database (closing any existing one)
     *
     * @param string|null $server Database server host
     * @param string|null $user Database user name
     * @param string|null $password Database user password
     * @param string|null $dbName Database name
     * @param string|null $schema Database schema name
     * @param string $tablePrefix Table prefix
     * @throws DBConnectionError
     */
    abstract protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix );
    /**
     * Construct a Database subclass instance given a database type and parameters
     *
     * This also connects to the database immediately upon object construction
     *
     * @param string $type A possible DB type (sqlite, mysql, postgres,...)
     * @param array $params Parameter map with keys:
     *   - host : The hostname of the DB server
     *   - user : The name of the database user the client operates under
     *   - password : The password for the database user
     *   - dbname : The name of the database to use where queries do not specify one.
     *      The database must exist or an error might be thrown. Setting this to the empty string
     *      will avoid any such errors and make the handle have no implicit database scope. This is
     *      useful for queries like SHOW STATUS, CREATE DATABASE, or DROP DATABASE. Note that a
     *      "database" in Postgres is rougly equivalent to an entire MySQL server. This the domain
     *      in which user names and such are defined, e.g. users are database-specific in Postgres.
     *   - schema : The database schema to use (if supported). A "schema" in Postgres is roughly
     *      equivalent to a "database" in MySQL. Note that MySQL and SQLite do not use schemas.
     *   - tablePrefix : Optional table prefix that is implicitly added on to all table names
     *      recognized in queries. This can be used in place of schemas for handle site farms.
     *   - flags : Optional bit field of DBO_* constants that define connection, protocol,
     *      buffering, and transaction behavior. It is STRONGLY adviced to leave the DBO_DEFAULT
     *      flag in place UNLESS this this database simply acts as a key/value store.
     *   - driver: Optional name of a specific DB client driver. For MySQL, there is only the
     *      'mysqli' driver; the old one 'mysql' has been removed.
     *   - variables: Optional map of session variables to set after connecting. This can be
     *      used to adjust lock timeouts or encoding modes and the like.
     *   - topologyRole: Optional IDatabase::ROLE_* constant for the server.
     *   - topologicalMaster: Optional name of the master server within the replication topology.
     *   - lbInfo: Optional map of field/values for the managing load balancer instance.
     *      The "master" and "replica" fields are used to flag the replication role of this
     *      database server and whether methods like getLag() should actually issue queries.
     *   - lazyMasterHandle: lazy-connecting IDatabase handle to the master DB for the cluster
     *      that this database belongs to. This is used for replication status purposes.
     *   - connLogger: Optional PSR-3 logger interface instance.
     *   - queryLogger: Optional PSR-3 logger interface instance.
     *   - profiler : Optional callback that takes a section name argument and returns
     *      a ScopedCallback instance that ends the profile section in its destructor.
     *      These will be called in query(), using a simplified version of the SQL that
     *      also includes the agent as a SQL comment.
     *   - trxProfiler: Optional TransactionProfiler instance.
     *   - errorLogger: Optional callback that takes an Exception and logs it.
     *   - deprecationLogger: Optional callback that takes a string and logs it.
     *   - cliMode: Whether to consider the execution context that of a CLI script.
     *   - agent: Optional name used to identify the end-user in query profiling/logging.
     *   - srvCache: Optional BagOStuff instance to an APC-style cache.
     *   - nonNativeInsertSelectBatchSize: Optional batch size for non-native INSERT SELECT.
     *   - ownerId: Optional integer ID of a LoadBalancer instance that manages this instance.
     * @param int $connect One of the class constants (NEW_CONNECTED, NEW_UNCONNECTED) [optional]
     * @return Database|null If the database driver or extension cannot be found
     * @throws InvalidArgumentException If the database driver or extension cannot be found
     * @since 1.18
     */
    final public static function factory( $type, $params = [], $connect = self::NEW_CONNECTED ) {
        $class = self::getClass( $type, $params['driver'] ?? null );
        if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
            $params += [
                // Default configuration
                'host' => null,
                'user' => null,
                'password' => null,
                'dbname' => null,
                'schema' => null,
                'tablePrefix' => '',
                'flags' => 0,
                'variables' => [],
                'lbInfo' => [],
                'cliMode' => ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ),
                'agent' => '',
                'ownerId' => null,
                'topologyRole' => null,
                'topologicalMaster' => null,
                // Objects and callbacks
                'lazyMasterHandle' => $params['lazyMasterHandle'] ?? null,
                'srvCache' => $params['srvCache'] ?? new HashBagOStuff(),
                'profiler' => $params['profiler'] ?? null,
                'trxProfiler' => $params['trxProfiler'] ?? new TransactionProfiler(),
                'connLogger' => $params['connLogger'] ?? new NullLogger(),
                'queryLogger' => $params['queryLogger'] ?? new NullLogger(),
                'replLogger' => $params['replLogger'] ?? new NullLogger(),
                'errorLogger' => $params['errorLogger'] ?? function ( Throwable $e ) {
                    trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
                },
                'deprecationLogger' => $params['deprecationLogger'] ?? function ( $msg ) {
                    trigger_error( $msg, E_USER_DEPRECATED );
                }
            ];
            /** @var Database $conn */
            $conn = new $class( $params );
            if ( $connect === self::NEW_CONNECTED ) {
                $conn->initConnection();
            }
        } else {
            $conn = null;
        }
        return $conn;
    }
    /**
     * @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
     * @param string|null $driver Optional name of a specific DB client driver
     * @return array Map of (Database::ATTR_* constant => value) for all such constants
     * @throws InvalidArgumentException
     * @since 1.31
     */
    final public static function attributesFromType( $dbType, $driver = null ) {
        static $defaults = [
            self::ATTR_DB_IS_FILE => false,
            self::ATTR_DB_LEVEL_LOCKING => false,
            self::ATTR_SCHEMAS_AS_TABLE_GROUPS => false
        ];
        $class = self::getClass( $dbType, $driver );
        return call_user_func( [ $class, 'getAttributes' ] ) + $defaults;
    }
    /**
     * @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
     * @param string|null $driver Optional name of a specific DB client driver
     * @return string Database subclass name to use
     * @throws InvalidArgumentException
     */
    private static function getClass( $dbType, $driver = null ) {
        // For database types with built-in support, the below maps type to IDatabase
        // implementations. For types with multiple driver implementations (PHP extensions),
        // an array can be used, keyed by extension name. In case of an array, the
        // optional 'driver' parameter can be used to force a specific driver. Otherwise,
        // we auto-detect the first available driver. For types without built-in support,
        // an class named "Database<Type>" us used, eg. DatabaseFoo for type 'foo'.
        static $builtinTypes = [
            'mysql' => [ 'mysqli' => DatabaseMysqli::class ],
            'sqlite' => DatabaseSqlite::class,
            'postgres' => DatabasePostgres::class,
        ];
        $dbType = strtolower( $dbType );
        $class = false;
        if ( isset( $builtinTypes[$dbType] ) ) {
            $possibleDrivers = $builtinTypes[$dbType];
            if ( is_string( $possibleDrivers ) ) {
                $class = $possibleDrivers;
            } elseif ( (string)$driver !== '' ) {
                if ( !isset( $possibleDrivers[$driver] ) ) {
                    throw new InvalidArgumentException( __METHOD__ .
                        " type '$dbType' does not support driver '{$driver}'" );
                }
                $class = $possibleDrivers[$driver];
            } else {
                foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
                    if ( extension_loaded( $posDriver ) ) {
                        $class = $possibleClass;
                        break;
                    }
                }
            }
        } else {
            $class = 'Database' . ucfirst( $dbType );
        }
        if ( $class === false ) {
            throw new InvalidArgumentException( __METHOD__ .
                " no viable database extension found for type '$dbType'" );
        }
        return $class;
    }
    /**
     * @stable to override
     * @return array Map of (Database::ATTR_* constant => value)
     * @since 1.31
     */
    protected static function getAttributes() {
        return [];
    }
    /**
     * Set the PSR-3 logger interface to use for query logging. (The logger
     * interfaces for connection logging and error logging can be set with the
     * constructor.)
     *
     * @param LoggerInterface $logger
     */
    public function setLogger( LoggerInterface $logger ) {
        $this->queryLogger = $logger;
    }
    public function getServerInfo() {
        return $this->getServerVersion();
    }
    public function getTopologyRole() {
        return $this->topologyRole;
    }
    public function getTopologyRootMaster() {
        return $this->topologyRootMaster;
    }
    final public function trxLevel() {
        return ( $this->trxShortId != '' ) ? 1 : 0;
    }
    public function trxTimestamp() {
        return $this->trxLevel() ? $this->trxTimestamp : null;
    }
    /**
     * @return int One of the STATUS_TRX_* class constants
     * @since 1.31
     */
    public function trxStatus() {
        return $this->trxStatus;
    }
    public function tablePrefix( $prefix = null ) {
        $old = $this->currentDomain->getTablePrefix();
        if ( $prefix !== null ) {
            $this->currentDomain = new DatabaseDomain(
                $this->currentDomain->getDatabase(),
                $this->currentDomain->getSchema(),
                $prefix
            );
        }
        return $old;
    }
    public function dbSchema( $schema = null ) {
        if ( strlen( $schema ) && $this->getDBname() === null ) {
            throw new DBUnexpectedError( $this, "Cannot set schema to '$schema'; no database set" );
        }
        $old = $this->currentDomain->getSchema();
        if ( $schema !== null ) {
            $this->currentDomain = new DatabaseDomain(
                $this->currentDomain->getDatabase(),
                // DatabaseDomain uses null for unspecified schemas
                strlen( $schema ) ? $schema : null,
                $this->currentDomain->getTablePrefix()
            );
        }
        return (string)$old;
    }
    /**
     * @stable to override
     * @return string Schema to use to qualify relations in queries
     */
    protected function relationSchemaQualifier() {
        return $this->dbSchema();
    }
    public function getLBInfo( $name = null ) {
        if ( $name === null ) {
            return $this->lbInfo;
        }
        if ( array_key_exists( $name, $this->lbInfo ) ) {
            return $this->lbInfo[$name];
        }
        return null;
    }
    public function setLBInfo( $nameOrArray, $value = null ) {
        if ( is_array( $nameOrArray ) ) {
            $this->lbInfo = $nameOrArray;
        } elseif ( is_string( $nameOrArray ) ) {
            if ( $value !== null ) {
                $this->lbInfo[$nameOrArray] = $value;
            } else {
                unset( $this->lbInfo[$nameOrArray] );
            }
        } else {
            throw new InvalidArgumentException( "Got non-string key" );
        }
    }
    /**
     * Get a handle to the master server of the cluster to which this server belongs
     *
     * @return IDatabase|null
     * @since 1.27
     */
    protected function getLazyMasterHandle() {
        return $this->lazyMasterHandle;
    }
    /**
     * @inheritDoc
     * @stable to override
     */
    public function implicitOrderby() {
        return true;
    }
    public function lastQuery() {
        return $this->lastQuery;
    }
    public function lastDoneWrites() {
        return $this->lastWriteTime ?: false;
    }
    public function writesPending() {
        return $this->trxLevel() && $this->trxDoneWrites;
    }
    public function writesOrCallbacksPending() {
        return $this->trxLevel() && (
            $this->trxDoneWrites ||
            $this->trxIdleCallbacks ||
            $this->trxPreCommitCallbacks ||
            $this->trxEndCallbacks ||
            $this->trxSectionCancelCallbacks
        );
    }
    public function preCommitCallbacksPending() {
        return $this->trxLevel() && $this->trxPreCommitCallbacks;
    }
    /**
     * @return string|null
     */
    final protected function getTransactionRoundId() {
        // If transaction round participation is enabled, see if one is active
        if ( $this->getFlag( self::DBO_TRX ) ) {
            $id = $this->getLBInfo( self::LB_TRX_ROUND_ID );
            return is_string( $id ) ? $id : null;
        }
        return null;
    }
    public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
        if ( !$this->trxLevel() ) {
            return false;
        } elseif ( !$this->trxDoneWrites ) {
            return 0.0;
        }
        switch ( $type ) {
            case self::ESTIMATE_DB_APPLY:
                return $this->pingAndCalculateLastTrxApplyTime();
            default: // everything
                return $this->trxWriteDuration;
        }
    }
    /**
     * @return float Time to apply writes to replicas based on trxWrite* fields
     */
    private function pingAndCalculateLastTrxApplyTime() {
        $this->ping( $rtt );
        $rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
        $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
        // For omitted queries, make them count as something at least
        $omitted = $this->trxWriteQueryCount - $this->trxWriteAdjQueryCount;
        $applyTime += self::$TINY_WRITE_SEC * $omitted;
        return $applyTime;
    }
    public function pendingWriteCallers() {
        return $this->trxLevel() ? $this->trxWriteCallers : [];
    }
    public function pendingWriteRowsAffected() {
        return $this->trxWriteAffectedRows;
    }
    /**
     * List the methods that have write queries or callbacks for the current transaction
     *
     * This method should not be used outside of Database/LoadBalancer
     *
     * @return string[]
     * @since 1.32
     */
    public function pendingWriteAndCallbackCallers() {
        $fnames = $this->pendingWriteCallers();
        foreach ( [
            $this->trxIdleCallbacks,
            $this->trxPreCommitCallbacks,
            $this->trxEndCallbacks,
            $this->trxSectionCancelCallbacks
        ] as $callbacks ) {
            foreach ( $callbacks as $callback ) {
                $fnames[] = $callback[1];
            }
        }
        return $fnames;
    }
    /**
     * @return string
     */
    private function flatAtomicSectionList() {
        return array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) {
            return $accum === null ? $v[0] : "$accum" . $v[0];
        } );
    }
    public function isOpen() {
        return (bool)$this->conn;
    }
    public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
        if ( $flag & ~static::$DBO_MUTABLE ) {
            throw new DBUnexpectedError(
                $this,
                "Got $flag (allowed: " . implode( ', ', static::$MUTABLE_FLAGS ) . ')'
            );
        }
        if ( $remember === self::REMEMBER_PRIOR ) {
            array_push( $this->priorFlags, $this->flags );
        }
        $this->flags |= $flag;
    }
    public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
        if ( $flag & ~static::$DBO_MUTABLE ) {
            throw new DBUnexpectedError(
                $this,
                "Got $flag (allowed: " . implode( ', ', static::$MUTABLE_FLAGS ) . ')'
            );
        }
        if ( $remember === self::REMEMBER_PRIOR ) {
            array_push( $this->priorFlags, $this->flags );
        }
        $this->flags &= ~$flag;
    }
    public function restoreFlags( $state = self::RESTORE_PRIOR ) {
        if ( !$this->priorFlags ) {
            return;
        }
        if ( $state === self::RESTORE_INITIAL ) {
            $this->flags = reset( $this->priorFlags );
            $this->priorFlags = [];
        } else {
            $this->flags = array_pop( $this->priorFlags );
        }
    }
    public function getFlag( $flag ) {
        return ( ( $this->flags & $flag ) === $flag );
    }
    public function getDomainID() {
        return $this->currentDomain->getId();
    }
    /**
     * Get information about an index into an object
     *
     * @stable to override
     * @param string $table Table name
     * @param string $index Index name
     * @param string $fname Calling function name
     * @return mixed Database-specific index description class or false if the index does not exist
     */
    abstract public function indexInfo( $table, $index, $fname = __METHOD__ );
    /**
     * Wrapper for addslashes()
     *
     * @stable to override
     * @param string $s String to be slashed.
     * @return string Slashed string.
     */
    abstract public function strencode( $s );
    /**
     * Set a custom error handler for logging errors during database connection
     */
    protected function installErrorHandler() {
        $this->lastPhpError = false;
        $this->htmlErrors = ini_set( 'html_errors', '0' );
        set_error_handler( [ $this, 'connectionErrorLogger' ] );
    }
    /**
     * Restore the previous error handler and return the last PHP error for this DB
     *
     * @return bool|string
     */
    protected function restoreErrorHandler() {
        restore_error_handler();
        if ( $this->htmlErrors !== false ) {
            ini_set( 'html_errors', $this->htmlErrors );
        }
        return $this->getLastPHPError();
    }
    /**
     * @return string|bool Last PHP error for this DB (typically connection errors)
     */
    protected function getLastPHPError() {
        if ( $this->lastPhpError ) {
            $error = preg_replace( '!\[<a.*</a>\]!', '', $this->lastPhpError );
            $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
            return $error;
        }
        return false;
    }
    /**
     * Error handler for logging errors during database connection
     * This method should not be used outside of Database classes
     *
     * @param int $errno
     * @param string $errstr
     */
    public function connectionErrorLogger( $errno, $errstr ) {
        $this->lastPhpError = $errstr;
    }
    /**
     * Create a log context to pass to PSR-3 logger functions.
     *
     * @param array $extras Additional data to add to context
     * @return array
     */
    protected function getLogContext( array $extras = [] ) {
        return array_merge(
            [
                'db_server' => $this->server,
                'db_name' => $this->getDBname(),
                'db_user' => $this->user,
            ],
            $extras
        );
    }
    final public function close( $fname = __METHOD__, $owner = null ) {
        $error = null; // error to throw after disconnecting
        $wasOpen = (bool)$this->conn;
        // This should mostly do nothing if the connection is already closed
        if ( $this->conn ) {
            // Roll back any dangling transaction first
            if ( $this->trxLevel() ) {
                if ( $this->trxAtomicLevels ) {
                    // Cannot let incomplete atomic sections be committed
                    $levels = $this->flatAtomicSectionList();
                    $error = "$fname: atomic sections $levels are still open";
                } elseif ( $this->trxAutomatic ) {
                    // Only the connection manager can commit non-empty DBO_TRX transactions
                    // (empty ones we can silently roll back)
                    if ( $this->writesOrCallbacksPending() ) {
                        $error = "$fname" .
                            "expected mass rollback of all peer transactions (DBO_TRX set)";
                    }
                } else {
                    // Manual transactions should have been committed or rolled
                    // back, even if empty.
                    $error = "$fname: transaction is still open (from {$this->trxFname})";
                }
                if ( $this->trxEndCallbacksSuppressed && $error === null ) {
                    $error = "$fname: callbacks are suppressed; cannot properly commit";
                }
                // Rollback the changes and run any callbacks as needed
                $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
            }
            // Close the actual connection in the binding handle
            $closed = $this->closeConnection();
        } else {
            $closed = true; // already closed; nothing to do
        }
        $this->conn = null;
        // Log or throw any unexpected errors after having disconnected
        if ( $error !== null ) {
            // T217819, T231443: if this is probably just LoadBalancer trying to recover from
            // errors and shutdown, then log any problems and move on since the request has to
            // end one way or another. Throwing errors is not very useful at some point.
            if ( $this->ownerId !== null && $owner === $this->ownerId ) {
                $this->queryLogger->error( $error );
            } else {
                throw new DBUnexpectedError( $this, $error );
            }
        }
        // Note that various subclasses call close() at the start of open(), which itself is
        // called by replaceLostConnection(). In that case, just because onTransactionResolution()
        // callbacks are pending does not mean that an exception should be thrown. Rather, they
        // will be executed after the reconnection step.
        if ( $wasOpen ) {
            // Sanity check that no callbacks are dangling
            $fnames = $this->pendingWriteAndCallbackCallers();
            if ( $fnames ) {
                throw new RuntimeException(
                    "Transaction callbacks are still pending: " . implode( ', ', $fnames )
                );
            }
        }
        return $closed;
    }
    /**
     * Make sure there is an open connection handle (alive or not) as a sanity check
     *
     * This guards against fatal errors to the binding handle not being defined
     * in cases where open() was never called or close() was already called
     *
     * @throws DBUnexpectedError
     */
    final protected function assertHasConnectionHandle() {
        if ( !$this->isOpen() ) {
            throw new DBUnexpectedError( $this, "DB connection was already closed" );
        }
    }
    /**
     * Make sure that this server is not marked as a replica nor read-only as a sanity check
     *
     * @throws DBReadOnlyError
     */
    protected function assertIsWritableMaster() {
        $info = $this->getReadOnlyReason();
        if ( $info ) {
            list( $reason, $source ) = $info;
            if ( $source === 'role' ) {
                throw new DBReadOnlyRoleError( $this, "Database is read-only: $reason" );
            } else {
                throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
            }
        }
    }
    /**
     * Closes underlying database connection
     * @return bool Whether connection was closed successfully
     * @since 1.20
     */
    abstract protected function closeConnection();
    /**
     * Run a query and return a DBMS-dependent wrapper or boolean
     *
     * This is meant to handle the basic command of actually sending a query to the
     * server via the driver. No implicit transaction, reconnection, nor retry logic
     * should happen here. The higher level query() method is designed to handle those
     * sorts of concerns. This method should not trigger such higher level methods.
     *
     * The lastError() and lastErrno() methods should meaningfully reflect what error,
     * if any, occurred during the last call to this method. Methods like executeQuery(),
     * query(), select(), insert(), update(), delete(), and upsert() implement their calls
     * to doQuery() such that an immediately subsequent call to lastError()/lastErrno()
     * meaningfully reflects any error that occurred during that public query method call.
     *
     * For SELECT queries, this returns either:
     *   - a) A driver-specific value/resource, only on success. This can be iterated
     *        over by calling fetchObject()/fetchRow() until there are no more rows.
     *        Alternatively, the result can be passed to resultObject() to obtain an
     *        IResultWrapper instance which can then be iterated over via "foreach".
     *   - b) False, on any query failure
     *
     * For non-SELECT queries, this returns either:
     *   - a) A driver-specific value/resource, only on success
     *   - b) True, only on success (e.g. no meaningful result other than "OK")
     *   - c) False, on any query failure
     *
     * @param string $sql SQL query
     * @return mixed|bool An object, resource, or true on success; false on failure
     */
    abstract protected function doQuery( $sql );
    /**
     * Determine whether a query writes to the DB. When in doubt, this returns true.
     *
     * Main use cases:
     *
     * - Subsequent web requests should not need to wait for replication from
     *   the master position seen by this web request, unless this request made
     *   changes to the master. This is handled by ChronologyProtector by checking
     *   doneWrites() at the end of the request. doneWrites() returns true if any
     *   query set lastWriteTime; which query() does based on isWriteQuery().
     *
     * - Reject write queries to replica DBs, in query().
     *
     * @param string $sql
     * @param int $flags Query flags to query()
     * @return bool
     */
    protected function isWriteQuery( $sql, $flags ) {
        if (
            $this->fieldHasBit( $flags, self::QUERY_CHANGE_ROWS ) ||
            $this->fieldHasBit( $flags, self::QUERY_CHANGE_SCHEMA )
        ) {
            return true;
        } elseif ( $this->fieldHasBit( $flags, self::QUERY_CHANGE_NONE ) ) {
            return false;
        }
        // BEGIN and COMMIT queries are considered read queries here.
        // Database backends and drivers (MySQL, MariaDB, php-mysqli) generally
        // treat these as write queries, in that their results have "affected rows"
        // as meta data as from writes, instead of "num rows" as from reads.
        // But, we treat them as read queries because when reading data (from
        // either replica or master) we use transactions to enable repeatable-read
        // snapshots, which ensures we get consistent results from the same snapshot
        // for all queries within a request. Use cases:
        // - Treating these as writes would trigger ChronologyProtector (see method doc).
        // - We use this method to reject writes to replicas, but we need to allow
        //   use of transactions on replicas for read snapshots. This is fine given
        //   that transactions by themselves don't make changes, only actual writes
        //   within the transaction matter, which we still detect.
        return !preg_match(
            '/^\s*(?:SELECT|BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|USE|\(SELECT)\b/i',
            $sql
        );
    }
    /**
     * @param string $sql
     * @return string|null
     */
    protected function getQueryVerb( $sql ) {
        return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
    }
    /**
     * Determine whether a SQL statement is sensitive to isolation level.
     *
     * A SQL statement is considered transactable if its result could vary
     * depending on the transaction isolation level. Operational commands
     * such as 'SET' and 'SHOW' are not considered to be transactable.
     *
     * Main purpose: Used by query() to decide whether to begin a transaction
     * before the current query (in DBO_TRX mode, on by default).
     *
     * @stable to override
     * @param string $sql
     * @return bool
     */
    protected function isTransactableQuery( $sql ) {
        return !in_array(
            $this->getQueryVerb( $sql ),
            [ 'BEGIN', 'ROLLBACK', 'COMMIT', 'SET', 'SHOW', 'CREATE', 'ALTER', 'USE', 'SHOW' ],
            true
        );
    }
    /**
     * @param string $sql SQL query
     * @param bool $pseudoPermanent Treat any table from CREATE TEMPORARY as pseudo-permanent
     * @return array[] List of change n-tuples with:
     *   - int: self::TEMP_* constant for temp table operations
     *   - string: SQL query verb from $sql
     *   - string: Name of the temp table changed in $sql
     */
    protected function getTempTableWrites( $sql, $pseudoPermanent ) {
        // Regexes for basic queries that can create/change/drop temporary tables.
        // For simplicity, this only looks for tables with sane, alphanumeric, names;
        // temporary tables only need simple programming names anyway.
        static $regexes = null;
        if ( $regexes === null ) {
            // Regex with a group for quoted table 0 and a group for quoted tables 1..N
            $qts = '((?:\w+|`\w+`|\'\w+\'|"\w+")(?:\s*,\s*(?:\w+|`\w+`|\'\w+\'|"\w+"))*)';
            // Regex to get query verb, table 0, and tables 1..N
            $regexes = [
                // DML write queries
                "/^(INSERT|REPLACE)\s+(?:\w+\s+)*?INTO\s+$qts/i",
                "/^(UPDATE)(?:\s+OR\s+\w+|\s+IGNORE|\s+ONLY)?\s+$qts/i",
                "/^(DELETE)\s+(?:\w+\s+)*?FROM(?:\s+ONLY)?\s+$qts/i",
                // DDL write queries
                "/^(CREATE)\s+TEMPORARY\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+$qts/i",
                "/^(DROP)\s+(?:TEMPORARY\s+)?TABLE(?:\s+IF\s+EXISTS)?\s+$qts/i",
                "/^(TRUNCATE)\s+(?:TEMPORARY\s+)?TABLE\s+$qts/i",
                "/^(ALTER)\s+TABLE\s+$qts/i"
            ];
        }
        $queryVerb = null;
        $queryTables = [];
        foreach ( $regexes as $regex ) {
            if ( preg_match( $regex, $sql, $m, PREG_UNMATCHED_AS_NULL ) ) {
                $queryVerb = $m[1];
                $allTables = preg_split( '/\s*,\s*/', $m[2] );
                foreach ( $allTables as $quotedTable ) {
                    $queryTables[] = trim( $quotedTable, "\"'`" );
                }
                break;
            }
        }
        $tempTableChanges = [];
        foreach ( $queryTables as $table ) {
            if ( $queryVerb === 'CREATE' ) {
                // Record the type of temporary table being created
                $tableType = $pseudoPermanent ? self::$TEMP_PSEUDO_PERMANENT : self::$TEMP_NORMAL;
            } else {
                $tableType = $this->sessionTempTables[$table] ?? null;
            }
            if ( $tableType !== null ) {
                $tempTableChanges[] = [ $tableType, $queryVerb, $table ];
            }
        }
        return $tempTableChanges;
    }
    /**
     * @param IResultWrapper|bool $ret
     * @param array[] $changes List of change n-tuples with from getTempWrites()
     */
    protected function registerTempWrites( $ret, array $changes ) {
        if ( $ret === false ) {
            return;
        }
        foreach ( $changes as list( $tmpTableType, $verb, $table ) ) {
            switch ( $verb ) {
                case 'CREATE':
                    $this->sessionTempTables[$table] = $tmpTableType;
                    break;
                case 'DROP':
                    unset( $this->sessionTempTables[$table] );
                    unset( $this->sessionDirtyTempTables[$table] );
                    break;
                case 'TRUNCATE':
                    unset( $this->sessionDirtyTempTables[$table] );
                    break;
                default:
                    $this->sessionDirtyTempTables[$table] = 1;
                    break;
            }
        }
    }
    /**
     * Check if the table is both a TEMPORARY table and has not yet received CRUD operations
     *
     * @param string $table
     * @return bool
     * @since 1.35
     */
    protected function isPristineTemporaryTable( $table ) {
        $rawTable = $this->tableName( $table, 'raw' );
        return (
            isset( $this->sessionTempTables[$rawTable] ) &&
            !isset( $this->sessionDirtyTempTables[$rawTable] )
        );
    }
    public function query( $sql, $fname = __METHOD__, $flags = self::QUERY_NORMAL ) {
        $flags = (int)$flags; // b/c; this field used to be a bool
        // Sanity check that the SQL query is appropriate in the current context and is
        // allowed for an outside caller (e.g. does not break transaction/session tracking).
        $this->assertQueryIsCurrentlyAllowed( $sql, $fname );
        // Send the query to the server and fetch any corresponding errors
        list( $ret, $err, $errno, $unignorable ) = $this->executeQuery( $sql, $fname, $flags );
        if ( $ret === false ) {
            $ignoreErrors = $this->fieldHasBit( $flags, self::QUERY_SILENCE_ERRORS );
            // Throw an error unless both the ignore flag was set and a rollback is not needed
            $this->reportQueryError( $err, $errno, $sql, $fname, $ignoreErrors && !$unignorable );
        }
        return $this->resultObject( $ret );
    }
    /**
     * Execute a query, retrying it if there is a recoverable connection loss
     *
     * This is similar to query() except:
     *   - It does not prevent all non-ROLLBACK queries if there is a corrupted transaction
     *   - It does not disallow raw queries that are supposed to use dedicated IDatabase methods
     *   - It does not throw exceptions for common error cases
     *
     * This is meant for internal use with Database subclasses.
     *
     * @param string $sql Original SQL query
     * @param string $fname Name of the calling function
     * @param int $flags Bit field of class QUERY_* constants
     * @return array An n-tuple of:
     *   - mixed|bool: An object, resource, or true on success; false on failure
     *   - string: The result of calling lastError()
     *   - int: The result of calling lastErrno()
     *   - bool: Whether a rollback is needed to allow future non-rollback queries
     * @throws DBUnexpectedError
     */
    final protected function executeQuery( $sql, $fname, $flags ) {
        $this->assertHasConnectionHandle();
        $priorTransaction = $this->trxLevel();
        if ( $this->isWriteQuery( $sql, $flags ) ) {
            // Do not treat temporary table writes as "meaningful writes" since they are only
            // visible to one session and are not permanent. Profile them as reads. Integration
            // tests can override this behavior via $flags.
            $pseudoPermanent = $this->fieldHasBit( $flags, self::QUERY_PSEUDO_PERMANENT );
            $tempTableChanges = $this->getTempTableWrites( $sql, $pseudoPermanent );
            $isPermWrite = !$tempTableChanges;
            foreach ( $tempTableChanges as list( $tmpType ) ) {
                $isPermWrite = $isPermWrite || ( $tmpType !== self::$TEMP_NORMAL );
            }
            // Permit temporary table writes on replica DB connections
            // but require a writable master connection for any persistent writes.
            if ( $isPermWrite ) {
                $this->assertIsWritableMaster();
                // DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries
                if ( $this->fieldHasBit( $flags, self::QUERY_REPLICA_ROLE ) ) {
                    throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" );
                }
            }
        } else {
            // No permanent writes in this query
            $isPermWrite = false;
            // No temporary tables written to either
            $tempTableChanges = [];
        }
        // Add trace comment to the begin of the sql string, right after the operator.
        // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598).
        $encAgent = str_replace( '/', '-', $this->agent );
        $commentedSql = preg_replace( '/\s|$/', " /* $fname $encAgent */ ", $sql, 1 );
        // Send the query to the server and fetch any corresponding errors.
        // This also doubles as a "ping" to see if the connection was dropped.
        list( $ret, $err, $errno, $recoverableSR, $recoverableCL, $reconnected ) =
            $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
        // Check if the query failed due to a recoverable connection loss
        $allowRetry = !$this->fieldHasBit( $flags, self::QUERY_NO_RETRY );
        if ( $ret === false && $recoverableCL && $reconnected && $allowRetry ) {
            // Silently resend the query to the server since it is safe and possible
            list( $ret, $err, $errno, $recoverableSR, $recoverableCL ) =
                $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
        }
        // Register creation and dropping of temporary tables
        $this->registerTempWrites( $ret, $tempTableChanges );
        $corruptedTrx = false;
        if ( $ret === false ) {
            if ( $priorTransaction ) {
                if ( $recoverableSR ) {
                    # We're ignoring an error that caused just the current query to be aborted.
                    # But log the cause so we can log a deprecation notice if a caller actually
                    # does ignore it.
                    $this->trxStatusIgnoredCause = [ $err, $errno, $fname ];
                } elseif ( !$recoverableCL ) {
                    # Either the query was aborted or all queries after BEGIN where aborted.
                    # In the first case, the only options going forward are (a) ROLLBACK, or
                    # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
                    # option is ROLLBACK, since the snapshots would have been released.
                    $corruptedTrx = true; // cannot recover
                    $this->trxStatus = self::STATUS_TRX_ERROR;
                    $this->trxStatusCause = $this->getQueryException( $err, $errno, $sql, $fname );
                    $this->trxStatusIgnoredCause = null;
                }
            }
        }
        return [ $ret, $err, $errno, $corruptedTrx ];
    }
    /**
     * Wrapper for doQuery() that handles DBO_TRX, profiling, logging, affected row count
     * tracking, and reconnects (without retry) on query failure due to connection loss
     *
     * @param string $sql Original SQL query
     * @param string $commentedSql SQL query with debugging/trace comment
     * @param bool $isPermWrite Whether the query is a (non-temporary table) write
     * @param string $fname Name of the calling function
     * @param int $flags Bit field of class QUERY_* constants
     * @return array An n-tuple of:
     *   - mixed|bool: An object, resource, or true on success; false on failure
     *   - string: The result of calling lastError()
     *   - int: The result of calling lastErrno()
     *      - bool: Whether a statement rollback error occurred
     *   - bool: Whether a disconnect *both* happened *and* was recoverable
     *   - bool: Whether a reconnection attempt was *both* made *and* succeeded
     * @throws DBUnexpectedError
     */
    private function executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags ) {
        $priorWritesPending = $this->writesOrCallbacksPending();
        if ( ( $flags & self::QUERY_IGNORE_DBO_TRX ) == 0 ) {
            $this->beginIfImplied( $sql, $fname );
        }
        // Keep track of whether the transaction has write queries pending
        if ( $isPermWrite ) {
            $this->lastWriteTime = microtime( true );
            if ( $this->trxLevel() && !$this->trxDoneWrites ) {
                $this->trxDoneWrites = true;
                $this->trxProfiler->transactionWritingIn(
                    $this->server, $this->getDomainID(), $this->trxShortId );
            }
        }
        $prefix = $this->topologyRole ? 'query-m: ' : 'query: ';
        $generalizedSql = new GeneralizedSql( $sql, $this->trxShortId, $prefix );
        $startTime = microtime( true );
        $ps = $this->profiler
            ? ( $this->profiler )( $generalizedSql->stringify() )
            : null;
        $this->affectedRowCount = null;
        $this->lastQuery = $sql;
        $ret = $this->doQuery( $commentedSql );
        $lastError = $this->lastError();
        $lastErrno = $this->lastErrno();
        $this->affectedRowCount = $this->affectedRows();
        unset( $ps ); // profile out (if set)
        $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
        $recoverableSR = false; // recoverable statement rollback?
        $recoverableCL = false; // recoverable connection loss?
        $reconnected = false; // reconnection both attempted and succeeded?
        if ( $ret !== false ) {
            $this->lastPing = $startTime;
            if ( $isPermWrite && $this->trxLevel() ) {
                $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
                $this->trxWriteCallers[] = $fname;
            }
        } elseif ( $this->wasConnectionError( $lastErrno ) ) {
            # Check if no meaningful session state was lost
            $recoverableCL = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
            # Update session state tracking and try to restore the connection
            $reconnected = $this->replaceLostConnection( __METHOD__ );
        } else {
            # Check if only the last query was rolled back
            $recoverableSR = $this->wasKnownStatementRollbackError();
        }
        if ( $sql === self::$PING_QUERY ) {
            $this->lastRoundTripEstimate = $queryRuntime;
        }
        $this->trxProfiler->recordQueryCompletion(
            $generalizedSql,
            $startTime,
            $isPermWrite,
            $isPermWrite ? $this->affectedRows() : $this->numRows( $ret )
        );
        // Avoid the overhead of logging calls unless debug mode is enabled
        if ( $this->getFlag( self::DBO_DEBUG ) ) {
            $this->queryLogger->debug(
                "{method} [{runtime}s] {db_host}: {sql}",
                [
                    'method' => $fname,
                    'db_host' => $this->getServer(),
                    'sql' => $sql,
                    'domain' => $this->getDomainID(),
                    'runtime' => round( $queryRuntime, 3 )
                ]
            );
        }
        return [ $ret, $lastError, $lastErrno, $recoverableSR, $recoverableCL, $reconnected ];
    }
    /**
     * Start an implicit transaction if DBO_TRX is enabled and no transaction is active
     *
     * @param string $sql
     * @param string $fname
     */
    private function beginIfImplied( $sql, $fname ) {
        if (
            !$this->trxLevel() &&
            $this->getFlag( self::DBO_TRX ) &&
            $this->isTransactableQuery( $sql )
        ) {
            $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
            $this->trxAutomatic = true;
        }
    }
    /**
     * Update the estimated run-time of a query, not counting large row lock times
     *
     * LoadBalancer can be set to rollback transactions that will create huge replication
     * lag. It bases this estimate off of pendingWriteQueryDuration(). Certain simple
     * queries, like inserting a row can take a long time due to row locking. This method
     * uses some simple heuristics to discount those cases.
     *
     * @param string $sql A SQL write query
     * @param float $runtime Total runtime, including RTT
     * @param int $affected Affected row count
     */
    private function updateTrxWriteQueryTime( $sql, $runtime, $affected ) {
        // Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
        $indicativeOfReplicaRuntime = true;
        if ( $runtime > self::$SLOW_WRITE_SEC ) {
            $verb = $this->getQueryVerb( $sql );
            // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
            if ( $verb === 'INSERT' ) {
                $indicativeOfReplicaRuntime = $this->affectedRows() > self::$SMALL_WRITE_ROWS;
            } elseif ( $verb === 'REPLACE' ) {
                $indicativeOfReplicaRuntime = $this->affectedRows() > self::$SMALL_WRITE_ROWS / 2;
            }
        }
        $this->trxWriteDuration += $runtime;
        $this->trxWriteQueryCount += 1;
        $this->trxWriteAffectedRows += $affected;
        if ( $indicativeOfReplicaRuntime ) {
            $this->trxWriteAdjDuration += $runtime;
            $this->trxWriteAdjQueryCount += 1;
        }
    }
    /**
     * Error out if the DB is not in a valid state for a query via query()
     *
     * @param string $sql
     * @param string $fname
     * @throws DBTransactionStateError
     */
    private function assertQueryIsCurrentlyAllowed( $sql, $fname ) {
        $verb = $this->getQueryVerb( $sql );
        if ( $verb === 'USE' ) {
            throw new DBUnexpectedError( $this, "Got USE query; use selectDomain() instead" );
        }
        if ( $verb === 'ROLLBACK' ) { // transaction/savepoint
            return;
        }
        if ( $this->trxStatus < self::STATUS_TRX_OK ) {
            throw new DBTransactionStateError(
                $this,
                "Cannot execute query from $fname while transaction status is ERROR",
                [],
                $this->trxStatusCause
            );
        } elseif ( $this->trxStatus === self::STATUS_TRX_OK && $this->trxStatusIgnoredCause ) {
            list( $iLastError, $iLastErrno, $iFname ) = $this->trxStatusIgnoredCause;
            call_user_func( $this->deprecationLogger,
                "Caller from $fname ignored an error originally raised from $iFname" .
                "[$iLastErrno$iLastError"
            );
            $this->trxStatusIgnoredCause = null;
        }
    }
    public function assertNoOpenTransactions() {
        if ( $this->explicitTrxActive() ) {
            throw new DBTransactionError(
                $this,
                "Explicit transaction still active. A caller may have caught an error. "
                . "Open transactions: " . $this->flatAtomicSectionList()
            );
        }
    }
    /**
     * Determine whether it is safe to retry queries after a database connection is lost
     *
     * @param string $sql SQL query
     * @param bool $priorWritesPending Whether there is a transaction open with
     *     possible write queries or transaction pre-commit/idle callbacks
     *     waiting on it to finish.
     * @return bool True if it is safe to retry the query, false otherwise
     */
    private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
        # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
        # Dropped connections also mean that named locks are automatically released.
        # Only allow error suppression in autocommit mode or when the lost transaction
        # didn't matter anyway (aside from DBO_TRX snapshot loss).
        if ( $this->sessionNamedLocks ) {
            return false; // possible critical section violation
        } elseif ( $this->sessionTempTables ) {
            return false; // tables might be queried latter
        } elseif ( $sql === 'COMMIT' ) {
            return !$priorWritesPending; // nothing written anyway? (T127428)
        } elseif ( $sql === 'ROLLBACK' ) {
            return true; // transaction lost...which is also what was requested :)
        } elseif ( $this->explicitTrxActive() ) {
            return false; // don't drop atomicity and explicit snapshots
        } elseif ( $priorWritesPending ) {
            return false; // prior writes lost from implicit transaction
        }
        return true;
    }
    /**
     * Clean things up after session (and thus transaction) loss before reconnect
     */
    private function handleSessionLossPreconnect() {
        // Clean up tracking of session-level things...
        // https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html
        // https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
        $this->sessionTempTables = [];
        $this->sessionDirtyTempTables = [];
        // https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
        // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
        $this->sessionNamedLocks = [];
        // Session loss implies transaction loss
        $oldTrxShortId = $this->consumeTrxShortId();
        $this->trxAtomicCounter = 0;
        $this->trxIdleCallbacks = []; // T67263; transaction already lost
        $this->trxPreCommitCallbacks = []; // T67263; transaction already lost
        // Clear additional subclass fields
        $this->doHandleSessionLossPreconnect();
        // @note: leave trxRecurringCallbacks in place
        if ( $this->trxDoneWrites ) {
            $this->trxProfiler->transactionWritingOut(
                $this->server,
                $this->getDomainID(),
                $oldTrxShortId,
                $this->pendingWriteQueryDuration( self::ESTIMATE_TOTAL ),
                $this->trxWriteAffectedRows
            );
        }
    }
    /**
     * Reset any additional subclass trx* and session* fields
     * @stable to override
     */
    protected function doHandleSessionLossPreconnect() {
        // no-op
    }
    /**
     * Clean things up after session (and thus transaction) loss after reconnect
     */
    private function handleSessionLossPostconnect() {
        try {
            // Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
            // If callback suppression is set then the array will remain unhandled.
            $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
        } catch ( Throwable $ex ) {
            // Already logged; move on...
        }
        try {
            // Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener()
            $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
        } catch ( Throwable $ex ) {
            // Already logged; move on...
        }
    }
    /**
     * Reset the transaction ID and return the old one
     *
     * @return string The old transaction ID or the empty string if there wasn't one
     */
    private function consumeTrxShortId() {
        $old = $this->trxShortId;
        $this->trxShortId = '';
        return $old;
    }
    /**
     * Checks whether the cause of the error is detected to be a timeout.
     *
     * It returns false by default, and not all engines support detecting this yet.
     * If this returns false, it will be treated as a generic query error.
     *
     * @stable to override
     * @param string $error Error text
     * @param int $errno Error number
     * @return bool
     */
    protected function wasQueryTimeout( $error, $errno ) {
        return false;
    }
    /**
     * Report a query error. Log the error, and if neither the object ignore
     * flag nor the $ignoreErrors flag is set, throw a DBQueryError.
     *
     * @param string $error
     * @param int $errno
     * @param string $sql
     * @param string $fname
     * @param bool $ignore
     * @throws DBQueryError
     */
    public function reportQueryError( $error, $errno, $sql, $fname, $ignore = false ) {
        if ( $ignore ) {
            $this->queryLogger->debug( "SQL ERROR (ignored): $error" );
        } else {
            throw $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
        }
    }
    /**
     * @param string $error
     * @param string|int $errno
     * @param string $sql
     * @param string $fname
     * @return DBError
     */
    private function getQueryExceptionAndLog( $error, $errno, $sql, $fname ) {
        // Information that instances of the same problem have in common should
        // not be normalized (T255202).
        $this->queryLogger->error(
            "Error $errno from $fname, {error} {sql1line} {db_server}",
            $this->getLogContext( [
                'method' => __METHOD__,
                'errno' => $errno,
                'error' => $error,
                'sql1line' => mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 ),
                'fname' => $fname,
                'exception' => new RuntimeException()
            ] )
        );
        return $this->getQueryException( $error, $errno, $sql, $fname );
    }
    /**
     * @param string $error
     * @param string|int $errno
     * @param string $sql
     * @param string $fname
     * @return DBError
     */
    private function getQueryException( $error, $errno, $sql, $fname ) {
        if ( $this->wasQueryTimeout( $error, $errno ) ) {
            return new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
        } elseif ( $this->wasConnectionError( $errno ) ) {
            return new DBQueryDisconnectedError( $this, $error, $errno, $sql, $fname );
        } else {
            return new DBQueryError( $this, $error, $errno, $sql, $fname );
        }
    }
    /**
     * @param string $error
     * @return DBConnectionError
     */
    final protected function newExceptionAfterConnectError( $error ) {
        // Connection was not fully initialized and is not safe for use
        $this->conn = null;
        $this->connLogger->error(
            "Error connecting to {db_server} as user {db_user}: {error}",
            $this->getLogContext( [
                'error' => $error,
                'exception' => new RuntimeException()
            ] )
        );
        return new DBConnectionError( $this, $error );
    }
    /**
     * @inheritDoc
     * @stable to override
     */
    public function freeResult( $res ) {
    }
    /**
     * @inheritDoc
     */
    public function newSelectQueryBuilder() {
        return new SelectQueryBuilder( $this );
    }
    public function selectField(
        $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
    ) {
        if ( $var === '*' ) { // sanity
            throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
        }
        $options = $this->normalizeOptions( $options );
        $options['LIMIT'] = 1;
        $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
        if ( $res === false ) {
            throw new DBUnexpectedError( $this, "Got false from select()" );
        }
        $row = $this->fetchRow( $res );
        if ( $row === false ) {
            return false;
        }
        return reset( $row );
    }
    public function selectFieldValues(
        $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
    ) {
        if ( $var === '*' ) { // sanity
            throw new DBUnexpectedError( $this, "Cannot use a * field" );
        } elseif ( !is_string( $var ) ) { // sanity
            throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
        }
        $options = $this->normalizeOptions( $options );
        $res = $this->select( $table, [ 'value' => $var ], $cond, $fname, $options, $join_conds );
        if ( $res === false ) {
            throw new DBUnexpectedError( $this, "Got false from select()" );
        }
        $values = [];
        foreach ( $res as $row ) {
            $values[] = $row->value;
       &nb