From 745f30fae77b1e81383106c69e3e7611d3272f3e Mon Sep 17 00:00:00 2001 From: butschster Date: Tue, 12 Oct 2021 10:00:25 +0300 Subject: [PATCH 1/2] Oracle database support - Added oracle driver - Added tests for oracle driver - Added oracle-xe-11g-r2 docker container - Added docker container PHP with pdo_oci - Added bash script with pdo_oci installer --- src/Driver/Oracle/OracleCompiler.php | 145 +++++++++++++ src/Driver/Oracle/OracleDriver.php | 135 ++++++++++++ src/Driver/Oracle/OracleHandler.php | 105 ++++++++++ src/Driver/Oracle/Schema/OracleColumn.php | 157 ++++++++++++++ src/Driver/Oracle/Schema/OracleForeignKey.php | 23 +++ src/Driver/Oracle/Schema/OracleIndex.php | 25 +++ src/Driver/Oracle/Schema/OracleTable.php | 194 ++++++++++++++++++ .../Driver/Oracle/AlterColumnTest.php | 21 ++ .../Driver/Oracle/BuildersAccessTest.php | 21 ++ .../Driver/Oracle/ConsistencyTest.php | 21 ++ .../Driver/Oracle/CreateTableTest.php | 21 ++ tests/Database/Driver/Oracle/DatabaseTest.php | 21 ++ .../Driver/Oracle/DatetimeColumnTest.php | 21 ++ .../Driver/Oracle/DefaultValueTest.php | 21 ++ .../Driver/Oracle/DeleteQueryTest.php | 21 ++ .../Database/Driver/Oracle/ExceptionsTest.php | 21 ++ .../Driver/Oracle/ForeignKeysTest.php | 21 ++ tests/Database/Driver/Oracle/IndexesTest.php | 21 ++ .../Driver/Oracle/InsertQueryTest.php | 21 ++ .../Database/Driver/Oracle/IsolationTest.php | 21 ++ .../Driver/Oracle/NestedQueriesTest.php | 21 ++ .../Database/Driver/Oracle/ReflectorTest.php | 21 ++ .../Driver/Oracle/SelectQueryTest.php | 21 ++ .../Database/Driver/Oracle/StatementTest.php | 21 ++ tests/Database/Driver/Oracle/TableTest.php | 21 ++ .../Driver/Oracle/TransactionsTest.php | 21 ++ .../Driver/Oracle/UpdateQueryTest.php | 21 ++ tests/bootstrap.php | 7 + tests/docker-compose.yml | 69 ++++--- tests/generate.php | 4 + tests/install-oci8.sh | 27 +++ tests/php/Dockerfile | 40 ++++ 32 files changed, 1327 insertions(+), 24 deletions(-) create mode 100644 src/Driver/Oracle/OracleCompiler.php create mode 100644 src/Driver/Oracle/OracleDriver.php create mode 100644 src/Driver/Oracle/OracleHandler.php create mode 100644 src/Driver/Oracle/Schema/OracleColumn.php create mode 100644 src/Driver/Oracle/Schema/OracleForeignKey.php create mode 100644 src/Driver/Oracle/Schema/OracleIndex.php create mode 100644 src/Driver/Oracle/Schema/OracleTable.php create mode 100644 tests/Database/Driver/Oracle/AlterColumnTest.php create mode 100644 tests/Database/Driver/Oracle/BuildersAccessTest.php create mode 100644 tests/Database/Driver/Oracle/ConsistencyTest.php create mode 100644 tests/Database/Driver/Oracle/CreateTableTest.php create mode 100644 tests/Database/Driver/Oracle/DatabaseTest.php create mode 100644 tests/Database/Driver/Oracle/DatetimeColumnTest.php create mode 100644 tests/Database/Driver/Oracle/DefaultValueTest.php create mode 100644 tests/Database/Driver/Oracle/DeleteQueryTest.php create mode 100644 tests/Database/Driver/Oracle/ExceptionsTest.php create mode 100644 tests/Database/Driver/Oracle/ForeignKeysTest.php create mode 100644 tests/Database/Driver/Oracle/IndexesTest.php create mode 100644 tests/Database/Driver/Oracle/InsertQueryTest.php create mode 100644 tests/Database/Driver/Oracle/IsolationTest.php create mode 100644 tests/Database/Driver/Oracle/NestedQueriesTest.php create mode 100644 tests/Database/Driver/Oracle/ReflectorTest.php create mode 100644 tests/Database/Driver/Oracle/SelectQueryTest.php create mode 100644 tests/Database/Driver/Oracle/StatementTest.php create mode 100644 tests/Database/Driver/Oracle/TableTest.php create mode 100644 tests/Database/Driver/Oracle/TransactionsTest.php create mode 100644 tests/Database/Driver/Oracle/UpdateQueryTest.php create mode 100644 tests/install-oci8.sh create mode 100644 tests/php/Dockerfile diff --git a/src/Driver/Oracle/OracleCompiler.php b/src/Driver/Oracle/OracleCompiler.php new file mode 100644 index 00000000..0c6106d6 --- /dev/null +++ b/src/Driver/Oracle/OracleCompiler.php @@ -0,0 +1,145 @@ +name($params, $q, self::ROW_NUMBER) + ) + ); + + $tokens['limit'] = null; + $tokens['offset'] = null; + + return sprintf( + "SELECT * FROM (\n%s\n) AS [ORD_FALLBACK] %s", + $this->baseSelect($params, $q, $tokens), + $this->limit($params, $q, $limit, $offset, self::ROW_NUMBER) + ); + } + + /** + * @inheritDoc + * + * SELECT ... FOR UPDATE + * @see https://www.devtechinfo.com/oracle-plsql-select-update-statement/ + * CURSOR_cursor name IS select_statement FOR UPDATE [OF column_list] [NOWAIT]; + */ + private function baseSelect(QueryParameters $params, Quoter $q, array $tokens): string + { + // This statement(s) parts should be processed first to define set of table and column aliases + $tables = []; + foreach ($tokens['from'] as $table) { + $tables[] = $this->name($params, $q, $table, true); + } + + $joins = $this->joins($params, $q, $tokens['join']); + + return sprintf( + "SELECT%s %s\nFROM %s%s%s%s%s%s%s%s%s", + $this->optional(' ', $this->distinct($params, $q, $tokens['distinct'])), + $this->columns($params, $q, $tokens['columns']), + implode(', ', $tables), + $this->optional(' ', $joins, ' '), + $this->optional("\nWHERE", $this->where($params, $q, $tokens['where'])), + $this->optional("\nGROUP BY", $this->groupBy($params, $q, $tokens['groupBy']), ' '), + $this->optional("\nHAVING", $this->where($params, $q, $tokens['having'])), + $this->optional("\n", $this->unions($params, $q, $tokens['union'])), + $this->optional("\nORDER BY", $this->orderBy($params, $q, $tokens['orderBy'])), + $this->optional("\n", $this->limit($params, $q, $tokens['limit'], $tokens['offset'])), + $this->optional(' ', $tokens['forUpdate'] ? 'FOR UPDATE' : '', ' ') + ); + } + + /** + * {@inheritdoc} + * + * @param string $rowNumber Row used in a fallback sorting mechanism, ONLY when no ORDER BY + * specified. + * + * @link https://www.oracletutorial.com/oracle-basics/oracle-fetch/ + */ + protected function limit( + QueryParameters $params, + Quoter $q, + int $limit = null, + int $offset = null, + string $rowNumber = null + ): string { + if ($limit === null && $offset === null) { + return ''; + } + + //Modern SQLServer are easier to work with + if ($rowNumber === null) { + $statement = 'OFFSET ? ROWS '; + $params->push(new Parameter((int)$offset)); + + if ($limit !== null) { + $statement .= 'FETCH FIRST ? ROWS ONLY'; + $params->push(new Parameter($limit)); + } + + return trim($statement); + } + + $statement = "WHERE {$this->name($params, $q, $rowNumber)} "; + + //0 = row_number(1) + ++$offset; + + if ($limit !== null) { + $statement .= 'BETWEEN ? AND ?'; + $params->push(new Parameter((int)$offset)); + $params->push(new Parameter($offset + $limit - 1)); + } else { + $statement .= '>= ?'; + $params->push(new Parameter((int)$offset)); + } + + return $statement; + } + +} diff --git a/src/Driver/Oracle/OracleDriver.php b/src/Driver/Oracle/OracleDriver.php new file mode 100644 index 00000000..8ef28587 --- /dev/null +++ b/src/Driver/Oracle/OracleDriver.php @@ -0,0 +1,135 @@ + + */ + private array $searchSchemas = []; + + /** + * @param array $options + */ + public function __construct(array $options) + { + parent::__construct( + $options, + new OracleHandler(), + new OracleCompiler('""'), + QueryBuilder::defaultBuilder() + ); + + $this->defineSchemas($this->options); + } + + /** + * @inheritDoc + */ + public function getType(): string + { + return 'Oracle'; + } + + /** + * @inheritDoc + */ + public function getSource(): string + { + // remove "oci:" + return substr($this->getDSN(), 4); + } + + /** + * Schemas to search tables in + * + * @return string[] + */ + public function getSearchSchemas(): array + { + return $this->searchSchemas; + } + + /** + * Check if schemas are defined + * + * @return bool + */ + public function shouldUseDefinedSchemas(): bool + { + return $this->searchSchemas !== []; + } + + /** + * Parse the table name and extract the schema and table. + * + * @param string $name + * @return string[] + */ + public function parseSchemaAndTable(string $name): array + { + $schema = null; + $table = $name; + + if (str_contains($name, '.')) { + [$schema, $table] = explode('.', $name, 2); + + if ($schema === '$user') { + $schema = $this->options['username']; + } + } + + return [$schema ?? $this->searchSchemas[0], $table]; + } + + /** + * @inheritDoc + */ + protected function mapException(Throwable $exception, string $query): StatementException + { + if ((int)$exception->getCode() === 23000) { + return new StatementException\ConstrainException($exception, $query); + } + + return new StatementException($exception, $query); + } + + /** + * Define schemas from config + */ + private function defineSchemas(array $options): void + { + $options[self::OPT_AVAILABLE_SCHEMAS] = (array)($options[self::OPT_AVAILABLE_SCHEMAS] ?? []); + + $defaultSchema = static::PUBLIC_SCHEMA; + + $this->searchSchemas = array_values(array_unique( + [$defaultSchema, ...$options[self::OPT_AVAILABLE_SCHEMAS]] + )); + } +} diff --git a/src/Driver/Oracle/OracleHandler.php b/src/Driver/Oracle/OracleHandler.php new file mode 100644 index 00000000..e7f9dfc4 --- /dev/null +++ b/src/Driver/Oracle/OracleHandler.php @@ -0,0 +1,105 @@ +driver->getSearchSchemas()) . "')"; + + $tables = []; + foreach ($this->driver->query($query) as $row) { + if ($prefix !== '' && strpos($row['TABLE_NAME'], $prefix) !== 0) { + continue; + } + + $tables[] = $row['TABLESPACE_NAME'] . '.' . $row['TABLE_NAME']; + } + + return $tables; + } + + public function hasTable(string $table): bool + { + [$schema, $name] = $this->driver->parseSchemaAndTable($table); + + $query = "SELECT COUNT(TABLE_NAME) + FROM ALL_TABLES + WHERE TABLESPACE_NAME = ? + AND TABLE_NAME = ?"; + + return (bool)$this->driver->query($query, [$schema, $name])->fetchColumn(); + } + + public function getSchema(string $table, string $prefix = null): AbstractTable + { + return new OracleTable($this->driver, $table, $prefix ?? ''); + } + + public function eraseTable(AbstractTable $table): void + { + // TODO: Implement eraseTable() method. + } + + public function alterColumn(AbstractTable $table, AbstractColumn $initial, AbstractColumn $column): void + { + if (!$initial instanceof OracleColumn || !$column instanceof OracleColumn) { + throw new SchemaException('Oracle handler can work only with Oracle columns'); + } + + //Rename is separate operation + if ($column->getName() !== $initial->getName()) { + $this->renameColumn($table, $initial, $column); + + //This call is required to correctly built set of alter operations + $initial->setName($column->getName()); + } + + //Oracle columns should be altered using set of operations + $operations = $column->alterOperations($this->driver, $initial); + if (empty($operations)) { + return; + } + + //Oracle columns should be altered using set of operations + $query = sprintf( + 'ALTER TABLE %s %s', + $this->identify($table), + trim(implode(', ', $operations), ', ') + ); + + $this->run($query); + } + + /** + * @param AbstractTable $table + * @param AbstractColumn $initial + * @param AbstractColumn $column + */ + private function renameColumn( + AbstractTable $table, + AbstractColumn $initial, + AbstractColumn $column + ): void { + $statement = sprintf( + 'ALTER TABLE %s RENAME COLUMN %s TO %s', + $this->identify($table), + $this->identify($initial), + $this->identify($column) + ); + + $this->run($statement); + } +} diff --git a/src/Driver/Oracle/Schema/OracleColumn.php b/src/Driver/Oracle/Schema/OracleColumn.php new file mode 100644 index 00000000..04dc9e06 --- /dev/null +++ b/src/Driver/Oracle/Schema/OracleColumn.php @@ -0,0 +1,157 @@ + [ + 'type' => 'number', + 'size' => 11, + 'autoIncrement' => true, + 'nullable' => false, + ], + 'bigPrimary' => [ + 'type' => 'number', + 'size' => 20, + 'autoIncrement' => true, + 'nullable' => false, + ], + + //Enum type (mapped via method) + 'enum' => 'enum', + + //Logical types + 'boolean' => ['type' => 'tinyint', 'size' => 1], + + //Integer types (size can always be changed with size method), longInteger has method alias + //bigInteger + 'integer' => ['type' => 'number', 'size' => 11], + 'tinyInteger' => ['type' => 'number', 'size' => 4], + 'bigInteger' => ['type' => 'number', 'size' => 20], + + //String with specified length (mapped via method) + 'string' => ['type' => 'varchar2', 'size' => 255], + + //Generic types + 'text' => 'long', + 'tinyText' => 'char', + 'longText' => 'long', + + //Real types + 'double' => 'numeric', + 'float' => 'float', + + //Decimal type (mapped via method) + 'decimal' => 'decimal', + + //Date and Time types + 'datetime' => 'datetime', + 'date' => 'date', + 'time' => 'time', + 'timestamp' => ['type' => 'timestamp', 'defaultValue' => null], + + //Binary types + 'binary' => 'blob', + 'tinyBinary' => 'blob', + 'longBinary' => 'blob', + + //Additional types + 'json' => 'text', + 'uuid' => ['type' => 'varchar2', 'size' => 36], + ]; + + /** + * {@inheritdoc} + */ + protected $reverseMapping = [ + 'primary' => [['type' => 'int', 'autoIncrement' => true]], + 'bigPrimary' => ['serial', ['type' => 'bigint', 'autoIncrement' => true]], + 'enum' => ['enum'], + 'boolean' => ['bool', 'boolean', ['type' => 'tinyint', 'size' => 1]], + 'integer' => ['int', 'integer', 'smallint', 'mediumint'], + 'tinyInteger' => ['tinyint'], + 'bigInteger' => ['bigint'], + 'string' => ['varchar', 'char'], + 'text' => ['text', 'mediumtext'], + 'tinyText' => ['tinytext'], + 'longText' => ['longtext'], + 'double' => ['double'], + 'float' => ['float', 'real'], + 'decimal' => ['decimal'], + 'datetime' => ['datetime'], + 'date' => ['date'], + 'time' => ['time'], + 'timestamp' => ['timestamp'], + 'binary' => ['blob', 'binary', 'varbinary'], + 'tinyBinary' => ['tinyblob'], + 'longBinary' => ['longblob'], + ]; + + /** + * List of types forbids default value set. + * + * @var array + */ + protected $forbiddenDefaults = [ + 'text', + 'mediumtext', + 'tinytext', + 'longtext', + 'blog', + 'tinyblob', + 'longblob', + ]; + + /** + * Column is auto incremental. + * + * @var bool + */ + protected $autoIncrement = false; + + + + /** + * @param string $table Table name. + * @param array $schema + * @param DriverInterface $driver Postgres columns are bit more complex. + * @return OracleColumn + */ + public static function createInstance( + string $table, + array $schema, + DriverInterface $driver + ): self { + $column = new self($table, $schema['COLUMN_NAME'], $driver->getTimezone()); + + $column->type = $schema['DATA_TYPE']; + $column->defaultValue = $schema['DATA_DEFAULT']; + $column->nullable = $schema['NULLABLE'] === 'Y'; + + if (strpos($column->type, 'char') !== false && $schema['character_maximum_length']) { + $column->size = $schema['character_maximum_length']; + } + + if ($column->type === 'number') { + $column->precision = $schema['numeric_precision']; + $column->scale = $schema['numeric_scale']; + } + + return $column; + } +} diff --git a/src/Driver/Oracle/Schema/OracleForeignKey.php b/src/Driver/Oracle/Schema/OracleForeignKey.php new file mode 100644 index 00000000..5db4c63d --- /dev/null +++ b/src/Driver/Oracle/Schema/OracleForeignKey.php @@ -0,0 +1,23 @@ +columns = $schema['COLUMN_NAME']; + $reference->foreignTable = $schema['r_table_name']; + $reference->foreignKeys = $schema['r_column_name']; + + $reference->deleteRule = $schema['DELETE_RULE']; + + return $reference; + } +} diff --git a/src/Driver/Oracle/Schema/OracleIndex.php b/src/Driver/Oracle/Schema/OracleIndex.php new file mode 100644 index 00000000..0dcbc83e --- /dev/null +++ b/src/Driver/Oracle/Schema/OracleIndex.php @@ -0,0 +1,25 @@ +type = $definition['UNIQUENESS'] === 'UNIQUE' ? self::UNIQUE : self::NORMAL; + $index->columns[] = $definition['COLUMN_NAME']; + if ($definition['DESCEND'] !== 'ASC') { + $index->sort[$definition['COLUMN_NAME']] = 'DESC'; + } + } + + return $index; + } +} diff --git a/src/Driver/Oracle/Schema/OracleTable.php b/src/Driver/Oracle/Schema/OracleTable.php new file mode 100644 index 00000000..752fdd21 --- /dev/null +++ b/src/Driver/Oracle/Schema/OracleTable.php @@ -0,0 +1,194 @@ +removeSchemaFromTableName($this->getFullName()); + } + + protected function fetchColumns(): array + { + [$tableSchema, $tableName] = $this->driver->parseSchemaAndTable($this->getFullName()); + + $query = $this->driver->query( + 'select col.column_id, + col.column_name, + col.data_type, + col.data_length, + col.data_default, + col.data_precision, + col.data_scale, + col.nullable + from ALL_TAB_COLUMNS col + inner join sys.all_tables t on col.owner = t.owner and col.table_name = t.table_name + where col.owner = ? + and col.table_name = ? + order by col.column_id', + [$tableSchema, $tableName] + ); + + $result = []; + foreach ($query->fetchAll() as $schema) { + $result[] = OracleColumn::createInstance($tableSchema . '.' . $tableName, $schema, $this->driver); + } + + return $result; + } + + protected function fetchIndexes(): array + { + [$tableSchema, $tableName] = $this->driver->parseSchemaAndTable($this->getFullName()); + + $query = $this->driver->query( + 'select ALL_INDEXES.INDEX_NAME, ALL_IND_COLUMNS.COLUMN_NAME, ALL_IND_COLUMNS.DESCEND, + ALL_INDEXES.UNIQUENESS + from ALL_INDEXES + join ALL_IND_COLUMNS + on ALL_INDEXES.INDEX_NAME = ALL_IND_COLUMNS.INDEX_NAME + and ALL_INDEXES.OWNER = ALL_IND_COLUMNS.INDEX_OWNER + where ALL_INDEXES.owner = ? + and ALL_INDEXES.table_name = ? + and ALL_INDEXES.table_type = ? + order by ALL_IND_COLUMNS.COLUMN_POSITION', + [$tableSchema, $tableName, 'TABLE'] + ); + + //Gluing all index definitions together + $schemas = []; + foreach ($query as $index) { + $schemas[$index['INDEX_NAME']][] = $index; + } + + $result = []; + foreach ($schemas as $name => $index) { + $result[] = OracleIndex::createInstance($this->getFullName(), $name, $index); + } + + return $result; + } + + protected function fetchReferences(): array + { + [$tableSchema, $tableName] = $this->driver->parseSchemaAndTable($this->getFullName()); + + $query = $this->driver->query( + 'SELECT a.table_name, a.column_name, a.constraint_name, c.owner, c.delete_rule, + -- referenced pk + c.r_owner, c_pk.table_name r_table_name, c_pk.constraint_name r_pk, a_pk.column_name r_column_name + FROM all_cons_columns a + JOIN all_constraints c ON a.owner = c.owner AND a.constraint_name = c.constraint_name + JOIN all_constraints c_pk ON c.r_owner = c_pk.owner AND c.r_constraint_name = c_pk.constraint_name + JOIN all_cons_columns a_pk ON a_pk.owner = c_pk.owner AND a_pk.constraint_name = c_pk.constraint_name + where a.owner = ? + and a.table_name = ? + and c.constraint_type = ? + order by a.table_name, a.position', + [$tableSchema, $tableName, 'R'] + ); + + $result = []; + foreach ($query as $index) { + $result[] = OracleForeignKey::createInstance( + $this->getFullName(), + $this->getPrefix(), + $index + ); + } + + return $result; + } + + protected function fetchPrimaryKeys(): array + { + [$tableSchema, $tableName] = $this->driver->parseSchemaAndTable($this->getFullName()); + + $query = $this->driver->query( + 'select * + from ALL_CONSTRAINTS + join ALL_CONS_COLUMNS + on ALL_CONS_COLUMNS.CONSTRAINT_NAME = ALL_CONSTRAINTS.CONSTRAINT_NAME + and ALL_CONS_COLUMNS.OWNER = ALL_CONSTRAINTS.OWNER + where ALL_CONSTRAINTS.owner = ? + and ALL_CONSTRAINTS.table_name = ? + and ALL_CONSTRAINTS.CONSTRAINT_TYPE = ? + order by ALL_CONS_COLUMNS.TABLE_NAME, ALL_CONS_COLUMNS.POSITION', + [$tableSchema, $tableName, 'P'] + ); + + $primaryKeys = []; + foreach ($query as $index) { + $primaryKeys[] = $index['COLUMN_NAME']; + } + + return $primaryKeys; + } + + protected function createColumn(string $name): AbstractColumn + { + return new OracleColumn( + $this->getNormalizedTableName(), + $this->removeSchemaFromTableName($name), + $this->driver->getTimezone() + ); + } + + protected function createIndex(string $name): AbstractIndex + { + return new OracleIndex($this->getFullName(), $name); + } + + protected function createForeign(string $name): AbstractForeignKey + { + return new OracleForeignKey($this->getFullName(), $this->getPrefix(), $name); + } + + /** + * {@inheritdoc} + */ + protected function prefixTableName(string $name): string + { + [$schema, $name] = $this->driver->parseSchemaAndTable($name); + + return $schema . '.' . parent::prefixTableName($name); + } + + /** + * Get table name with schema. If table doesn't contain schema, schema will be added from config + * + * @return string + */ + protected function getNormalizedTableName(): string + { + [$schema, $name] = $this->driver->parseSchemaAndTable($this->getFullName()); + + return $schema . '.' . $name; + } + + /** + * Return table name without schema + * + * @param string $name + * @return string + */ + protected function removeSchemaFromTableName(string $name): string + { + if (strpos($name, '.') !== false) { + [, $name] = explode('.', $name, 2); + } + + return $name; + } +} diff --git a/tests/Database/Driver/Oracle/AlterColumnTest.php b/tests/Database/Driver/Oracle/AlterColumnTest.php new file mode 100644 index 00000000..5ff95859 --- /dev/null +++ b/tests/Database/Driver/Oracle/AlterColumnTest.php @@ -0,0 +1,21 @@ + 'SSpaSS__1', 'queryCache' => 100 ], + 'oracle' => [ + 'driver' => Database\Driver\Oracle\OracleDriver::class, + 'conn' => 'oci:host=oracle;port=1521;dbname=oracle/xe', + 'user' => 'SYSTEM', + 'pass' => 'oracle', + 'queryCache' => 100 + ], ]; $db = getenv('DB') ?: null; diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 5ee2054f..b4710945 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -1,32 +1,53 @@ version: "3" services: - sqlserver: - image: mcr.microsoft.com/mssql/server:2019-latest - restart: always - ports: - - "11433:1433" - environment: - SA_PASSWORD: "SSpaSS__1" - ACCEPT_EULA: "Y" +# sqlserver: +# image: mcr.microsoft.com/mssql/server:2019-latest +# restart: always +# ports: +# - "11433:1433" +# environment: +# SA_PASSWORD: "SSpaSS__1" +# ACCEPT_EULA: "Y" +# +# mysql_latest: +# image: mysql:latest +# restart: always +# command: --default-authentication-plugin=mysql_native_password +# ports: +# - "13306:3306" +# environment: +# MYSQL_DATABASE: "spiral" +# MYSQL_ROOT_PASSWORD: "root" +# MYSQL_ROOT_HOST: "%" +# +# postgres: +# image: postgres:12 +# restart: always +# ports: +# - "15432:5432" +# environment: +# POSTGRES_DB: "spiral" +# POSTGRES_USER: "postgres" +# POSTGRES_PASSWORD: "postgres" - mysql_latest: - image: mysql:latest - restart: always - command: --default-authentication-plugin=mysql_native_password - ports: - - "13306:3306" - environment: - MYSQL_DATABASE: "spiral" - MYSQL_ROOT_PASSWORD: "root" - MYSQL_ROOT_HOST: "%" + php: + build: "./php" + command: /app/vendor/bin/phpunit -c /app/phpunit.xml --filter Oracle /app/tests + volumes: + - ../:/app + depends_on: + - oracle - postgres: - image: postgres:12 + oracle: + image: wnameless/oracle-xe-11g-r2:latest restart: always ports: - - "15432:5432" + - 11521:1521 environment: - POSTGRES_DB: "spiral" - POSTGRES_USER: "postgres" - POSTGRES_PASSWORD: "postgres" + WEB_CONSOLE: "false" + # sid: xe + # service name: xe + # username: system + # password: oracle + # Password for SYS & SYSTEM oracle diff --git a/tests/generate.php b/tests/generate.php index a4cf47e7..134ac69b 100644 --- a/tests/generate.php +++ b/tests/generate.php @@ -35,6 +35,10 @@ 'sqlserver' => [ 'namespace' => 'Cycle\Database\Tests\Driver\SQLServer', 'directory' => __DIR__ . '/Database/Driver/SQLServer/' + ], + 'oracle' => [ + 'namespace' => 'Cycle\Database\Tests\Driver\Oracle', + 'directory' => __DIR__ . '/Database/Driver/Oracle/' ] ]; diff --git a/tests/install-oci8.sh b/tests/install-oci8.sh new file mode 100644 index 00000000..910fa0b2 --- /dev/null +++ b/tests/install-oci8.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -ex + +sudo apt-get install unzip +sudo rm -rf /opt/oracle +sudo mkdir -p /opt/oracle + +sudo curl -o /opt/oracle/basic.zip https://download.oracle.com/otn_software/linux/instantclient/213000/instantclient-basic-linux.x64-21.3.0.0.0.zip +sudo curl -o /opt/oracle/sdk.zip https://download.oracle.com/otn_software/linux/instantclient/213000/instantclient-sdk-linux.x64-21.3.0.0.0.zip + +sudo unzip /opt/oracle/basic.zip -d /opt/oracle +sudo unzip /opt/oracle/sdk.zip -d /opt/oracle + +sudo ln -s /opt/oracle/instantclient_21_3 /opt/oracle/instantclient + +sudo rm /opt/oracle/instantclient/libclntsh.so +sudo rm /opt/oracle/instantclient/libocci.so +sudo ln -s /opt/oracle/instantclient/libclntsh.so.12.1 /opt/oracle/instantclient/libclntsh.so +sudo ln -s /opt/oracle/instantclient/libocci.so.12.1 /opt/oracle/instantclient/libocci.so + +sudo sh -c 'echo /opt/oracle/instantclient > /etc/ld.so.conf.d/oracle-instantclient.conf' +sudo apt-get install -y make php-dev php-pear build-essential libaio1 +sudo pecl channel-update pecl.php.net +sudo pecl install oci8 + +# When you are prompted for the Instant Client location, enter the following: +# instantclient,/opt/oracle/instantclient \ No newline at end of file diff --git a/tests/php/Dockerfile b/tests/php/Dockerfile new file mode 100644 index 00000000..94a7e9e3 --- /dev/null +++ b/tests/php/Dockerfile @@ -0,0 +1,40 @@ +FROM php:8.0 + +RUN apt-get update +RUN apt-get install unzip + +RUN mkdir -p /opt/oracle + +RUN curl -o /opt/oracle/basic.zip https://download.oracle.com/otn_software/linux/instantclient/213000/instantclient-basic-linux.x64-21.3.0.0.0.zip +RUN curl -o /opt/oracle/sdk.zip https://download.oracle.com/otn_software/linux/instantclient/213000/instantclient-sdk-linux.x64-21.3.0.0.0.zip + +RUN unzip /opt/oracle/basic.zip -d /opt/oracle +RUN unzip /opt/oracle/sdk.zip -d /opt/oracle + +RUN ln -s /opt/oracle/instantclient_21_3 /opt/oracle/instantclient +RUN rm /opt/oracle/instantclient/libclntsh.so +RUN rm /opt/oracle/instantclient/libocci.so +RUN rm /opt/oracle/instantclient/libclntshcore.so + +RUN ln -s /opt/oracle/instantclient/libclntsh.so.12.1 /opt/oracle/instantclient/libclntsh.so +RUN ln -s /opt/oracle/instantclient/libclntshcore.so.12.1 /opt/oracle/instantclient/libclntshcore.so +RUN ln -s /opt/oracle/instantclient/libocci.so.12.1 /opt/oracle/instantclient/libocci.so + +RUN sh -c 'echo /opt/oracle/instantclient > /etc/ld.so.conf.d/oracle-instantclient.conf' + +RUN apt-get install -y make build-essential libaio1 +RUN pecl channel-update pecl.php.net + +RUN echo 'instantclient,/opt/oracle/instantclient' | pecl install oci8 && ldconfig + +ENV ORACLE_HOME=/opt/oracle/instantclient +ENV LD_LIBRARY_PATH /opt/oracle/instantclient + +RUN docker-php-ext-install pdo +RUN docker-php-ext-configure pdo_oci --with-pdo-oci=instantclient,$ORACLE_HOME,12.1 && docker-php-ext-install pdo_oci +RUN docker-php-ext-configure mysqli --with-mysqli=mysqlnd && docker-php-ext-install pdo_mysql + +RUN apt-get install -y libpq-dev +RUN docker-php-ext-configure pgsql --with-pgsql=/usr/local/pgsql && docker-php-ext-install pgsql pdo_pgsql + +RUN apt-get clean -y && rm -rf /var/lib/apt/lists/* \ No newline at end of file From d69550df0723ad1f838c3292c4ee6eea45d1c5ef Mon Sep 17 00:00:00 2001 From: butschster Date: Tue, 12 Oct 2021 11:05:34 +0300 Subject: [PATCH 2/2] Added oracle CI --- .github/workflows/ci-oraclel.yml | 82 ++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .github/workflows/ci-oraclel.yml diff --git a/.github/workflows/ci-oraclel.yml b/.github/workflows/ci-oraclel.yml new file mode 100644 index 00000000..7f3f405a --- /dev/null +++ b/.github/workflows/ci-oraclel.yml @@ -0,0 +1,82 @@ +on: + - pull_request + - push + +name: ci-oracle + +jobs: + tests: + name: PHP ${{ matrix.php-version }}-oracle-${{ matrix.pgsql-version }} + env: + extensions: curl, intl, pdo, pdo_oci, oci8 + key: cache-v1 + + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + php-version: + - "8.0" + - "8.1" + + services: + oci: + image: wnameless/oracle-xe-11g-r2:latest + ports: + - 1521:1521 + options: --name=oci + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup cache environment + id: cache-env + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php-version }} + extensions: ${{ env.extensions }} + key: ${{ env.key }} + + - name: Cache extensions + uses: actions/cache@v2 + with: + path: ${{ steps.cache-env.outputs.dir }} + key: ${{ steps.cache-env.outputs.key }} + restore-keys: ${{ steps.cache-env.outputs.key }} + + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: ${{ env.extensions }} + ini-values: date.timezone='UTC' + coverage: pcov + + - name: Determine composer cache directory + if: matrix.os == 'ubuntu-latest' + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v2 + with: + path: ${{ env.COMPOSER_CACHE_DIR }} + key: php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }} + restore-keys: | + php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- + + - name: Install dependencies with composer + if: matrix.php-version != '8.1' + run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Install dependencies with composer php 8.1 + if: matrix.php-version == '8.1' + run: composer update --ignore-platform-reqs --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run oracle tests with phpunit + env: + DB: oracle + run: vendor/bin/phpunit --group driver-oracle --colors=always