rfc:pdo_driver_specific_subclasses

PHP RFC: PDO driver specific sub-classes

Introduction

PDO is a generic database class. Some of the databases it supports have functionality that is specific to that database. For example, when connected to an SQLite database, the sqlite specific function PDO::sqliteCreateFunction is available.

Having methods exist on a class depending on how the program was compiled and/or what database it is connecting to is quite surprising. It would make the code be easier to reason about if there were subclasses of the PDO class, specific to each database that has specific functionality available.

Proposal

The proposal has two parts:

  • Add new subclasses of PDO with driver specific methods.
  • Add PDO::connect to be able to create the specific subclasses.

Add new subclasses of PDO

All of the PDO extensions that are part of the PHP src repository will have subclasses created for them.

Some of these subclasses will expose functionality that exists that is peculiar to that DB, and not present in the generic PDO class. e.g. PdoSqlite::createFunction() should be there rather than on the generic PDO class PDO::sqliteCreateFunction.

class PdoDblib extends PDO
{
 
}
class PdoFirebird extends PDO
{
 
}
class PdoMySql extends PDO
{
    public function getWarningCount(): int {}
}
class PdoOci extends PDO
{
 
}
class PdoOdbc extends PDO
{
 
}
class PdoPgsql extends PDO
{
    /**
     * @var int
     * @cname PDO_PGSQL_ATTR_DISABLE_PREPARES
     */
    public const ATTR_DISABLE_PREPARES = UNKNOWN;
 
    /**
     * @var int
     * @cname PGSQL_TRANSACTION_IDLE
     */
    public const TRANSACTION_IDLE = UNKNOWN;
 
    /**
     * @var int
     * @cname PGSQL_TRANSACTION_ACTIVE
     */
    public const TRANSACTION_ACTIVE = UNKNOWN;
 
    /**
     * @var int
     * @cname PGSQL_TRANSACTION_INTRANS
     */
    public const TRANSACTION_INTRANS = UNKNOWN;
 
    /**
     * @var int
     * @cname PGSQL_TRANSACTION_INERROR
     */
    public const TRANSACTION_INERROR = UNKNOWN;
 
    /**
     * @var int
     * @cname PGSQL_TRANSACTION_UNKNOWN
     */
    public const TRANSACTION_UNKNOWN = UNKNOWN;
 
    public function escapeIdentifier(string $input): string {}
 
    public function copyFromArray(string $tableName, array $rows, string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null): bool {}
 
    public function copyFromFile(string $tableName, string $filename, string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null): bool {}
 
    public function copyToArray(string $tableName, string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null): array|false {}
 
    public function copyToFile(string $tableName, string $filename, string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null): bool {}
 
    public function lobCreate(): string|false {}
 
    // Opens an existing large object stream.  Must be called inside a transaction.
    /** @return resource|false */
    public function lobOpen(string $oid, string $mode = "rb"){}
 
    public function lobUnlink(string $oid): bool {}
 
    public function getNotify(int $fetchMode = PDO::FETCH_USE_DEFAULT, int $timeoutMilliseconds = 0): array|false {}
 
    public function getPid(): int {}
}
class PdoSqlite extends PDO
{
    /**
     * @var int
     * @cname SQLITE_DETERMINISTIC
     */
    public const DETERMINISTIC = UNKNOWN;
 
    /**
     * @var int
     * @cname SQLITE_ATTR_OPEN_FLAGS
     */
    public const ATTR_OPEN_FLAGS = UNKNOWN;
 
    /**
     * @var int
     * @cname SQLITE_OPEN_READONLY
     */
    public const OPEN_READONLY = UNKNOWN;
 
    /**
     * @var int
     * @cname SQLITE_OPEN_READWRITE
     */
    public const OPEN_READWRITE = UNKNOWN;
 
    /**
     * @var int
     * @cname SQLITE_OPEN_CREATE
     */
    public const OPEN_CREATE = UNKNOWN;
 
    /**
     * @var int
     * @cname SQLITE_ATTR_READONLY_STATEMENT
     */
    public const ATTR_READONLY_STATEMENT = UNKNOWN;
 
    /**
     * @var int
     * @cname
     */
    public const ATTR_EXTENDED_RESULT_CODES = UNKNOWN;
 
    // Registers an aggregating User Defined Function for use in SQL statements
    public function createAggregate(
        string $name,
        callable $step,
        callable $finalize,
        int $numArgs = -1
    ): bool {}
 
    // Registers a User Defined Function for use as a collating function in SQL statements
    public function createCollation(string $name, callable $callback): bool {}
 
    public function createFunction(
        string $function_name,
        callable $callback,
        int $num_args = -1,
        int $flags = 0
    ): bool {}
 
// Whether SQLITE_OMIT_LOAD_EXTENSION is defined or not depends on how
// SQLite was compiled: https://www.sqlite.org/compile.html
#ifndef SQLITE_OMIT_LOAD_EXTENSION
    public function loadExtension(string $name): bool {}
#endif

    public function openBlob(
        string $table,
        string $column,
        int $rowid,
        ?string $dbname = "main", //null,
        int $flags = PdoSqlite::OPEN_READONLY
    ): mixed /* resource|false */ {}
}

