├── .gitignore
├── src
├── SqlException.php
├── SqlTransactionError.php
├── SqlConnectionException.php
├── SqlTransientResource.php
├── SqlTransactionIsolation.php
├── SqlStatement.php
├── SqlLink.php
├── SqlConnector.php
├── SqlQueryError.php
├── SqlTransactionIsolationLevel.php
├── SqlConnection.php
├── SqlResult.php
├── SqlConnectionPool.php
├── SqlExecutor.php
├── SqlTransaction.php
└── SqlConfig.php
├── .php-cs-fixer.dist.php
├── .php_cs
├── psalm.xml
├── test
├── SqlQueryErrorTest.php
└── SqlConfigTest.php
├── README.md
├── phpunit.xml.dist
├── composer.json
├── LICENSE
└── .github
└── workflows
└── ci.yml
/.gitignore:
--------------------------------------------------------------------------------
1 | .php_cs.cache
2 | .phpunit.result.cache
3 | .idea
4 | build
5 | composer.lock
6 | phpunit.xml
7 | vendor
8 | *.pid
9 |
--------------------------------------------------------------------------------
/src/SqlException.php:
--------------------------------------------------------------------------------
1 | getFinder()
5 | ->in(__DIR__ . '/src')
6 | ->in(__DIR__ . '/test');
7 |
8 | $config->setCacheFile(__DIR__ . '/.php_cs.cache');
9 |
10 | return $config;
11 |
--------------------------------------------------------------------------------
/.php_cs:
--------------------------------------------------------------------------------
1 | getFinder()->in(__DIR__);
5 |
6 | $cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__;
7 |
8 | $config->setCacheFile($cacheDir . '/.php_cs.cache');
9 |
10 | return $config;
11 |
--------------------------------------------------------------------------------
/src/SqlTransientResource.php:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/SqlLink.php:
--------------------------------------------------------------------------------
1 |
8 | * @template TTransaction of SqlTransaction
9 | *
10 | * @extends SqlExecutor
11 | */
12 | interface SqlLink extends SqlExecutor
13 | {
14 | /**
15 | * Starts a transaction, returning an object where all queries are executed on a single connection.
16 | *
17 | * @return TTransaction
18 | */
19 | public function beginTransaction(): SqlTransaction;
20 | }
21 |
--------------------------------------------------------------------------------
/test/SqlQueryErrorTest.php:
--------------------------------------------------------------------------------
1 | getQuery());
18 | self::assertStringStartsWith("Amp\Sql\SqlQueryError: error\nCurrent query was SELECT * FROM foo", (string) $error);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/SqlConnector.php:
--------------------------------------------------------------------------------
1 | query;
18 | }
19 |
20 | public function __toString(): string
21 | {
22 | if ($this->query === "") {
23 | return parent::__toString();
24 | }
25 |
26 | $msg = $this->message;
27 | $this->message .= "\nCurrent query was {$this->query}";
28 | $str = parent::__toString();
29 | $this->message = $msg;
30 | return $str;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 | test
18 |
19 |
20 |
21 |
22 | src
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/SqlTransactionIsolationLevel.php:
--------------------------------------------------------------------------------
1 | 'Uncommitted',
16 | self::Committed => 'Committed',
17 | self::Repeatable => 'Repeatable',
18 | self::Serializable => 'Serializable',
19 | };
20 | }
21 |
22 | public function toSql(): string
23 | {
24 | return match ($this) {
25 | self::Uncommitted => 'READ UNCOMMITTED',
26 | self::Committed => 'READ COMMITTED',
27 | self::Repeatable => 'REPEATABLE READ',
28 | self::Serializable => 'SERIALIZABLE',
29 | };
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/SqlConnection.php:
--------------------------------------------------------------------------------
1 |
9 | * @template TTransaction of SqlTransaction
10 | *
11 | * @extends SqlLink
12 | */
13 | interface SqlConnection extends SqlLink
14 | {
15 | /**
16 | * @return TConfig The configuration used to create this connection.
17 | */
18 | public function getConfig(): SqlConfig;
19 |
20 | /**
21 | * @return SqlTransactionIsolation Current transaction isolation used when beginning transactions on this connection.
22 | */
23 | public function getTransactionIsolation(): SqlTransactionIsolation;
24 |
25 | /**
26 | * Sets the transaction isolation level for transactions began on this link.
27 | *
28 | * @see SqlLink::beginTransaction()
29 | */
30 | public function setTransactionIsolation(SqlTransactionIsolation $isolation): void;
31 | }
32 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "amphp/sql",
3 | "description": "Asynchronous SQL client for Amp.",
4 | "keywords": [
5 | "database",
6 | "db",
7 | "sql",
8 | "asynchronous",
9 | "async"
10 | ],
11 | "homepage": "https://amphp.org",
12 | "license": "MIT",
13 | "require": {
14 | "php": ">=8.1",
15 | "amphp/amp": "^3"
16 | },
17 | "require-dev": {
18 | "amphp/php-cs-fixer-config": "^2",
19 | "phpunit/phpunit": "^9",
20 | "psalm/phar": "5.23"
21 | },
22 | "autoload": {
23 | "psr-4": {
24 | "Amp\\Sql\\": "src"
25 | }
26 | },
27 | "autoload-dev": {
28 | "psr-4": {
29 | "Amp\\Sql\\Test\\": "test"
30 | }
31 | },
32 | "scripts": {
33 | "check": [
34 | "@cs",
35 | "@test"
36 | ],
37 | "cs": "php-cs-fixer fix -v --diff --dry-run",
38 | "cs-fix": "php-cs-fixer fix -v --diff",
39 | "test": "phpunit --coverage-text"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018-2022 amphp (Aaron Piotrowski, Niklas Keller, and contributors)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/SqlResult.php:
--------------------------------------------------------------------------------
1 | >
8 | */
9 | interface SqlResult extends \Traversable
10 | {
11 | /**
12 | * Returns the next row in the result set or null if no rows remain. This method may be used as an alternative
13 | * to foreach iteration to obtain single rows from the result.
14 | *
15 | * @return array|null
16 | */
17 | public function fetchRow(): ?array;
18 |
19 | /**
20 | * Resolves with a new instance of Result if another result is available after this result. Resolves with null if
21 | * no further results are available.
22 | *
23 | * @return SqlResult|null
24 | */
25 | public function getNextResult(): ?self;
26 |
27 | /**
28 | * Returns the number of rows affected or returned by the query if applicable or null if the number of rows is
29 | * unknown or not applicable to the query.
30 | */
31 | public function getRowCount(): ?int;
32 |
33 | /**
34 | * Returns the number of columns returned by the query if applicable or null if the number of columns is
35 | * unknown or not applicable to the query.
36 | */
37 | public function getColumnCount(): ?int;
38 | }
39 |
--------------------------------------------------------------------------------
/src/SqlConnectionPool.php:
--------------------------------------------------------------------------------
1 |
9 | * @template TTransaction of SqlTransaction
10 | *
11 | * @extends SqlConnection
12 | */
13 | interface SqlConnectionPool extends SqlConnection
14 | {
15 | /**
16 | * Gets a single connection from the pool to run a set of queries against a single connection.
17 | * Generally a transaction should be used instead of this method.
18 | *
19 | * @return SqlConnection
20 | */
21 | public function extractConnection(): SqlConnection;
22 |
23 | /**
24 | * @return int Total number of active connections in the pool.
25 | */
26 | public function getConnectionCount(): int;
27 |
28 | /**
29 | * @return int Total number of idle connections in the pool.
30 | */
31 | public function getIdleConnectionCount(): int;
32 |
33 | /**
34 | * @return int Maximum number of connections this pool will create.
35 | */
36 | public function getConnectionLimit(): int;
37 |
38 | /**
39 | * @return int Number of seconds a connection may remain idle before it is automatically closed.
40 | */
41 | public function getIdleTimeout(): int;
42 | }
43 |
--------------------------------------------------------------------------------
/src/SqlExecutor.php:
--------------------------------------------------------------------------------
1 | |array $params Query parameters.
36 | *
37 | * @return TResult
38 | *
39 | * @throws SqlException If the operation fails due to unexpected condition.
40 | * @throws SqlConnectionException If the connection to the database is lost.
41 | * @throws SqlQueryError If the operation fails due to an error in the query (such as a syntax error).
42 | */
43 | public function execute(string $sql, array $params = []): SqlResult;
44 | }
45 |
--------------------------------------------------------------------------------
/src/SqlTransaction.php:
--------------------------------------------------------------------------------
1 |
8 | * @template TTransaction of SqlTransaction
9 | *
10 | * @extends SqlLink
11 | */
12 | interface SqlTransaction extends SqlLink
13 | {
14 | public function getIsolation(): SqlTransactionIsolation;
15 |
16 | /**
17 | * @return bool True if the transaction is active, false if it has been committed or rolled back.
18 | */
19 | public function isActive(): bool;
20 |
21 | /**
22 | * @return string|null Nested transaction identifier or null if a top-level transaction.
23 | */
24 | public function getSavepointIdentifier(): ?string;
25 |
26 | /**
27 | * Commits the transaction and makes it inactive.
28 | *
29 | * @throws SqlTransactionError If the transaction has been committed or rolled back.
30 | */
31 | public function commit(): void;
32 |
33 | /**
34 | * Rolls back the transaction and makes it inactive.
35 | *
36 | * @throws SqlTransactionError If the transaction has been committed or rolled back.
37 | */
38 | public function rollback(): void;
39 |
40 | /**
41 | * Attaches a callback which is invoked when the entire transaction is committed. If this transaction
42 | * is a nested transaction, the callback will not be invoked until the top-level transaction is committed.
43 | *
44 | * @param \Closure():void $onCommit
45 | */
46 | public function onCommit(\Closure $onCommit): void;
47 |
48 | /**
49 | * Attaches a callback which is invoked when the transaction is rolled back. If in a nested transaction, the
50 | * callbacks may be invoked when rolling back to a savepoint or if the entire transaction is rolled back,
51 | * regardless of if the savepoint was released prior.
52 | *
53 | * @param \Closure():void $onRollback
54 | */
55 | public function onRollback(\Closure $onRollback): void;
56 | }
57 |
--------------------------------------------------------------------------------
/test/SqlConfigTest.php:
--------------------------------------------------------------------------------
1 | createConfigFromString("host=localhost:5432 user=user database=test");
31 |
32 | self::assertSame("localhost", $config->getHost());
33 | self::assertSame(5432, $config->getPort());
34 | self::assertSame("user", $config->getUser());
35 | self::assertSame("", $config->getPassword());
36 | self::assertSame("test", $config->getDatabase());
37 | }
38 |
39 | public function testBasicSyntax(): void
40 | {
41 | $config = $this->createConfigFromString("host=localhost port=5432 user=user pass=test db=test");
42 |
43 | self::assertSame("localhost", $config->getHost());
44 | self::assertSame(5432, $config->getPort());
45 | self::assertSame("user", $config->getUser());
46 | self::assertSame("test", $config->getPassword());
47 | self::assertSame("test", $config->getDatabase());
48 | }
49 |
50 | public function testAlternativeSyntax(): void
51 | {
52 | $config = $this->createConfigFromString("host=localhost;port=3306;user=user;password=test;db=test");
53 |
54 | self::assertSame("localhost", $config->getHost());
55 | self::assertSame(3306, $config->getPort());
56 | self::assertSame("user", $config->getUser());
57 | self::assertSame("test", $config->getPassword());
58 | self::assertSame("test", $config->getDatabase());
59 | }
60 |
61 | public function testInvalidString(): void
62 | {
63 | $this->expectException(\ValueError::class);
64 | $this->expectExceptionMessage("Empty key name in connection string");
65 | $this->createConfigFromString("invalid =connection string");
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration
2 |
3 | on:
4 | - push
5 | - pull_request
6 |
7 | jobs:
8 | tests:
9 | strategy:
10 | matrix:
11 | include:
12 | - operating-system: 'ubuntu-latest'
13 | php-version: '8.1'
14 |
15 | - operating-system: 'ubuntu-latest'
16 | php-version: '8.2'
17 |
18 | - operating-system: 'ubuntu-latest'
19 | php-version: '8.3'
20 |
21 | - operating-system: 'ubuntu-latest'
22 | php-version: '8.4'
23 | static-analysis: none
24 | style-fix: none
25 |
26 | - operating-system: 'windows-latest'
27 | php-version: '8.3'
28 | job-description: 'on Windows'
29 |
30 | - operating-system: 'macos-latest'
31 | php-version: '8.3'
32 | job-description: 'on macOS'
33 |
34 | name: PHP ${{ matrix.php-version }} ${{ matrix.job-description }}
35 |
36 | runs-on: ${{ matrix.operating-system }}
37 |
38 | steps:
39 | - name: Set git to use LF
40 | run: |
41 | git config --global core.autocrlf false
42 | git config --global core.eol lf
43 |
44 | - name: Checkout code
45 | uses: actions/checkout@v2
46 |
47 | - name: Setup PHP
48 | uses: shivammathur/setup-php@v2
49 | with:
50 | php-version: ${{ matrix.php-version }}
51 |
52 | - name: Get Composer cache directory
53 | id: composer-cache
54 | run: echo "::set-output name=dir::$(composer config cache-dir)"
55 |
56 | - name: Cache dependencies
57 | uses: actions/cache@v2
58 | with:
59 | path: ${{ steps.composer-cache.outputs.dir }}
60 | key: composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }}-${{ matrix.composer-flags }}
61 | restore-keys: |
62 | composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }}-
63 | composer-${{ runner.os }}-${{ matrix.php-version }}-
64 | composer-${{ runner.os }}-
65 | composer-
66 |
67 | - name: Install dependencies
68 | uses: nick-invision/retry@v2
69 | with:
70 | timeout_minutes: 5
71 | max_attempts: 5
72 | retry_wait_seconds: 30
73 | command: |
74 | composer update --optimize-autoloader --no-interaction --no-progress ${{ matrix.composer-flags }}
75 | composer info -D
76 |
77 | - name: Run tests
78 | run: vendor/bin/phpunit ${{ matrix.phpunit-flags }}
79 |
80 | - name: Run static analysis
81 | run: vendor/bin/psalm.phar
82 | if: matrix.static-analysis != 'none'
83 |
84 | - name: Run style fixer
85 | env:
86 | PHP_CS_FIXER_IGNORE_ENV: 1
87 | run: vendor/bin/php-cs-fixer --diff --dry-run -v fix
88 | if: runner.os != 'Windows' && matrix.style-fix != 'none'
89 |
--------------------------------------------------------------------------------
/src/SqlConfig.php:
--------------------------------------------------------------------------------
1 | 'host',
9 | 'username' => 'user',
10 | 'pass' => 'password',
11 | 'database' => 'db',
12 | 'dbname' => 'db',
13 | ];
14 |
15 | private string $host;
16 |
17 | private int $port;
18 |
19 | private ?string $user;
20 |
21 | private ?string $password;
22 |
23 | private ?string $database;
24 |
25 | /**
26 | * Parses a connection string into an array of keys and values given.
27 | *
28 | * @param string $connectionString Connection string, e.g., "hostname=localhost username=sql password=default"
29 | * @param array $keymap Map of alternative key names to canonical key names.
30 | *
31 | * @return array
32 | */
33 | protected static function parseConnectionString(string $connectionString, array $keymap = self::KEY_MAP): array
34 | {
35 | $values = [];
36 |
37 | $params = \explode(";", $connectionString);
38 |
39 | if (\count($params) === 1) { // Attempt to explode on a space if no ';' are found.
40 | $params = \explode(" ", $connectionString);
41 | }
42 |
43 | foreach ($params as $param) {
44 | /** @psalm-suppress PossiblyInvalidArgument */
45 | [$key, $value] = \array_map(\trim(...), \explode("=", $param, 2) + [1 => ""]);
46 | if ($key === '') {
47 | throw new \ValueError("Empty key name in connection string");
48 | }
49 |
50 | $values[$keymap[$key] ?? $key] = $value;
51 | }
52 |
53 | if (\preg_match('/^(?.+):(?\d{1,5})$/', $values["host"] ?? "", $matches)) {
54 | $values["host"] = $matches["host"];
55 | $values["port"] = $matches["port"];
56 | }
57 |
58 | return $values;
59 | }
60 |
61 | public function __construct(
62 | string $host,
63 | int $port,
64 | ?string $user = null,
65 | ?string $password = null,
66 | ?string $database = null
67 | ) {
68 | $this->host = $host;
69 | $this->port = $port;
70 | $this->user = $user;
71 | $this->password = $password;
72 | $this->database = $database;
73 | }
74 |
75 | final public function getHost(): string
76 | {
77 | return $this->host;
78 | }
79 |
80 | final public function withHost(string $host): static
81 | {
82 | $new = clone $this;
83 | $new->host = $host;
84 | return $new;
85 | }
86 |
87 | final public function getPort(): int
88 | {
89 | return $this->port;
90 | }
91 |
92 | final public function withPort(int $port): static
93 | {
94 | $new = clone $this;
95 | $new->port = $port;
96 | return $new;
97 | }
98 |
99 | final public function getUser(): ?string
100 | {
101 | return $this->user;
102 | }
103 |
104 | final public function withUser(?string $user = null): static
105 | {
106 | $new = clone $this;
107 | $new->user = $user;
108 | return $new;
109 | }
110 |
111 | final public function getPassword(): ?string
112 | {
113 | return $this->password;
114 | }
115 |
116 | final public function withPassword(?string $password = null): static
117 | {
118 | $new = clone $this;
119 | $new->password = $password;
120 | return $new;
121 | }
122 |
123 | final public function getDatabase(): ?string
124 | {
125 | return $this->database;
126 | }
127 |
128 | final public function withDatabase(?string $database = null): static
129 | {
130 | $new = clone $this;
131 | $new->database = $database;
132 | return $new;
133 | }
134 | }
135 |
--------------------------------------------------------------------------------