├── LICENSE ├── src ├── CallbackStatement.php ├── AbstractConnection.php ├── RdsDataConverter.php ├── RdsDataDriver.php ├── RdsDataParameterBag.php ├── RdsDataException.php ├── RdsDataResult.php ├── RdsDataStatement.php └── RdsDataConnection.php ├── composer.json └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Marco Pfeiffer 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/CallbackStatement.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 22 | } 23 | 24 | 25 | public function bindValue($param, $value, $type = ParameterType::STRING) 26 | { 27 | return false; 28 | } 29 | 30 | public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null) 31 | { 32 | return false; 33 | } 34 | 35 | public function errorCode() 36 | { 37 | return false; 38 | } 39 | 40 | public function errorInfo() 41 | { 42 | return []; 43 | } 44 | 45 | public function execute($params = null) 46 | { 47 | return ($this->callback)() ?? true; 48 | } 49 | 50 | public function rowCount() 51 | { 52 | return 0; 53 | } 54 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nemo64/dbal-rds-data", 3 | "description": "rds-data driver for doctrine dbal", 4 | "keywords": [ 5 | "doctrine", 6 | "dbal", 7 | "orm", 8 | "driver", 9 | "rds", 10 | "aurora", 11 | "serverless", 12 | "aws", 13 | "sdk", 14 | "data", 15 | "data-api", 16 | "symfony" 17 | ], 18 | "license": "MIT", 19 | "type": "library", 20 | "authors": [ 21 | { 22 | "name": "Marco Pfeiffer", 23 | "email": "git@marco.zone" 24 | } 25 | ], 26 | "require": { 27 | "php": "~7.2||~8.0", 28 | "aws/aws-sdk-php": "^3.98", 29 | "doctrine/dbal": "^2.7" 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "^8.5" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Nemo64\\DbalRdsData\\": "src" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Nemo64\\DbalRdsData\\Tests\\": "tests" 42 | } 43 | }, 44 | "scripts": { 45 | "test": "phpunit tests" 46 | }, 47 | "config": { 48 | "sort-packages": true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/AbstractConnection.php: -------------------------------------------------------------------------------- 1 | prepare($sql); 39 | $stmt->execute(); 40 | 41 | return $stmt; 42 | } 43 | 44 | /** 45 | * @inheritDoc 46 | * @see https://docs.aws.amazon.com/rdsdataservice/latest/APIReference/API_ExecuteStatement.html 47 | */ 48 | public function exec($statement): int 49 | { 50 | $stmt = $this->prepare($statement); 51 | $success = $stmt->execute(); 52 | 53 | $errorInfo = $stmt->errorInfo(); 54 | if (!empty($errorInfo)) { 55 | throw new \Exception(reset($errorInfo), $stmt->errorCode()); 56 | } 57 | 58 | if (!$success) { 59 | return 0; 60 | } 61 | 62 | return $stmt->rowCount(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/RdsDataConverter.php: -------------------------------------------------------------------------------- 1 | $value]; 26 | 27 | case ParameterType::BOOLEAN: 28 | return ['booleanValue' => (bool)$value]; 29 | 30 | // missing double because there is no official double type 31 | 32 | case ParameterType::NULL: 33 | return ['isNull' => true]; 34 | 35 | case ParameterType::INTEGER: 36 | return ['longValue' => (int)$value]; 37 | 38 | case ParameterType::STRING: 39 | return ['stringValue' => (string)$value]; 40 | } 41 | 42 | throw new \RuntimeException("Type $type is not implemented."); 43 | } 44 | 45 | /** 46 | * Results from rds are formatted in an array like this: 47 | * ['stringValue' => 'string'] 48 | * ['longValue' => 5] 49 | * ['isNull' => true] 50 | * 51 | * This method converts this to a normal array that you'd expect. 52 | * 53 | * @param array $json 54 | * 55 | * @return mixed 56 | */ 57 | public function convertToValue(array $json) 58 | { 59 | $key = key($json); 60 | $value = current($json); 61 | 62 | switch ($key) { 63 | case 'isNull': 64 | return null; 65 | 66 | case 'blobValue': 67 | return base64_decode($value); 68 | 69 | case 'arrayValue': 70 | throw new \RuntimeException("arrayValue is not implemented."); 71 | 72 | case 'structValue': 73 | return array_map([$this, 'convertToValue'], $value); 74 | 75 | // case 'booleanValue': 76 | // case 'longValue': 77 | // case 'stringValue': 78 | default: 79 | return $value; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/RdsDataDriver.php: -------------------------------------------------------------------------------- 1 | '2018-08-01', 21 | 'region' => $params['host'], 22 | 'http' => [ 23 | // all calls to the data-api will time out after 45 seconds 24 | // https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html 25 | 'timeout' => $driverOptions['timeout'] ?? 45, 26 | ], 27 | ]; 28 | 29 | if ($username !== null && $username !== 'root') { 30 | $options['credentials']['key'] = $username; 31 | } 32 | 33 | if ($password !== null) { 34 | $options['credentials']['secret'] = $password; 35 | } 36 | 37 | $connection = new RdsDataConnection( 38 | new RDSDataServiceClient($options), 39 | $driverOptions['resourceArn'], 40 | $driverOptions['secretArn'], 41 | $params['dbname'] ?? null 42 | ); 43 | 44 | $connection->setPauseRetries($driverOptions['pauseRetries'] ?? 0); 45 | $connection->setPauseRetryDelay($driverOptions['pauseRetryDelay'] ?? 10); 46 | 47 | return $connection; 48 | } 49 | 50 | /** 51 | * @inheritDoc 52 | */ 53 | public function getName(): string 54 | { 55 | return 'rds-data'; 56 | } 57 | 58 | public function convertException($message, DriverException $exception) 59 | { 60 | switch ($exception->getErrorCode()) { 61 | case '6000': 62 | return new DBALException\ConnectionException($message, $exception); 63 | 64 | default: 65 | return parent::convertException($message, $exception); 66 | } 67 | } 68 | 69 | public function getDatabase(Connection $conn) 70 | { 71 | $params = $conn->getParams(); 72 | if (isset($params['dbname'])) { 73 | return $params['dbname']; 74 | } 75 | 76 | $connection = $conn->getWrappedConnection(); 77 | if (!$connection instanceof RdsDataConnection) { 78 | return null; 79 | } 80 | 81 | return $connection->getDatabase(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/RdsDataParameterBag.php: -------------------------------------------------------------------------------- 1 | dataConverter = $dataConverter ?? new RdsDataConverter(); 33 | } 34 | 35 | /** 36 | * @inheritDoc 37 | */ 38 | public function bindValue($param, $value, $type = ParameterType::STRING) 39 | { 40 | // The difference between bindValue and bindParam is that bindParam takes a reference. 41 | // https://stackoverflow.com/questions/1179874/what-is-the-difference-between-bindparam-and-bindvalue 42 | // I decided not to support that for simplicity. 43 | // It might create issues with some implementations that rely on that fact. 44 | return $this->bindParam($param, $value, $type); 45 | } 46 | 47 | /** 48 | * @inheritDoc 49 | */ 50 | public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null): bool 51 | { 52 | if ($length !== null) { 53 | throw new \RuntimeException("length parameter not implemented."); 54 | } 55 | 56 | $this->parameters[$column] = [&$variable, $type]; 57 | return true; 58 | } 59 | 60 | /** 61 | * Because the variable is only "bound" and can therefore change, 62 | * I must convert the representation just before sending the query. 63 | * 64 | * @return array 65 | */ 66 | public function getParameters(): array 67 | { 68 | $result = []; 69 | 70 | foreach ($this->parameters as $column => $arguments) { 71 | $result[] = [ 72 | 'name' => (string)$column, 73 | 'value' => $this->dataConverter->convertToJson(...$arguments), 74 | ]; 75 | } 76 | 77 | return $result; 78 | } 79 | 80 | /** 81 | * The rds data api only supports named parameters but most dbal implementations heavily use numeric parameters. 82 | * 83 | * This method converts "?" into ":0" parameters. 84 | * 85 | * @param string $sql 86 | * 87 | * @return string 88 | */ 89 | public function prepareSqlStatement(string $sql): string 90 | { 91 | $numericParameters = array_filter(array_keys($this->parameters), 'is_int'); 92 | if (count($numericParameters) <= 0) { 93 | return $sql; 94 | } 95 | 96 | // it is valid to start numeric parameters 0 and 1 97 | $index = min($numericParameters); 98 | if ($index !== 0 && $index !== 1) { 99 | throw new \LogicException("Numeric parameters must start with 0 or 1."); 100 | } 101 | 102 | $createParameter = static function () use (&$index) { 103 | return ':' . $index++; 104 | }; 105 | 106 | $sql = preg_replace_callback(self::NUMERIC_PARAMETER_EXPRESSION, $createParameter, $sql); 107 | if (!is_string($sql)) { 108 | // snipped from https://www.php.net/manual/de/function.preg-last-error.php#124124 109 | $pregError = array_flip(array_filter(get_defined_constants(true)['pcre'], function ($value) { 110 | return substr($value, -6) === '_ERROR'; 111 | }, ARRAY_FILTER_USE_KEY))[preg_last_error()] ?? 'unknown error'; 112 | throw new \RuntimeException("sql param replacement failed: $pregError"); 113 | } 114 | 115 | return $sql; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/RdsDataException.php: -------------------------------------------------------------------------------- 1 | { 23 | * const paragraphs = item.getElementsByTagName('p'); 24 | * const codes = item.getElementsByTagName('code'); 25 | * if (!paragraphs[1]) { 26 | * return null; 27 | * } 28 | * 29 | * // these are the codes that are defined here: \Doctrine\DBAL\Driver\AbstractMySQLDriver::convertException 30 | * if (![1213,1205,1050,1051,1146,1216,1217,1451,1452,1701,1062,1557,1569,1586,1054,1166,1611,1052,1060,1110,1064,1149,1287,1341,1342,1343,1344,1382,1479,1541,1554,1626,1044,1045,1046,1049,1095,1142,1143,1227,1370,1429,2002,2005,1048,1121,1138,1171,1252,1263,1364,1566].includes(Number(codes[0].textContent))) { 31 | * return null; 32 | * } 33 | * 34 | * const message = paragraphs[1].textContent.trim() 35 | * .replace(/^Message:\s+/g, '') 36 | * .replace(/[\.\\\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:\-\#]/g, c => `\\${c}`) 37 | * .replace(/\s+/g, ' ') 38 | * .replace(/%\w+/g, '.*'); 39 | * return `(*:${codes[0].textContent})${message}` 40 | * }) 41 | * .filter(d => d !== null) 42 | * .map((v, i) => i > 0 ? `|${v}` : v) 43 | * .map(v => JSON.stringify(v)) 44 | * .join("\n . ") 45 | * 46 | * @see https://dev.mysql.com/doc/refman/5.6/en/server-error-reference.html 47 | * @see \Doctrine\DBAL\Driver\AbstractMySQLDriver::convertException 48 | */ 49 | private const EXPRESSION = "#^" 50 | . "((*:1044)Access denied for user '.*'@'.*' to database '.*'" 51 | . "|(*:1045)Access denied for user '.*'@'.*' \\(using password\\: .*\\)" 52 | . "|(*:1046)No database selected" 53 | . "|(*:1048)Column '.*' cannot be null" 54 | . "|(*:1049)Unknown database '.*'" 55 | . "|(*:1050)Table '.*' already exists" 56 | . "|(*:1051)Unknown table '.*'" 57 | . "|(*:1052)Column '.*' in .* is ambiguous" 58 | . "|(*:1054)Unknown column '.*' in '.*'" 59 | . "|(*:1060)Duplicate column name '.*'" 60 | . "|(*:1062)Duplicate entry '.*' for key .*" 61 | . "|(*:1064).* near '.*' at line .*" 62 | . "|(*:1095)You are not owner of thread .*" 63 | . "|(*:1110)Column '.*' specified twice" 64 | . "|(*:1121)Table handler doesn't support NULL in given index\\. Please change column '.*' to be NOT NULL or use another handler" 65 | . "|(*:1138)Invalid use of NULL value" 66 | . "|(*:1142).* command denied to user '.*'@'.*' for table '.*'" 67 | . "|(*:1143).* command denied to user '.*'@'.*' for column '.*' in table '.*'" 68 | . "|(*:1146)Table '.*\\..*' doesn't exist" 69 | . "|(*:1149)You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use" 70 | . "|(*:1166)Incorrect column name '.*'" 71 | . "|(*:1171)All parts of a PRIMARY KEY must be NOT NULL; if you need NULL in a key, use UNIQUE instead" 72 | . "|(*:1205)Lock wait timeout exceeded; try restarting transaction" 73 | . "|(*:1213)Deadlock found when trying to get lock; try restarting transaction" 74 | . "|(*:1216)Cannot add or update a child row\\: a foreign key constraint fails" 75 | . "|(*:1217)Cannot delete or update a parent row\\: a foreign key constraint fails" 76 | . "|(*:1227)Access denied; you need \\(at least one of\\) the .* privilege\\(s\\) for this operation" 77 | . "|(*:1252)All parts of a SPATIAL index must be NOT NULL" 78 | . "|(*:1263)Column set to default value; NULL supplied to NOT NULL column '.*' at row .*" 79 | . "|(*:1287)'.*' is deprecated and will be removed in a future release\\. Please use .* instead" 80 | . "|(*:1341)Malformed file type header in file '.*'" 81 | . "|(*:1342)Unexpected end of file while parsing comment '.*'" 82 | . "|(*:1343)Error while parsing parameter '.*' \\(line\\: '.*'\\)" 83 | . "|(*:1344)Unexpected end of file while skipping unknown parameter '.*'" 84 | . "|(*:1364)Field '.*' doesn't have a default value" 85 | . "|(*:1370).* command denied to user '.*'@'.*' for routine '.*'" 86 | . "|(*:1382)The '.*' syntax is reserved for purposes internal to the MySQL server" 87 | . "|(*:1429)Unable to connect to foreign data source\\: .*" 88 | . "|(*:1451)Cannot delete or update a parent row\\: a foreign key constraint fails \\(.*\\)" 89 | . "|(*:1452)Cannot add or update a child row\\: a foreign key constraint fails \\(.*\\)" 90 | . "|(*:1479)Syntax error\\: .* PARTITIONING requires definition of VALUES .* for each partition" 91 | . "|(*:1541)Failed to drop .*" 92 | . "|(*:1554)The syntax '.*' is deprecated and will be removed in MySQL .*\\. Please use .* instead" 93 | . "|(*:1557)Upholding foreign key constraints for table '.*', entry '.*', key .* would lead to a duplicate entry" 94 | . "|(*:1566)Not allowed to use NULL value in VALUES LESS THAN" 95 | . "|(*:1569)ALTER TABLE causes auto_increment resequencing, resulting in duplicate entry '.*' for key '.*'" 96 | . "|(*:1586)Duplicate entry '.*' for key '.*'" 97 | . "|(*:1611)Invalid column reference \\(.*\\) in LOAD DATA" 98 | . "|(*:1626)Error in parsing conflict function\\. Message\\: .*" 99 | . "|(*:1701)Cannot truncate a table referenced in a foreign key constraint \\(.*\\)" 100 | 101 | // this error is custom and specific to aurora serverless proxies 102 | // I decided to use 6xxx error codes for proxy errors since server errors are 1xxx and client errors 2xxx 103 | . "|(*:6000)Communications link failure.*" 104 | 105 | . ")$#s"; // note the PCRE_DOTALL modifier 106 | 107 | public static function interpretErrorMessage(string $message): self 108 | { 109 | if (preg_match(self::EXPRESSION, $message, $match)) { 110 | return new self($message, null, $match['MARK']); 111 | } 112 | 113 | return new self($message); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/RdsDataResult.php: -------------------------------------------------------------------------------- 1 | result = $result; 31 | $this->dataConverter = $dataConverter ?? new RdsDataConverter(); 32 | } 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null): bool 38 | { 39 | $this->fetchMode = func_get_args(); 40 | return true; 41 | } 42 | 43 | /** 44 | * @inheritDoc 45 | */ 46 | public function columnCount(): int 47 | { 48 | return count($this->result['columnMetadata']); 49 | } 50 | 51 | /** 52 | * @inheritDoc 53 | */ 54 | public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0) 55 | { 56 | if ($cursorOrientation !== \PDO::FETCH_ORI_NEXT) { 57 | throw new \RuntimeException("Cursor direction not implemented"); 58 | } 59 | 60 | $result = current($this->result['records']); 61 | if (!is_array($result)) { 62 | return $result; 63 | } 64 | 65 | $fetchMode = $fetchMode !== null ? [$fetchMode, null] : $this->fetchMode; 66 | $result = $this->convertResultToFetchMode($result, ...$fetchMode); 67 | 68 | // advance the pointer and return 69 | next($this->result['records']); 70 | return $result; 71 | } 72 | 73 | /** 74 | * @inheritDoc 75 | */ 76 | public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null) 77 | { 78 | $previousFetchMode = $this->fetchMode; 79 | if ($fetchMode !== null) { 80 | $this->setFetchMode($fetchMode, $fetchArgument, $ctorArgs); 81 | } 82 | 83 | $result = iterator_to_array($this); 84 | $this->setFetchMode(...$previousFetchMode); 85 | 86 | return $result; 87 | } 88 | 89 | /** 90 | * @inheritDoc 91 | */ 92 | public function fetchColumn($columnIndex = 0) 93 | { 94 | $row = $this->fetch(FetchMode::NUMERIC); 95 | if (!is_array($row)) { 96 | return false; 97 | } 98 | 99 | return $row[$columnIndex] ?? false; 100 | } 101 | 102 | /** 103 | * @return \Iterator 104 | */ 105 | public function getIterator(): \Iterator 106 | { 107 | while (($row = $this->fetch()) !== false) { 108 | yield $row; 109 | } 110 | } 111 | 112 | /** 113 | * @inheritDoc 114 | */ 115 | public function closeCursor(): bool 116 | { 117 | if (isset($this->result['records'])) { 118 | $this->result['records'] = null; 119 | } 120 | 121 | return true; 122 | } 123 | 124 | /** 125 | * @see \Doctrine\DBAL\Driver\Statement::rowCount 126 | */ 127 | public function rowCount(): int 128 | { 129 | if (isset($this->result['numberOfRecordsUpdated'])) { 130 | return $this->result['numberOfRecordsUpdated']; 131 | } 132 | 133 | if (isset($this->result['records'])) { 134 | return count($this->result['records']); 135 | } 136 | 137 | return 0; 138 | } 139 | 140 | /** 141 | * @param array $result 142 | * @param int $fetchMode 143 | * @param mixed $fetchArgument 144 | * @param null|array $ctorArgs 145 | * 146 | * @return array|object 147 | * @throws RdsDataException 148 | */ 149 | private function convertResultToFetchMode(array $result, int $fetchMode, $fetchArgument = null, $ctorArgs = null) 150 | { 151 | $numResult = array_map([$this->dataConverter, 'convertToValue'], $result); 152 | 153 | switch ($fetchMode) { 154 | case FetchMode::NUMERIC: 155 | return $numResult; 156 | 157 | case FetchMode::ASSOCIATIVE: 158 | $columnNames = array_column($this->result['columnMetadata'], 'label'); 159 | return array_combine($columnNames, $numResult); 160 | 161 | case FetchMode::MIXED: 162 | $columnNames = array_column($this->result['columnMetadata'], 'label'); 163 | return $numResult + array_combine($columnNames, $numResult); 164 | 165 | case FetchMode::STANDARD_OBJECT: 166 | $columnNames = array_column($this->result['columnMetadata'], 'label'); 167 | return (object)array_combine($columnNames, $numResult); 168 | 169 | case FetchMode::COLUMN: 170 | return $numResult[$fetchArgument ?? 0]; 171 | 172 | case FetchMode::CUSTOM_OBJECT: 173 | try { 174 | $class = new \ReflectionClass($fetchArgument); 175 | $result = $class->newInstanceWithoutConstructor(); 176 | 177 | self::mapProperties($class, $result, $this->result['columnMetadata'], $numResult); 178 | 179 | $constructor = $class->getConstructor(); 180 | if ($constructor !== null) { 181 | $constructor->invokeArgs($result, (array)$ctorArgs); 182 | } 183 | 184 | return $result; 185 | } catch (\ReflectionException $e) { 186 | throw new RdsDataException("could not fetch as class '$fetchArgument': {$e->getMessage()}", 0, $e); 187 | } 188 | 189 | default: 190 | throw new \RuntimeException("Fetch mode $fetchMode not supported"); 191 | } 192 | } 193 | 194 | private static function mapProperties(\ReflectionClass $class, $result, array $metadata, array $numResult) 195 | { 196 | foreach ($metadata as $columnIndex => ['label' => $columnName]) { 197 | if ($class->hasProperty($columnName)) { 198 | $property = $class->getProperty($columnName); 199 | $property->setAccessible(true); 200 | $property->setValue($result, $numResult[$columnIndex]); 201 | continue; 202 | } 203 | 204 | $result->{$columnName} = $numResult[$columnIndex]; 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/RdsDataStatement.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 61 | $this->dataConverter = $dataConverter ?? new RdsDataConverter(); 62 | $this->parameterBag = new RdsDataParameterBag($this->dataConverter); 63 | $this->sql = $sql; 64 | } 65 | 66 | /** 67 | * @inheritDoc 68 | */ 69 | public function closeCursor(): bool 70 | { 71 | // there is not really a cursor but I can free the memory the records are taking up. 72 | $this->result = null; 73 | return true; 74 | } 75 | 76 | /** 77 | * @inheritDoc 78 | */ 79 | public function columnCount(): int 80 | { 81 | return $this->result->columnCount(); 82 | } 83 | 84 | /** 85 | * @inheritDoc 86 | */ 87 | public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null): bool 88 | { 89 | $this->fetchMode = func_get_args(); 90 | 91 | if ($this->result !== null) { 92 | $this->result->setFetchMode(...$this->fetchMode); 93 | } 94 | 95 | return true; 96 | } 97 | 98 | /** 99 | * @inheritDoc 100 | */ 101 | public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null) 102 | { 103 | return $this->result->fetchAll($fetchMode, $fetchArgument, $ctorArgs); 104 | } 105 | 106 | /** 107 | * @inheritDoc 108 | */ 109 | public function fetchColumn($columnIndex = 0) 110 | { 111 | return $this->result->fetchColumn($columnIndex); 112 | } 113 | 114 | /** 115 | * @inheritDoc 116 | */ 117 | public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0) 118 | { 119 | return $this->result->fetch($fetchMode, $cursorOrientation, $cursorOffset); 120 | } 121 | 122 | /** 123 | * @inheritDoc 124 | */ 125 | public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null): bool 126 | { 127 | return $this->parameterBag->bindParam($column, $variable, $type, $length); 128 | } 129 | 130 | /** 131 | * @inheritDoc 132 | */ 133 | public function bindValue($param, $value, $type = ParameterType::STRING) 134 | { 135 | return $this->parameterBag->bindValue($param, $value, $type); 136 | } 137 | 138 | /** 139 | * Returns the sql that can be used in a query. 140 | * 141 | * There is one big modification needed: 142 | * Doctrine polyfills named parameters to numbered parameters. 143 | * The rds-data api _only_ supports named parameters. 144 | * 145 | * But numbered parameters aren't straight forward too. 146 | * Some implementations start the numbers with 1 and others with 0. 147 | * 148 | * @return string 149 | */ 150 | private function getSql(): string 151 | { 152 | return $this->parameterBag->prepareSqlStatement($this->sql); 153 | } 154 | 155 | /** 156 | * @inheritDoc 157 | */ 158 | public function errorCode() 159 | { 160 | // TODO: Implement errorCode() method. 161 | return false; 162 | } 163 | 164 | /** 165 | * @inheritDoc 166 | */ 167 | public function errorInfo() 168 | { 169 | // TODO: Implement errorInfo() method. 170 | return []; 171 | } 172 | 173 | /** 174 | * @inheritDoc 175 | * @throws RdsDataException 176 | */ 177 | public function execute($params = null): bool 178 | { 179 | if (is_iterable($params)) { 180 | foreach ($params as $paramKey => $paramValue) { 181 | $this->bindValue($paramKey, $paramValue); 182 | } 183 | } 184 | 185 | $args = [ 186 | 'continueAfterTimeout' => preg_match(self::DDL_REGEX, $this->sql) > 0, 187 | 'database' => $this->connection->getDatabase(), 188 | 'includeResultMetadata' => true, 189 | 'parameters' => $this->parameterBag->getParameters(), 190 | 'resourceArn' => $this->connection->getResourceArn(), // REQUIRED 191 | 'resultSetOptions' => [ 192 | 'decimalReturnType' => 'STRING', 193 | ], 194 | // 'schema' => '', 195 | 'secretArn' => $this->connection->getSecretArn(), // REQUIRED 196 | 'sql' => $this->getSql(), // REQUIRED 197 | ]; 198 | 199 | $transactionId = $this->connection->getTransactionId(); 200 | if ($transactionId) { 201 | $args['transactionId'] = $transactionId; 202 | } 203 | 204 | $result = $this->connection->call('executeStatement', $args); 205 | 206 | if (!empty($result['generatedFields'])) { 207 | $generatedValue = $this->dataConverter->convertToValue(reset($result['generatedFields'])); 208 | $this->connection->setLastInsertId($generatedValue); 209 | } 210 | 211 | $this->result = new RdsDataResult($result, $this->dataConverter); 212 | $this->result->setFetchMode(...$this->fetchMode); 213 | return true; 214 | } 215 | 216 | /** 217 | * @inheritDoc 218 | */ 219 | public function rowCount(): int 220 | { 221 | return $this->result->rowCount(); 222 | } 223 | 224 | /** 225 | * @inheritDoc 226 | */ 227 | public function getIterator() 228 | { 229 | return $this->result->getIterator(); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/RdsDataConnection.php: -------------------------------------------------------------------------------- 1 | client = $client; 66 | $this->resourceArn = $resourceArn; 67 | $this->secretArn = $secretArn; 68 | $this->database = $database; 69 | $this->dataConverter = new RdsDataConverter(); 70 | } 71 | 72 | public function __destruct() 73 | { 74 | // Since this connection is actually connectionless, 75 | // I want to make sure that transactions aren't left to time out after a request. 76 | $this->rollBack(); 77 | } 78 | 79 | /** 80 | * @inheritDoc 81 | */ 82 | public function prepare($prepareString): Statement 83 | { 84 | // allow selecting a database by "use database;" statement 85 | if (preg_match('#^\s*use\s+(?:(\w+)|`([^`]+)`)\s*;?\s*$#i', $prepareString, $match)) { 86 | return new CallbackStatement(function () use ($match) { 87 | $this->setDatabase($match[1] ?: $match[2]); 88 | }); 89 | } 90 | 91 | $this->lastStatement = new RdsDataStatement($this, $prepareString, $this->dataConverter); 92 | return $this->lastStatement; 93 | } 94 | 95 | /** 96 | * @param string $id 97 | * 98 | * @internal should only be used by the statement class 99 | */ 100 | public function setLastInsertId(string $id): void 101 | { 102 | $this->lastInsertedId = $id; 103 | } 104 | 105 | /** 106 | * @inheritDoc 107 | */ 108 | public function lastInsertId($name = null): string 109 | { 110 | return $this->lastInsertedId; 111 | } 112 | 113 | /** 114 | * @inheritDoc 115 | * @see https://docs.aws.amazon.com/rdsdataservice/latest/APIReference/API_BeginTransaction.html 116 | * @throws RdsDataException 117 | */ 118 | public function beginTransaction(): bool 119 | { 120 | if ($this->transactionId !== null) { 121 | return false; 122 | } 123 | 124 | $args = [ 125 | 'database' => $this->database, 126 | 'resourceArn' => $this->resourceArn, 127 | 'secretArn' => $this->secretArn, 128 | ]; 129 | 130 | $response = $this->call('beginTransaction', $args); 131 | $this->transactionId = $response['transactionId']; 132 | return true; 133 | } 134 | 135 | /** 136 | * @inheritDoc 137 | * @see https://docs.aws.amazon.com/rdsdataservice/latest/APIReference/API_CommitTransaction.html 138 | * @throws RdsDataException 139 | */ 140 | public function commit(): bool 141 | { 142 | if ($this->transactionId === null) { 143 | return false; 144 | } 145 | 146 | $args = [ 147 | 'resourceArn' => $this->resourceArn, 148 | 'secretArn' => $this->secretArn, 149 | 'transactionId' => $this->transactionId, 150 | ]; 151 | 152 | $this->call('commitTransaction', $args); 153 | $this->transactionId = null; 154 | return true; 155 | } 156 | 157 | /** 158 | * @inheritDoc 159 | * @see https://docs.aws.amazon.com/rdsdataservice/latest/APIReference/API_RollbackTransaction.html 160 | * @throws RdsDataException 161 | */ 162 | public function rollBack(): bool 163 | { 164 | if ($this->transactionId === null) { 165 | return false; 166 | } 167 | 168 | $args = [ 169 | 'resourceArn' => $this->resourceArn, 170 | 'secretArn' => $this->secretArn, 171 | 'transactionId' => $this->transactionId, 172 | ]; 173 | 174 | $this->call('rollbackTransaction', $args); 175 | $this->transactionId = null; 176 | return true; 177 | } 178 | 179 | /** 180 | * @inheritDoc 181 | */ 182 | public function errorCode(): ?string 183 | { 184 | if ($this->lastStatement === null) { 185 | return null; 186 | } 187 | 188 | return $this->lastStatement->errorCode(); 189 | } 190 | 191 | /** 192 | * @inheritDoc 193 | */ 194 | public function errorInfo(): array 195 | { 196 | if ($this->lastStatement === null) { 197 | return []; 198 | } 199 | 200 | return $this->lastStatement->errorInfo(); 201 | } 202 | 203 | public function getClient(): RDSDataServiceClient 204 | { 205 | return $this->client; 206 | } 207 | 208 | public function getResourceArn(): string 209 | { 210 | return $this->resourceArn; 211 | } 212 | 213 | public function getSecretArn(): string 214 | { 215 | return $this->secretArn; 216 | } 217 | 218 | public function getDatabase(): ?string 219 | { 220 | return $this->database; 221 | } 222 | 223 | public function setDatabase(?string $database): void 224 | { 225 | $this->database = $database; 226 | } 227 | 228 | public function getTransactionId(): ?string 229 | { 230 | return $this->transactionId; 231 | } 232 | 233 | public function getPauseRetries(): int 234 | { 235 | return $this->pauseRetries; 236 | } 237 | 238 | public function setPauseRetries(int $pauseRetries): void 239 | { 240 | $this->pauseRetries = $pauseRetries; 241 | } 242 | 243 | public function getPauseRetryDelay(): int 244 | { 245 | return $this->pauseRetryDelay; 246 | } 247 | 248 | public function setPauseRetryDelay(int $pauseRetryDelay): void 249 | { 250 | $this->pauseRetryDelay = $pauseRetryDelay; 251 | } 252 | 253 | /** 254 | * Runs a rds data command and handles errors. 255 | * 256 | * @param string $command 257 | * @param array $args 258 | * @param int $retry 259 | * @return Result 260 | * @throws RdsDataException 261 | */ 262 | public function call(string $command, array $args, int $retry = 0): Result 263 | { 264 | try { 265 | return $this->client->__call($command, [$args]); 266 | } catch (RDSDataServiceException $exception) { 267 | if ($exception->getAwsErrorCode() !== 'BadRequestException') { 268 | throw $exception; 269 | } 270 | 271 | $interpretedException = RdsDataException::interpretErrorMessage($exception->getAwsErrorMessage()); 272 | if ($interpretedException->getErrorCode() === '6000' && $this->getPauseRetries() > $retry) { 273 | sleep($this->getPauseRetryDelay()); 274 | return $this->call($command, $args, $retry + 1); 275 | } 276 | 277 | throw $interpretedException; 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Packagist Version](https://img.shields.io/packagist/v/Nemo64/dbal-rds-data)](https://packagist.org/packages/nemo64/dbal-rds-data) 2 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/Nemo64/dbal-rds-data/Test?label=tests)](https://github.com/Nemo64/dbal-rds-data/actions?query=workflow%3ATest) 3 | [![Packagist License](https://img.shields.io/packagist/l/Nemo64/dbal-rds-data)](https://github.com/Nemo64/dbal-rds-data/blob/master/LICENSE) 4 | [![Packagist Downloads](https://img.shields.io/packagist/dm/Nemo64/dbal-rds-data)](https://packagist.org/packages/nemo64/dbal-rds-data) 5 | 6 | # doctrine driver for the Aurora Serverless rds data api 7 | 8 | This is a driver to use the aws [rds-data] api on projects 9 | that are using [dbal] for their database access. 10 | 11 | It emulates a MySQL connection including transactions. 12 | However: the driver does never establish a persistent connection. 13 | 14 | This is experimental. I implemented it in a symfony project 15 | with the doctrine orm and with this driver it worked fine. 16 | I tested the schema tools, migrations and transactions. 17 | 18 | At this moment, I wouldn't recommend using the rds-data api at all 19 | because of it's 1000 request per second limitation that they secretly added at the end of 2020. 20 | It is listed as "Data API requests per second" in your service quota list but not in the documentation. 21 | 22 | ## Why would you use it? 23 | 24 | - The data api makes it possible to use a database in an aws hosting environment 25 | without the need for VPC's which add complexity 26 | and cost money if you need internet access though NAT Gateways. 27 | - Your application does not need the database password in plain text. 28 | You just need access to the aws api which can be managed a lot better. 29 | (there are other ways to achieve the same but still, it is really easy with the data api) 30 | - There might be a performance benefit due to not needing to establish 31 | a direct database connection and automatic pool management. 32 | 33 | ## Why wouldn't you use it? 34 | 35 | - This implementation hasn't been battle tested for 20 years. Have a look into the 36 | [Implementation Details](#implementation-details) section and see if you are comfortable with them. 37 | - Performance while running many small queries is probably the biggest issue. 38 | The rds data api has (as of writing this) a not well documented limit of 1000 queries per second per account. 39 | If you have ever worked with the doctrine orm, then you know that that's not a lot, especially if you run unoptimized background jobs. 40 | The aws sdk will retry queries, which just means that everything will become slow. 41 | - The [rds-data] api has size restrictions in the [ExecuteStatement] call 42 | which might become a problem when your application grows although they don't seem to be enabled at this moment. 43 | - The [rds-data] api is [not available everywhere]. This limitation is slowly getting lifted though. 44 | - The [rds-data] api has some inherit limitations base on the fact that it is mostly stateless. 45 | The biggest problem is that you can't set (session) variables. 46 | This also means you can't [setTransactionIsolation] levels although that is an optional feature in dbal anyways. 47 | You can still use normal locking in transactions though. 48 | - The [rds-data] api is only available with [Aurora Serverless] and this library also limits you to MySQL mode. 49 | If you plan on using other databases then you can't use the rds-data api and this library (yet). 50 | Here are alternatives you might want to consider: 51 | - Aurora Serverless in Postgres mode (although this can probably very easily be added here, I'm open to pull requests) 52 | - Aurora Classic to get an [SLA] or to benefit from reserved instance pricing on predictable workloads 53 | - Aurora Global for better availability and all the benefits of Aurora Classic 54 | - or even normal RDS to save money or use engines that are not emulated by Aurora 55 | 56 | ## How to use it 57 | 58 | First you must store your database credentials as [a secret] including the username. 59 | Then make sure to correctly configure [access to your database] to use the secret and the database. 60 | If you create a iam user then there is a "AmazonRDSDataFullAccess" policy that can be used directly. 61 | 62 | If you use dbal directly than this is the way: 63 | 64 | ```php 65 | \Nemo64\DbalRdsData\RdsDataDriver::class, 68 | 'host' => 'eu-west-1', // the aws region 69 | 'user' => '[aws-api-key]', // optional if it is defined in the environment 70 | 'password' => '[aws-api-secret]', // optional if it is defined in the environment 71 | 'dbname' => 'mydb', 72 | 'driverOptions' => [ 73 | 'resourceArn' => 'arn:aws:rds:eu-west-1:012345678912:cluster:database-1np9t9hdbf4mk', 74 | 'secretArn' => 'arn:aws:secretsmanager:eu-west-1:012345678912:secret:db-password-tSo334', 75 | ] 76 | ); 77 | $conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams); 78 | ``` 79 | 80 | 81 | Or use the short url syntax which is easier to change using environment variables: 82 | 83 | ```php 84 | \Nemo64\DbalRdsData\RdsDataDriver::class, 87 | 'url' => '//eu-west-1/mydb' 88 | . '?driverOptions[resourceArn]=arn:aws:rds:eu-west-1:012345678912:cluster:database-1np9t9hdbf4mk' 89 | . '&driverOptions[secretArn]=arn:aws:secretsmanager:eu-west-1:012345678912:secret:db-password-tSo334' 90 | ); 91 | $conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams); 92 | ``` 93 | 94 | 95 | Since I developed in symfony project, I might as well add how to define the driver in symfony: 96 | 97 | ```yaml 98 | # doctrine.yaml 99 | doctrine: 100 | dbal: 101 | # the url can override the driver class 102 | # but I can't define this driver in the url which is why i made it the default 103 | # Doctrine\DBAL\DriverManager::parseDatabaseUrlScheme 104 | driver_class: Nemo64\DbalRdsData\RdsDataDriver 105 | url: '%env(resolve:DATABASE_URL)%' 106 | ``` 107 | ```sh 108 | # .env 109 | 110 | # you must not include a driver in the database url 111 | # in this case I also didn't include the aws tokens in the url 112 | DATABASE_URL=//eu-west-1/mydb?driverOptions[resourceArn]=arn&driverOptions[secretArn]=arn 113 | 114 | # the aws-sdk will pick those up 115 | # they are automatically configured in lambda and ec2 environments 116 | #AWS_ACCESS_KEY_ID=... 117 | #AWS_SECRET_ACCESS_KEY=... 118 | #AWS_SESSION_TOKEN=... 119 | ``` 120 | 121 | Other than the configuration it should work exactly like any other dbal connection. 122 | 123 | ### Driver options 124 | 125 | - `resourceArn` (string; required) this is the ARN of the database cluster. 126 | Go into the your [RDS-Management] > your database > Configuration and copy it from there. 127 | - `secretArn` (string; required) this is the ARN of the secret that stores the database password. 128 | Go into the [SecretManager] > your secret and use the Secret ARN. 129 | - `timeout` (int; default=45) The [timeout setting of guzzle]. Set to 0 for indefinite but that might not be useful. 130 | The rds-data api will block for a maximum of 45 seconds (see the [rds-data] docs). 131 | Schema update queries will automatically be executed with the `continueAfterTimeout` option. 132 | If you need to run long update queries than you might want to use the rds data client directly. 133 | Use `$dbalConnection->getWrappedConnection()->getClient()` to get the aws-sdk client. 134 | - `pauseRetries` (int; default=0) The amount of retries when the database is paused. 135 | If you set this, also consider setting `pauseRetryDelay` to ensure a somewhat correct retry time. 136 | - `pauseRetryDelay` (int; default=10) The amount of seconds to wait until another attempt is made 137 | if the last one failed due to the database being paused. 138 | As of writing this, Aurora takes anywhere from 30 seconds to 2 minutes to unpause. 139 | This is way too long to wait in most cases. 140 | Lambda will automatically retry events, so you are normally better of just letting the event fail. 141 | A user also usually won't wait a minute for a page to load, so you should present them with a proper error. 142 | See [Paused databases](#paused-databases) for a way to do that. 143 | 144 | ### CloudFormation 145 | 146 | Sure, here is a CloudFormation template to configure [Aurora Serverless] and a [Secret], 147 | putting both together and setting an environment variable with the needed information. 148 | 149 | This might be [serverless] flavoured but you should get the hang of it. 150 | 151 | ```yaml 152 | 153 | # [...] 154 | 155 | iamRoleStatements: 156 | # allow using the rds-data api 157 | # https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html#data-api.access 158 | - Effect: Allow 159 | Resource: '*' # it isn't supported to limit this 160 | Action: 161 | # https://docs.aws.amazon.com/IAM/latest/UserGuide/list_amazonrdsdataapi.html 162 | - rds-data:ExecuteStatement 163 | - rds-data:BeginTransaction 164 | - rds-data:CommitTransaction 165 | - rds-data:RollbackTransaction 166 | # this rds-data endpoint will use the same identity to get the secret 167 | # so you need to be able to read the password secret 168 | - Effect: Allow 169 | Resource: !Ref DatabaseSecret 170 | Action: 171 | # https://docs.aws.amazon.com/IAM/latest/UserGuide/list_awssecretsmanager.html 172 | - secretsmanager:GetSecretValue 173 | 174 | # [...] 175 | 176 | environment: 177 | DATABASE_URL: !Join 178 | - '' 179 | - - '//' # rds-data is set to default because custom drivers can't be named in a way that they can be used here 180 | - !Ref AWS::Region # the hostname is the region 181 | - '/mydb' 182 | - '?driverOptions[resourceArn]=' 183 | - !Join [':', ['arn:aws:rds', !Ref AWS::Region, !Ref AWS::AccountId, 'cluster', !Ref Database]] 184 | - '&driverOptions[secretArn]=' 185 | - !Ref DatabaseSecret 186 | 187 | # [...] 188 | 189 | # Make sure that there is a default VPC in your account. 190 | # https://console.aws.amazon.com/vpc/home#vpcs:isDefault=true 191 | # If not, click "Actions" > "Create Default VPC" 192 | # While your applications doesn't need it, the database must still be provisioned into a VPC so use the default. 193 | Database: 194 | Type: AWS::RDS::DBCluster # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbcluster.html 195 | Properties: 196 | Engine: aurora 197 | EngineMode: serverless 198 | EnableHttpEndpoint: true # https://stackoverflow.com/a/58759313 (not fully documented in every language yet) 199 | DatabaseName: 'mydb' 200 | MasterUsername: !Join ['', ['{{resolve:secretsmanager:', !Ref DatabaseSecret, ':SecretString:username}}']] 201 | MasterUserPassword: !Join ['', ['{{resolve:secretsmanager:', !Ref DatabaseSecret, ':SecretString:password}}']] 202 | BackupRetentionPeriod: 1 # day 203 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-dbcluster-scalingconfiguration.html 204 | ScalingConfiguration: {MinCapacity: 1, MaxCapactiy: 2, AutoPause: true} 205 | DatabaseSecret: 206 | Type: AWS::SecretsManager::Secret # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secret.html 207 | Properties: 208 | GenerateSecretString: 209 | SecretStringTemplate: '{"username": "admin"}' 210 | GenerateStringKey: "password" 211 | PasswordLength: 41 # max length of a mysql password 212 | ExcludeCharacters: '"@/\' 213 | DatabaseSecretAttachment: 214 | Type: AWS::SecretsManager::SecretTargetAttachment # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secrettargetattachment.html 215 | Properties: 216 | SecretId: !Ref DatabaseSecret 217 | TargetId: !Ref Database 218 | TargetType: AWS::RDS::DBCluster 219 | ``` 220 | 221 | I also wrote an article going more into detail on how to setup and share an Aurora Serverless between multiple stacks: 222 | [www.marco.zone/shared-aurora-serverless-using-cloudformation](https://www.marco.zone/shared-aurora-serverless-using-cloudformation) 223 | 224 | ## Implementation details 225 | 226 | ### Error handling 227 | 228 | The rds data api only provides error messages, not error codes. 229 | To correctly map those errors to dbal exceptions I use a huge regular expression. 230 | See: [`Nemo64\DbalRdsData\RdsDataException::EXPRESSION`](src/RdsDataException.php#L49). 231 | Since most of it is generated using the [mysql error documentation], 232 | it should be fine but it might not be 100% reliable. 233 | I asked for this in the [amazon developer forum] but haven't gotten a response yet. 234 | 235 | ### Paused databases 236 | 237 | If an [Aurora Serverless] is paused, you'll get this error message: 238 | > Communications link failure 239 | > 240 | > The last packet sent successfully to the server was 0 milliseconds ago. 241 | > The driver has not received any packets from the server. 242 | 243 | I mapped this error message to error code `6000` (server errors are 1xxx and client errors 2xxx). 244 | It'll also be converted to dbal's `Doctrine\DBAL\Exception\ConnectionException` 245 | which existing application might already handle gracefully. 246 | But the most important thing is that you can catch and handle code `6000` specifically 247 | to better tell your user that the database is paused and will probably be available soon. 248 | 249 | ### Parameters in prepared statements 250 | 251 | While [ExecuteStatement] does support parameters, it only supports named parameters. 252 | Question mark placeholders need to be emulated (which is funny because 253 | the `mysqli` driver only supports question mark placeholders and no named parameters). 254 | This is achieved by replacing `?` with `:1`, `:2` etc. 255 | The replacement algorithm will avoid replacing `?` within string literals but be aware that you 256 | shouldn't mix heavy string literals and question mark placeholders, just to be safe. 257 | 258 | ### String literals 259 | 260 | Every driver has some form of connection aware literal string escape function. 261 | But because the rds-data api is connectionless, it doesn't have such a method (except for parameters of course). 262 | To emulate the escape feature, a check for none ASCII characters is performed. 263 | If the string is pure ASCII it'll just pass though `addslashes` and gets quotes. 264 | If it has more exotic characters it'll be base64 encoded to prevent any chance of multibyte sql injection attacks. 265 | This should work transparently in most situations but you should definitely avoid using the `literal` function 266 | and instead use parameter binding whenever possible. 267 | 268 | 269 | [rds-data]: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html 270 | [dbal]: https://www.doctrine-project.org/projects/doctrine-dbal/en/2.10/index.html 271 | [ExecuteStatement]: https://docs.aws.amazon.com/rdsdataservice/latest/APIReference/API_ExecuteStatement.html 272 | [not available everywhere]: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html#data-api.regions 273 | [setTransactionIsolation]: https://www.doctrine-project.org/projects/doctrine-dbal/en/2.10/reference/transactions.html 274 | [Aurora Serverless]: https://aws.amazon.com/de/rds/aurora/serverless/ 275 | [SLA]: https://aws.amazon.com/de/rds/aurora/sla/ 276 | [access to your database]: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html#data-api.access 277 | [a secret]: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html#data-api.secrets 278 | [Secret]: https://aws.amazon.com/de/secrets-manager/ 279 | [serverless]: https://serverless.com/ 280 | [RDS-Management]: https://console.aws.amazon.com/rds/home 281 | [timeout setting of guzzle]: http://docs.guzzlephp.org/en/stable/request-options.html#timeout 282 | [mysql error documentation]: https://dev.mysql.com/doc/refman/5.6/en/server-error-reference.html 283 | [amazon developer forum]: https://forums.aws.amazon.com/thread.jspa?threadID=317595 284 | --------------------------------------------------------------------------------