Although there may be DB specific functionality that could be exposed for the other database types that PDO supports, this RFC does not intend to expose them.

Add a way of creating them through PDO static factory method

Add a static factory method to PDO called connect. During the connect process, the exact type of database being connected to will be checked, and if it is a DB that has a specific sub-class (i.e. all of those extension that ship with PHP core), return that sub-class instead of a generic PDO object

class PDO
{
    public static function connect(string $dsn [, string $username [, string $password [, array $options ]]]) {
 
        if (connecting to SQLite DB) {
            return new PdoSqlite(...);
        }
 
 
        return new PDO(...);
    }
}

PDO::connect will return the appropriate sub-class when connecting to specific type of DB.

Users will also be able to create the DB specific classes directly, through the appropriate constructor e.g.

$db = new PdoSqlite($dsn, $username, $password, $options);

The code will throw an exception if the type of database being connected to isn't of the correct type for the class.

Sqlite Extension directory ini setting

The Sqlite3 extension requires users to set an ini setting to allow SQLite extensions to be loaded, presumably from a desire to keep things 'secure'. However, as this appears to only be a vulnerability if someone is able to upload and execute PHP code on a server, in which case the box is completely compromised any way, it doesn't seem to be necessary.

Additionally, in the proposed PdoSqlite class this code:

sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1);

is used to temporarily enable extension loading, before loading the extension, which only enables it through the C api, whereas the Sqlite3 extension uses the code:

sqlite3_enable_load_extension(sqlite_handle, 1);

which affects both the C api and loading extensions through SQL code.

Backward Incompatible Changes

None known. It might be inconvenient for people who are extending PDO class directly, and would like to extend the specific types. They would need to write their own connect functions, that wrap and proxy those specific types.

Proposed PHP Version(s)

PHP 8.3

RFC Impact

To SAPIs

Describe the impact to CLI, Development web server, embedded PHP etc.

Open Issues

These are the known current issues.

Unaffected PHP Functionality

Everything not PDO

Frequently asked questions

if someone does 'new PDO(...)' will they now get back 'PdoPgsql'

No.

Future Scope

When to deprecate old function on PDO

The method PDO::sqliteCreateFunction and other driver specific methods should probably be deprecated and removed at 'some point'. Although this cleanup should happen, the position of this RFC is that it's very low priority, as that code has sat there for years not requiring much maintainance.

Removing the methods that magically exist or not depending on the specific driver created would save some complication in the PDO code. So although there's no benefit in userland for deprecating the existing magic methods, there may be a complexity saving in maintenance.

Choosing the version for them to be deprecated in can be left until the future.

Quoting identifiers

During the discussion, a point was raised that currently there is no “escape identifier” method on the base PDO class. Postgres at least has a specific method for escaping identifiers as the rules for escaping those are different from escaping values. It sounds valuable to add an escape method to the base PDO class, but that needs to be done by someone familiar with all of the database drivers.

SQLite constants

There are SQLite constants that are not currently exposed in the SQLite3 extension including:

  • SQLITE_DIRECTONLY
  • SQLITE_INNOCUOUS
  • SQLITE_SUBTYPE

I don't intend to add these as I don't understand them.

PdoSqlite aggregations, collations and functions

The code for these functions has been copied from the Sqlite3 extension. Although SQLite allows creating these with different flags to indicate what character set data should be in, the implementation is hard-coded to use the SQLITE_UTF8 flag.

It would be possible to expose, and allow users to set, the flags of SQLITE_UTF16, SQLITE_UTF16BE, SQLITE_UTF16LE, SQLITE_UTF16_ALIGNED, and SQLITE_UTF8, however what the API should look like for that is 'not obvious', aka I have no idea what it should be like, despite having thought about it for a few hours. So, I'm planning to leave it out of this RFC.

Proposed Voting Choices

Accept the RFC or not.

PDO driver specific subclasses
Real name Yes No
alcaeus (alcaeus)  
ashnazg (ashnazg)  
brzuchal (brzuchal)  
crell (crell)  
danack (danack)  
ehrhardt (ehrhardt)  
ericmann (ericmann)  
galvao (galvao)  
girgias (girgias)  
heiglandreas (heiglandreas)  
imsop (imsop)  
kalle (kalle)  
kocsismate (kocsismate)  
levim (levim)  
mauricio (mauricio)  
nicolasgrekas (nicolasgrekas)  
ocramius (ocramius)  
petk (petk)  
pollita (pollita)  
ramsey (ramsey)  
sergey (sergey)  
svpernova09 (svpernova09)  
weierophinney (weierophinney)  
Final result: 23 0
This poll has been closed.

Voting closes at 2023-07-17T17:00:00Z

Patches and Tests

Implementation

References

A proposal to add sqlite openblob functionality previously failed to pass. The discussion gave the impression that the sub-classing approach would be more acceptable.

Rejected Features

Keep this updated with features that were discussed on the mail lists.

rfc/pdo_driver_specific_subclasses.txt · Last modified: 2024/02/12 14:24 by derick