diff --git a/.gitattributes b/.gitattributes index 36c9cd12..162760ef 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ /.github export-ignore /tests export-ignore -/phpunit.xml export-ignore \ No newline at end of file +/phpunit.xml export-ignore +/vendor-bin export-ignore \ No newline at end of file diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 2b22254e..034e5fc2 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -17,6 +17,19 @@ jobs: ports: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + postgres: + image: postgres:15 + env: + POSTGRES_USER: root + POSTGRES_PASSWORD: password + POSTGRES_DB: testing + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U root" + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - name: Checkout @@ -53,14 +66,35 @@ jobs: - name: Install dependencies (composer.lock) run: composer install --prefer-dist --no-progress --no-suggest + - name: Run DB Migrate + run: | + composer bin phinx install + composer run db-migrate + env: + TESTS_DB_MYSQL_HOSTNAME: 127.0.0.1 + TESTS_DB_MYSQL_HOSTPORT: 3306 + TESTS_DB_MYSQL_USERNAME: root + TESTS_DB_MYSQL_PASSWORD: password + TESTS_DB_MYSQL_DATABASE: testing + TESTS_DB_PGSQL_HOSTNAME: 127.0.0.1 + TESTS_DB_PGSQL_HOSTPORT: 5432 + TESTS_DB_PGSQL_USERNAME: root + TESTS_DB_PGSQL_PASSWORD: password + TESTS_DB_PGSQL_DATABASE: testing + - name: Run test suite run: composer exec -- phpunit --coverage-clover=coverage.xml -v env: - TESTS_DB_MYSQL_HOST: 127.0.0.1 - TESTS_DB_MYSQL_PORT: 3306 + TESTS_DB_MYSQL_HOSTNAME: 127.0.0.1 + TESTS_DB_MYSQL_HOSTPORT: 3306 TESTS_DB_MYSQL_USERNAME: root TESTS_DB_MYSQL_PASSWORD: password TESTS_DB_MYSQL_DATABASE: testing + TESTS_DB_PGSQL_HOSTNAME: 127.0.0.1 + TESTS_DB_PGSQL_HOSTPORT: 5432 + TESTS_DB_PGSQL_USERNAME: root + TESTS_DB_PGSQL_PASSWORD: password + TESTS_DB_PGSQL_DATABASE: testing - name: Codecov uses: codecov/codecov-action@v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bea3c2b8..ef70cc0e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,6 +29,19 @@ jobs: ports: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + postgres: + image: postgres:15 + env: + POSTGRES_USER: root + POSTGRES_PASSWORD: password + POSTGRES_DB: testing + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U root" + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - name: Checkout @@ -65,11 +78,32 @@ jobs: - name: Install dependencies (composer.lock) run: composer install --prefer-dist --no-progress --no-suggest + - name: Run DB Migrate + run: | + composer bin phinx install + composer run db-migrate + env: + TESTS_DB_MYSQL_HOSTNAME: 127.0.0.1 + TESTS_DB_MYSQL_HOSTPORT: 3306 + TESTS_DB_MYSQL_USERNAME: root + TESTS_DB_MYSQL_PASSWORD: password + TESTS_DB_MYSQL_DATABASE: testing + TESTS_DB_PGSQL_HOSTNAME: 127.0.0.1 + TESTS_DB_PGSQL_HOSTPORT: 5432 + TESTS_DB_PGSQL_USERNAME: root + TESTS_DB_PGSQL_PASSWORD: password + TESTS_DB_PGSQL_DATABASE: testing + - name: Run test suite - run: composer exec -- phpunit + run: composer exec -- phpunit --testdox env: - TESTS_DB_MYSQL_HOST: 127.0.0.1 - TESTS_DB_MYSQL_PORT: 3306 + TESTS_DB_MYSQL_HOSTNAME: 127.0.0.1 + TESTS_DB_MYSQL_HOSTPORT: 3306 TESTS_DB_MYSQL_USERNAME: root TESTS_DB_MYSQL_PASSWORD: password TESTS_DB_MYSQL_DATABASE: testing + TESTS_DB_PGSQL_HOSTNAME: 127.0.0.1 + TESTS_DB_PGSQL_HOSTPORT: 5432 + TESTS_DB_PGSQL_USERNAME: root + TESTS_DB_PGSQL_PASSWORD: password + TESTS_DB_PGSQL_DATABASE: testing diff --git a/.gitignore b/.gitignore index 36e8429f..5a2d75d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ /vendor composer.phar composer.lock +.phpunit.result.cache .DS_Store Thumbs.db /.idea /.vscode -/.settings \ No newline at end of file +/.settings +/vendor-bin/**/vendor/ \ No newline at end of file diff --git a/composer.json b/composer.json index 16ee6048..00e5a714 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ "topthink/think-helper":"^3.1" }, "require-dev": { - "phpunit/phpunit": "^9.6|^10" + "bamarni/composer-bin-plugin": "^1.8", + "phpunit/phpunit": "^9.6" }, "autoload": { "psr-4": { @@ -40,6 +41,39 @@ "ext-mongodb": "provide mongodb support" }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } + }, + "scripts": { + "post-install-cmd": [ + "@composer bin phinx install --ansi" + ], + "phinx-update": [ + "@composer bin phinx update --ansi" + ], + "db-migrate": [ + "./vendor/bin/phinx migrate -e mysql -vvv", + "./vendor/bin/phinx migrate -e pgsql -vvv" + ], + "db-rollback": [ + "./vendor/bin/phinx rollback -f -e mysql", + "./vendor/bin/phinx rollback -f -e pgsql" + ], + "db-status": [ + "./vendor/bin/phinx status -e mysql", + "./vendor/bin/phinx status -e pgsql" + ], + "db-rebuild": [ + "@composer run db-rollback", + "@composer run db-migrate" + ] + }, + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } } } diff --git a/phinx.php b/phinx.php new file mode 100644 index 00000000..ff111ade --- /dev/null +++ b/phinx.php @@ -0,0 +1,31 @@ + [ + 'migrations' => '%%PHINX_CONFIG_DIR%%/tests/db/migrations', + 'seeds' => '%%PHINX_CONFIG_DIR%%/tests/db/seeds' + ], + 'environments' => [ + 'default_migration_table' => 'phinxlog', + 'default_environment' => '', + 'mysql' => [ + 'adapter' => 'mysql', + 'host' => getenv('TESTS_DB_MYSQL_HOSTNAME') ?: 'localhost', + 'name' => getenv('TESTS_DB_MYSQL_DATABASE') ?: 'tp_orm_test', + 'user' => getenv('TESTS_DB_MYSQL_USERNAME') ?: 'homestead', + 'pass' => getenv('TESTS_DB_MYSQL_PASSWORD') ?: 'secret', + 'port' => getenv('TESTS_DB_MYSQL_HOSTPORT') ?: '3306', + 'charset' => 'utf8', + ], + 'pgsql' => [ + 'adapter' => 'pgsql', + 'host' => getenv('TESTS_DB_PGSQL_HOSTNAME') ?: 'localhost', + 'name' => getenv('TESTS_DB_PGSQL_DATABASE') ?: 'tp_orm_test', + 'user' => getenv('TESTS_DB_PGSQL_USERNAME') ?: 'homestead', + 'pass' => getenv('TESTS_DB_PGSQL_PASSWORD') ?: 'secret', + 'port' => getenv('TESTS_DB_PGSQL_HOSTPORT') ?: '5432', + 'charset' => 'utf8', + ] + ], + 'version_order' => 'creation' +]; diff --git a/phpunit.xml b/phpunit.xml index bb93b3c9..464f7650 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -5,14 +5,15 @@ beStrictAboutTestsThatDoNotTestAnything="false" bootstrap="tests/bootstrap.php" colors="true" + convertDeprecationsToExceptions="false" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" + failOnWarning="false" processIsolation="false" stopOnError="false" stopOnFailure="false" - verbose="true" - xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd" > @@ -30,5 +31,10 @@ + + + + + diff --git a/src/db/connector/Pgsql.php b/src/db/connector/Pgsql.php index 665c4188..dd365582 100644 --- a/src/db/connector/Pgsql.php +++ b/src/db/connector/Pgsql.php @@ -12,6 +12,7 @@ namespace think\db\connector; use PDO; +use think\db\BaseQuery; use think\db\PDOConnection; /** @@ -109,4 +110,70 @@ protected function supportSavepoint(): bool { return true; } + + protected function getFieldType(string $type): string + { + // 将字段类型转换为小写以进行比较 + $type = strtolower($type); + + return match (true) { + str_starts_with($type, 'set') => 'set', + str_starts_with($type, 'enum') => 'enum', + str_starts_with($type, 'bigint'), + str_contains($type, 'numeric') => 'bigint', + str_contains($type, 'float') || str_contains($type, 'double') || + str_contains($type, 'decimal') || str_contains($type, 'real') || + str_contains($type, 'int') || str_contains($type, 'serial') || + str_contains($type, 'bit') => 'int', + str_contains($type, 'bool') => 'bool', + str_starts_with($type, 'timestamp') => 'timestamp', + str_starts_with($type, 'datetime') => 'datetime', + str_starts_with($type, 'date') => 'date', + default => 'string', + }; + } + + public function insert(BaseQuery $query, bool $getLastInsID = false) + { + // 分析查询表达式 + $options = $query->parseOptions(); + + // 生成SQL语句 + $sql = $this->builder->insert($query); + + // 执行操作 + $result = '' == $sql ? 0 : $this->pdoExecute($query, $sql); + + if ($result) { + // todo 应该改造为使用 returning 返回完全解决该问题 + $sequence = $options['sequence'] ?? null; + $lastInsId = $getLastInsID ? $this->getLastInsID($query, $sequence) : null; + + $data = $options['data']; + + if ($lastInsId) { + $pk = $query->getAutoInc(); + if ($pk && is_string($pk)) { + $data[$pk] = $lastInsId; + } + } + + $query->setOption('data', $data); + + $this->db->trigger('after_insert', $query); + + if ($getLastInsID && $lastInsId) { + return $lastInsId; + } + } + + return $result; + } + + public function getLastInsID(BaseQuery $query, ?string $sequence = null) + { + $insertId = $this->linkID->lastInsertId($sequence); + + return $this->autoInsIDType($query, $insertId); + } } diff --git a/tests/Base.php b/tests/Base.php deleted file mode 100644 index 6c64c2b2..00000000 --- a/tests/Base.php +++ /dev/null @@ -1,21 +0,0 @@ -=')) { - $this->assertMatchesRegularExpression($pattern, $string, $message); - } else { - $this->assertRegExp($pattern, $string, $message); - } - } -} diff --git a/tests/TestCaseBase.php b/tests/TestCaseBase.php new file mode 100644 index 00000000..540166f5 --- /dev/null +++ b/tests/TestCaseBase.php @@ -0,0 +1,90 @@ +setConnection(static::$connectName); + var_dump('maker:' . __FUNCTION__ . '-' . $model::class . '-' . spl_object_id($model)); + }); + } + + public function __get(string $name) + { + if ($name === 'connectName') { + return static::$connectName; + } + + throw new \Exception('Undefined property: ' . static::class . '::$' . $name); + } + + public function setUp(): void + { + $this->db ??= Db::connect(static::$connectName); + + if (static::$connectName === 'pgsql') { + if (self::$isResetPgScript === false) { + pg_reset_function(); + self::$isResetPgScript = true; + } + pg_install_func(); + } + + // var_dump(static::class . '-' . __FUNCTION__ . '-' . spl_object_id($this)); + } + + protected static function compatibleInsertAll(BaseQuery $query, array $data): void + { + if ($query->getConnection() instanceof Pgsql) { + // 当前驱动批量插入不兼容,会产生类型错误,修复后可以移除兼容性 + foreach ($data as $datum) { + (clone $query)->insert($datum); + } + } else { + $query->insertAll($data); + } + } + + protected static function compatibleModelInsertAll(Model $query, array $data): void + { + if ($query->getConnection() === 'pgsql') { + // 当前驱动批量插入不兼容,会产生类型错误,修复后可以移除兼容性 + foreach ($data as $datum) { + (clone $query)->insert($datum); + } + } else { + $query->insertAll($data); + } + } + + protected function proxyAssertMatchesRegularExpression(string $pattern, string $string, string $message = '') + { + if (version_compare(Version::id(), '9.1', '>=')) { + $this->assertMatchesRegularExpression($pattern, $string, $message); + } else { + $this->assertRegExp($pattern, $string, $message); + } + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index cce8e7d3..c0387bc7 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -55,5 +55,49 @@ // 数据库调试模式 'debug' => false, ], + 'pgsql' => [ + // 数据库类型 + 'type' => 'pgsql', + // 主机地址 + 'hostname' => getenv('TESTS_DB_PGSQL_HOSTNAME'), + // 主机端口 + 'hostport' => getenv('TESTS_DB_PGSQL_HOSTPORT'), + // 数据库名 + 'database' => getenv('TESTS_DB_PGSQL_DATABASE'), + // 用户名 + 'username' => getenv('TESTS_DB_PGSQL_USERNAME'), + // 密码 + 'password' => getenv('TESTS_DB_PGSQL_PASSWORD'), + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => 'test_', + // 是否需要断线重连 + 'break_reconnect' => false, + // 断线标识字符串 + 'break_match_str' => [], + // 数据库调试模式 + 'debug' => false, + ], + 'pgsql_manage' => [ + // 数据库类型 + 'type' => 'pgsql', + // 主机地址 + 'hostname' => getenv('TESTS_DB_PGSQL_HOSTNAME'), + // 主机端口 + 'hostport' => getenv('TESTS_DB_PGSQL_HOSTPORT'), + // 数据库名 + 'database' => getenv('TESTS_DB_PGSQL_DATABASE'), + // 用户名 + 'username' => getenv('TESTS_DB_PGSQL_USERNAME'), + // 密码 + 'password' => getenv('TESTS_DB_PGSQL_PASSWORD'), + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => 'test_', + // 数据库调试模式 + 'debug' => false, + ], ], ]); diff --git a/tests/db/migrations/20250517151353_test_user.php b/tests/db/migrations/20250517151353_test_user.php new file mode 100644 index 00000000..7e25e990 --- /dev/null +++ b/tests/db/migrations/20250517151353_test_user.php @@ -0,0 +1,49 @@ +table('test_user', ['id' => false, 'primary_key' => ['id']]) + ->addColumn('id', 'integer', [ + 'identity' => true, + 'signed' => false, + 'null' => false, + ]) + ->addColumn('type', 'integer', [ + 'limit' => 4, // 对应 MySQL 的 TINYINT(4) + 'default' => 0, + 'null' => false, + 'signed' => false, // 转换为 PostgreSQL 的 SMALLINT + ]) + ->addColumn('username', 'string', [ + 'limit' => 32, + 'null' => false, + ]) + ->addColumn('nickname', 'string', [ + 'limit' => 32, + 'null' => false, + ]) + ->addColumn('password', 'string', [ + 'limit' => 64, + 'null' => false, + ]) + ->create(); + } +} diff --git a/tests/db/migrations/20250518164819_tran.php b/tests/db/migrations/20250518164819_tran.php new file mode 100644 index 00000000..2876139d --- /dev/null +++ b/tests/db/migrations/20250518164819_tran.php @@ -0,0 +1,44 @@ +table('test_tran_a', [ + 'id' => false, + 'primary_key' => ['id'], + ]) + ->addColumn('id', 'integer', [ + 'signed' => false, + 'identity' => true, + 'null' => false, + ]) + ->addColumn('type', 'integer', [ + 'limit' => 2, // MySQL:TINYINT(4),PG:SMALLINT + 'default' => 0, + 'signed' => false, + 'null' => false, + ]) + ->addColumn('username', 'string', [ + 'limit' => 32, + 'null' => false, + ]) + ->create(); + } +} diff --git a/tests/db/migrations/20250519162501_test_goods.php b/tests/db/migrations/20250519162501_test_goods.php new file mode 100644 index 00000000..4f000758 --- /dev/null +++ b/tests/db/migrations/20250519162501_test_goods.php @@ -0,0 +1,43 @@ +table('test_goods', [ + 'id' => false, + 'primary_key' => ['id'], + ]) + ->addColumn('id', 'integer', [ + 'signed' => false, + 'identity' => true, + 'null' => false, + ]) + ->addColumn('name', 'string', [ + 'limit' => 32, + 'default' => '', + 'null' => false, + ]) + ->addColumn('extend', 'json', [ + 'null' => true, + 'default' => null, + ]) + ->create(); + } +} diff --git a/tests/db/migrations/20250519165915_model_one_to_one.php b/tests/db/migrations/20250519165915_model_one_to_one.php new file mode 100644 index 00000000..624d3069 --- /dev/null +++ b/tests/db/migrations/20250519165915_model_one_to_one.php @@ -0,0 +1,74 @@ +table('orm_test_user', [ + 'id' => false, + 'primary_key' => ['id'], + ]) + ->addColumn('id', 'integer', [ + 'identity' => true, + 'signed' => true, + 'null' => false, + ]) + ->addColumn('account', 'string', [ + 'limit' => 255, + 'null' => false, + 'default' => '', + ])->create(); + + $this + ->table('orm_test_profile', [ + 'id' => false, + 'primary_key' => ['id'], + ]) + ->addColumn('id', 'integer', [ + 'identity' => true, + 'signed' => true, + 'null' => false, + ]) + ->addColumn('uid', 'integer', [ + 'signed' => true, + 'null' => false, + ]) + ->addColumn('email', 'string', [ + 'limit' => 255, + 'null' => false, + 'default' => '', + ]) + ->addColumn('nickname', 'string', [ + 'limit' => 255, + 'null' => false, + 'default' => '', + ]) + ->addColumn('update_time', 'datetime', [ + 'null' => false, + ]) + ->addColumn('delete_time', 'datetime', [ + 'null' => true, + 'default' => null, + ]) + ->addColumn('create_time', 'datetime', [ + 'null' => false, + ]) + ->create(); + } +} diff --git a/tests/db/migrations/20250520095311_model_field_type.php b/tests/db/migrations/20250520095311_model_field_type.php new file mode 100644 index 00000000..6158096d --- /dev/null +++ b/tests/db/migrations/20250520095311_model_field_type.php @@ -0,0 +1,64 @@ +getAdapter()->getAdapterType(); + + $this + ->table('test_field_type', ['id' => false, 'primary_key' => ['id']]) + ->addColumn( + 'id', + 'integer', + [ + 'identity' => true, + 'signed' => false, // MySQL用UNSIGNED, PostgreSQL需要容错 + 'null' => false, + ] + ) + ->addColumn( + 't_json', + 'json', + [ + 'null' => true, + 'default' => null, + ]) + ->addColumn( + 't_php', + 'text', + [ // 改用text类型更通用 + 'limit' => 512, + 'null' => true, + 'default' => null, + ] + ) + ->addColumn( + 'bigint', + $adapterType === 'pgsql' ? 'decimal' : 'biginteger', + [ + 'signed' => false, + 'null' => true, + 'default' => null, + 'after' => 't_php', // 可选字段排序 + 'precision' => $adapterType === 'pgsql' ? 20 : null, // PG BIGINT 最大19位 + ] + ) + ->create(); + } +} diff --git a/tests/functions.php b/tests/functions.php index 35bc7c71..682c12c3 100644 --- a/tests/functions.php +++ b/tests/functions.php @@ -50,7 +50,120 @@ function query_mysql_connection_id(ConnectionInterface $connect): int return (int) $cid; } +function query_pgsql_connection_id(ConnectionInterface $connect): int +{ + $cid = $connect->query('SELECT pg_backend_pid() as cid')[0]['cid']; + + return (int) $cid; +} + +function query_connection_id(ConnectionInterface $connect): int +{ + if ($connect->getConfig('type') === 'mysql') { + return query_mysql_connection_id($connect); + } elseif ($connect->getConfig('type') === 'pgsql') { + return query_pgsql_connection_id($connect); + } else { + throw new \RuntimeException('Unsupported database type'); + } +} + + function mysql_kill_connection(string $name, $cid) { Db::connect($name)->execute("KILL {$cid}"); } + +function pgsql_kill_connection(string $name, $cid) +{ + Db::connect($name)->execute("SELECT pg_terminate_backend({$cid})"); +} + +function kill_connection(string $name, $cid): void +{ + $connect = Db::connect($name); + if ($connect->getConfig('type') === 'mysql') { + mysql_kill_connection($name, $cid); + } elseif ($connect->getConfig('type') === 'pgsql') { + pgsql_kill_connection($name, $cid); + } else { + throw new \RuntimeException('Unsupported database type'); + } +} + +global $pg_func_installed; +$pg_func_installed = []; + +function pg_server_version(string $name = 'pgsql', bool $raw = false): string +{ + $pdo = Db::connect($name)->connect(); + $version = $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION); + + return $raw ? $version : explode(' ', $version)[0]; +} + +function pg_install_func(string $name = 'pgsql'): void +{ + global $pg_func_installed; + + if ($pg_func_installed[$name] ?? false) { + return; + } + + /** @var \PDO $pdo */ + $pdo = Db::connect($name)->connect(); + + $rawVersion = pg_server_version($name, true); + $version = pg_server_version($name); + $file_path = version_compare($version, '12.0', '>=') + ? __DIR__ . '/../src/db/connector/pgsql12.sql' + : __DIR__ . '/../src/db/connector/pgsql.sql'; + + echo PHP_EOL, "> Installing PostgreSQL({$rawVersion}) functions from {$file_path}", PHP_EOL; + + $content = file_get_contents($file_path); + $statements = preg_split('/;\s*(?=CREATE|COMMENT|DROP)/i', $content); + + $pdo->beginTransaction(); + try { + foreach ($statements as $stmt) { + $stmt = trim($stmt); + if (!empty($stmt)) { + $pdo->exec($stmt); + } + } + $pdo->commit(); + } catch (\Throwable $exception) { + $pdo->rollBack(); + throw $exception; + } + + $pg_func_installed[$name] = true; +} + +function pg_reset_function(string $name = 'pgsql'): void +{ + global $pg_func_installed; + + /** @var \PDO $pdo */ + $pdo = Db::connect($name)->connect(); + + $statements = [ + "DROP FUNCTION IF EXISTS public.table_msg(a_table_name varchar)", + "DROP FUNCTION IF EXISTS public.table_msg(a_schema_name varchar, a_table_name varchar)", + "DROP FUNCTION IF EXISTS pgsql_type(a_type varchar)", + "DROP TYPE IF EXISTS public.tablestruct CASCADE", + ]; + $pdo->beginTransaction(); + try { + foreach ($statements as $statement) { + $pdo->exec($statement); + } + $pdo->commit(); + } catch (\Throwable $exception) { + $pdo->rollBack(); + throw $exception; + } + + $pg_func_installed[$name] = false; +} diff --git a/tests/orm/DbJsonFieldsBase.php b/tests/orm/DbJsonFieldsBase.php new file mode 100644 index 00000000..6b029a3f --- /dev/null +++ b/tests/orm/DbJsonFieldsBase.php @@ -0,0 +1,99 @@ + 1, 'name' => '肥皂', 'extend' => '{"brand": "TP6", "standard": null, "type": "清洁"}'], + ['id' => 2, 'name' => '牙膏', 'extend' => '{"brand": "TP8", "standard": "大", "type": "清洁"}'], + ['id' => 3, 'name' => '牙刷', 'extend' => '{"brand": "TP8", "standard": "大", "type": "清洁"}'], + ['id' => 4, 'name' => '卫生纸', 'extend' => '{"brand": null, "standard": null, "type": "日用品" ,"amount": 20}'], + ['id' => 5, 'name' => '香肠', 'extend' => '{"brand": null, "weight": 480, "type": "食品" ,"pack": 1}'], + ]; + return array_map(fn ($item) => ['extend' => json_decode($item['extend'], true)] + $item, $data); + } + + public function testInitGoods(): array + { + $this->db->execute('TRUNCATE TABLE test_goods;'); + + $userData = $this->provideTestData(); + self::compatibleInsertAll($this->db->table('test_goods')->json(['extend']), $userData); + + return $userData; + } + + /** + * @test 测试当 json 字段的指定成员不存在 + * @depends testInitGoods + */ + public function testJsonFieldMemberNotExists(array $goods) + { + $collect = collect($goods); + + $data = $this->db->table('test_goods')->where('extend->weight', null)->select(); + $this->assertSame($data->count(), $collect->where('extend.weight', null)->count()); + + $data = $this->db->table('test_goods')->where('extend->amount', null)->select(); + $this->assertSame($data->count(), $collect->where('extend.amount', null)->count()); + + $data = $this->db->table('test_goods')->where('extend->pack', null)->select(); + $this->assertSame($data->count(), $collect->where('extend.pack', null)->count()); + } + + /** + * @test 测试当 json 字段的指定成员不存在或为 null + * @depends testInitGoods + */ + public function testJsonFieldMemberNotExistsOrNull(array $goods) + { + $collect = collect($this->provideTestData()); + + $data = $this->db->table('test_goods')->where('extend->brand', null)->select(); + $this->assertSame($data->count(), $collect->where('extend.brand', null)->count()); + + $data = $this->db->table('test_goods')->where('extend->standard', null)->select(); + $this->assertSame($data->count(), $collect->where('extend.standard', null)->count()); + } + + /** + * @test 测试搜索 json 字段指定成员为指定的值 + * @depends testInitGoods + */ + public function testJsonFieldMemberEqual(array $goods) + { + $collect = collect($goods); + + $data = $this->db->table('test_goods')->where('extend->brand', 'TP8')->select(); + $this->assertSame($data->count(), $collect->where('extend.brand', 'TP8')->count()); + + $data = $this->db->table('test_goods')->where('extend->standard', '大')->select(); + $this->assertSame($data->count(), $collect->where('extend.standard', '大')->count()); + + $data = $this->db->table('test_goods')->where('extend->type', '清洁')->select(); + $this->assertSame($data->count(), $collect->where('extend.type', '清洁')->count()); + } + + /** + * @test 测试搜索 json 字段指定成员不为指定的值 + * @depends testInitGoods + */ + public function testJsonFieldMemberNotEqual(array $goods) + { + $collect = collect($goods); + + $data = $this->db->table('test_goods')->where('extend->brand', '<>', 'TP8')->whereNull('extend->brand', "or")->select(); + $this->assertSame($data->count(), $collect->where('extend.brand', '<>', 'TP8')->count()); + + $data = $this->db->table('test_goods')->where('extend->standard', '<>', '大')->whereNull('extend->standard', "or")->select(); + $this->assertSame($data->count(), $collect->where('extend.standard', '<>', '大')->count()); + + $data = $this->db->table('test_goods')->where('extend->type', '<>', '清洁')->whereNull('extend->type', "or")->select(); + $this->assertSame($data->count(), $collect->where('extend.type', '<>', '清洁')->count()); + } +} diff --git a/tests/orm/DbJsonFieldsTest.php b/tests/orm/DbJsonFieldsTest.php deleted file mode 100644 index 10765974..00000000 --- a/tests/orm/DbJsonFieldsTest.php +++ /dev/null @@ -1,106 +0,0 @@ - 1, 'name' => '肥皂', 'extend' => '{"brand": "TP6", "standard": null, "type": "清洁"}'], - ['id' => 2, 'name' => '牙膏', 'extend' => '{"brand": "TP8", "standard": "大", "type": "清洁"}'], - ['id' => 3, 'name' => '牙刷', 'extend' => '{"brand": "TP8", "standard": "大", "type": "清洁"}'], - ['id' => 4, 'name' => '卫生纸', 'extend' => '{"brand": null, "standard": null, "type": "日用品" ,"amount": 20}'], - ['id' => 5, 'name' => '香肠', 'extend' => '{"brand": null, "weight": 480, "type": "食品" ,"pack": 1}'], - ]; - self::$testGoodsData = $data; - foreach ($data as &$item) { - $item['extend'] = json_decode($item['extend'], true); - } - self::$testGoodsDataCollect = collect($data); - Db::table(self::$table)->insertAll(self::$testGoodsData); - } - - /** - * @test 测试当 json 字段的指定成员不存在 - */ - public function testJsonFieldMemberNotExists() - { - $data = Db::table(self::$table)->where('extend->weight', null)->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.weight', null)->count()); - - $data = Db::table(self::$table)->where('extend->amount', null)->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.amount', null)->count()); - - $data = Db::table(self::$table)->where('extend->pack', null)->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.pack', null)->count()); - } - - /** - * @test 测试当 json 字段的指定成员不存在或为 null - */ - public function testJsonFieldMemberNotExistsOrNull() - { - $data = Db::table(self::$table)->where('extend->brand', null)->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.brand', null)->count()); - - $data = Db::table(self::$table)->where('extend->standard', null)->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.standard', null)->count()); - } - - /** - * @test 测试搜索 json 字段指定成员为指定的值 - */ - public function testJsonFieldMemberEqual() - { - $data = Db::table(self::$table)->where('extend->brand', 'TP8')->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.brand', 'TP8')->count()); - - $data = Db::table(self::$table)->where('extend->standard', '大')->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.standard', '大')->count()); - - $data = Db::table(self::$table)->where('extend->type', '清洁')->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.type', '清洁')->count()); - } - - /** - * @test 测试搜索 json 字段指定成员不为指定的值 - */ - public function testJsonFieldMemberNotEqual() - { - $data = Db::table(self::$table)->where('extend->brand', '<>', 'TP8')->whereNull('extend->brand', "or")->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.brand', '<>', 'TP8')->count()); - - $data = Db::table(self::$table)->where('extend->standard', '<>', '大')->whereNull('extend->standard', "or")->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.standard', '<>', '大')->count()); - - $data = Db::table(self::$table)->where('extend->type', '<>', '清洁')->whereNull('extend->type', "or")->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.type', '<>', '清洁')->count()); - } -} diff --git a/tests/orm/DbTest.php b/tests/orm/DbTestBase.php similarity index 52% rename from tests/orm/DbTest.php rename to tests/orm/DbTestBase.php index e5fcdcb5..161b2dd0 100644 --- a/tests/orm/DbTest.php +++ b/tests/orm/DbTestBase.php @@ -4,115 +4,106 @@ namespace tests\orm; +use tests\TestCaseBase; +use think\Collection; +use think\db\exception\DbException; +use think\db\Raw; +use think\Exception as ThinkException; +use think\facade\Db; use function array_column; use function array_keys; use function array_unique; use function array_values; use function tests\array_column_ex; use function tests\array_value_sort; -use tests\Base; -use think\Collection; -use think\db\exception\DbException; -use think\db\Raw; -use think\Exception as ThinkException; -use think\facade\Db; -class DbTest extends Base +abstract class DbTestBase extends TestCaseBase { - protected static $testUserData; - - public static function setUpBeforeClass(): void - { - Db::execute('DROP TABLE IF EXISTS `test_user`;'); - Db::execute( - <<<'SQL' -CREATE TABLE `test_user` ( - `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, - `type` tinyint(4) NOT NULL DEFAULT '0', - `username` varchar(32) NOT NULL, - `nickname` varchar(32) NOT NULL, - `password` varchar(64) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -SQL - ); - } - - public function setUp(): void + protected function provideTestData(): array { - Db::execute('TRUNCATE TABLE `test_user`;'); - self::$testUserData = [ - ['id' => 1, 'type' => 3, 'username' => 'qweqwe', 'nickname' => 'asdasd', 'password' => '123123'], - ['id' => 2, 'type' => 2, 'username' => 'rtyrty', 'nickname' => 'fghfgh', 'password' => '456456'], + return [ + ['id' => 1, 'type' => 3, 'username' => 'qw"e"qwe', 'nickname' => 'asdasd', 'password' => '123123'], + ['id' => 2, 'type' => 2, 'username' => 'rt"yrty', 'nickname' => 'fghfgh', 'password' => '456456'], ['id' => 3, 'type' => 1, 'username' => 'uiouio', 'nickname' => 'jkljkl', 'password' => '789789'], ['id' => 5, 'type' => 2, 'username' => 'qazqaz', 'nickname' => 'wsxwsx', 'password' => '098098'], ['id' => 7, 'type' => 2, 'username' => 'rfvrfv', 'nickname' => 'tgbtgb', 'password' => '765765'], ]; - Db::table('test_user')->insertAll(self::$testUserData); } - public function testColumn() + public function testInitUsers(): array { - $users = self::$testUserData; + $this->db->execute('TRUNCATE TABLE test_user;'); + $userData = $this->provideTestData(); + self::compatibleInsertAll($this->db->table('test_user'), $userData); + + return $userData; + } + + /** + * @depends testInitUsers + */ + public function testColumn(array $users) + { // 获取全部列 - $result = Db::table('test_user')->column('*', 'id'); + $result = $this->db->table('test_user')->column('*', 'id'); $this->assertCount(5, $result); $this->assertEquals($users, array_values($result)); $this->assertEquals(array_column($users, 'id'), array_keys($result)); // 获取某一个字段 - $result = Db::table('test_user')->column('username'); + $result = $this->db->table('test_user')->column('username'); $this->assertEquals(array_column($users, 'username'), $result); // 获取某字段唯一 - $result = Db::table('test_user')->column('DISTINCT type'); + $result = $this->db->table('test_user')->order('type', 'desc')->column('DISTINCT type'); $expected = array_unique(array_column($users, 'type')); $this->assertEquals($expected, $result); // 字段别名 - $result = Db::table('test_user')->column('username as name2'); + $result = $this->db->table('test_user')->column('username as name2'); $expected = array_column($users, 'username'); $this->assertEquals($expected, $result); // 表别名 - $result = Db::table('test_user')->alias('test2')->column('test2.username'); + $result = $this->db->table('test_user')->alias('test2')->column('test2.username'); $expected = array_column($users, 'username'); $this->assertEquals($expected, $result); // 获取若干列 - $result = Db::table('test_user')->column('username,nickname', 'id'); + $result = $this->db->table('test_user')->column('username,nickname', 'id'); $expected = array_column_ex($users, ['username', 'nickname', 'id'], 'id'); $this->assertEquals($expected, $result); // 获取若干列不指定key时不报错 - $result = Db::table('test_user')->column('username,nickname,id'); + $result = $this->db->table('test_user')->column('username,nickname,id'); $expected = array_column_ex($users, ['username', 'nickname', 'id']); $this->assertEquals($expected, $result); // 数组方式获取 - $result = Db::table('test_user')->column(['username', 'nickname', 'type'], 'id'); + $result = $this->db->table('test_user')->column(['username', 'nickname', 'type'], 'id'); $expected = array_column_ex($users, ['username', 'nickname', 'type', 'id'], 'id'); $this->assertEquals($expected, $result); // 数组方式获取(单字段) - $result = Db::table('test_user')->column(['type'], 'id'); + $result = $this->db->table('test_user')->column(['type'], 'id'); $expected = array_column($users, 'type', 'id'); $this->assertEquals($expected, $result); // 数组方式获取(重命名字段) - $result = Db::table('test_user')->column(['username' => 'my_name', 'nickname'], 'id'); + $result = $this->db->table('test_user')->column(['username' => 'my_name', 'nickname'], 'id'); $expected = array_column_ex($users, ['username' => 'my_name', 'nickname', 'id'], 'id'); array_value_sort($result); array_value_sort($expected); $this->assertEquals($expected, $result); // 数组方式获取(定义表达式) - $result = Db::table('test_user') + $result = $this->db->table('test_user') ->column([ 'username' => 'my_name', 'nickname', - new Raw('`type`+1000 as type2'), + new Raw('type+1000 as type2'), ], 'id'); $expected = array_column_ex( $users, @@ -131,46 +122,49 @@ public function testColumn() $this->assertEquals($expected, $result); } - public function testWhereIn() + /** + * @depends testInitUsers + */ + public function testWhereIn(array $users) { $sqlLogs = []; Db::listen(function ($sql) use (&$sqlLogs) { $sqlLogs[] = $sql; }); - $expected = Collection::make(self::$testUserData)->whereIn('type', [1, 3])->values()->toArray(); - $result = Db::table('test_user')->whereIn('type', [1, 3])->column('*'); + $expected = Collection::make($users)->whereIn('type', [1, 3])->values()->toArray(); + $result = $this->db->table('test_user')->whereIn('type', [1, 3])->column('*'); $this->assertEquals($expected, $result); - $expected = Collection::make(self::$testUserData)->whereIn('type', [1])->values()->toArray(); - $result = Db::table('test_user')->whereIn('type', [1])->column('*'); + $expected = Collection::make($users)->whereIn('type', [1])->values()->toArray(); + $result = $this->db->table('test_user')->whereIn('type', [1])->column('*'); $this->assertEquals($expected, $result); - $expected = Collection::make(self::$testUserData)->whereIn('type', [1, ''])->values()->toArray(); - $result = Db::table('test_user')->whereIn('type', [1, ''])->column('*'); + $expected = Collection::make($users)->whereIn('type', [1, ''])->values()->toArray(); + $result = $this->db->table('test_user')->whereIn('type', [1, ''])->column('*'); $this->assertEquals($expected, $result); - $result = Db::table('test_user')->whereIn('type', [])->column('*'); + $result = $this->db->table('test_user')->whereIn('type', [])->column('*'); $this->assertEquals([], $result); - $expected = Collection::make(self::$testUserData)->whereNotIn('type', [1, 3])->values()->toArray(); - $result = Db::table('test_user')->whereNotIn('type', [1, 3])->column('*'); + $expected = Collection::make($users)->whereNotIn('type', [1, 3])->values()->toArray(); + $result = $this->db->table('test_user')->whereNotIn('type', [1, 3])->column('*'); $this->assertEquals($expected, $result); - $expected = Collection::make(self::$testUserData)->values()->toArray(); - $result = Db::table('test_user')->whereNotIn('type', [])->column('*'); + $expected = Collection::make($users)->values()->toArray(); + $result = $this->db->table('test_user')->whereNotIn('type', [])->column('*'); $this->assertEquals($expected, $result); - // 合并多余空格 - $sqlLogs = array_map(static fn ($str) => preg_replace('#\s{2,}#', ' ', $str), $sqlLogs); + // 合并多余空格,替换 "`" 是为了同时兼容 pg 与 mysql + $sqlLogs = array_map(static fn ($str) => preg_replace(['#\s{2,}#', '~`~'], [' ', ''], $str), $sqlLogs); $this->assertEquals([ - 'SELECT * FROM `test_user` WHERE `type` IN (1,3)', - 'SELECT * FROM `test_user` WHERE `type` = 1', - 'SELECT * FROM `test_user` WHERE `type` IN (1,0)', - 'SELECT * FROM `test_user` WHERE 0 = 1', - 'SELECT * FROM `test_user` WHERE `type` NOT IN (1,3)', - 'SELECT * FROM `test_user` WHERE 1 = 1', + 'SELECT * FROM test_user WHERE type IN (1,3)', + 'SELECT * FROM test_user WHERE type = 1', + 'SELECT * FROM test_user WHERE type IN (1,0)', + 'SELECT * FROM test_user WHERE 0 = 1', + 'SELECT * FROM test_user WHERE type NOT IN (1,3)', + 'SELECT * FROM test_user WHERE 1 = 1', ], $sqlLogs); } @@ -179,7 +173,7 @@ public function testException() $this->expectException(DbException::class); try { - Db::query('wrong syntax'); + $this->db->query('wrong syntax'); } catch (DbException $exception) { $this->assertInstanceOf(ThinkException::class, $exception); diff --git a/tests/orm/DbTransactionTest.php b/tests/orm/DbTransactionTest.php deleted file mode 100644 index 2f6177cd..00000000 --- a/tests/orm/DbTransactionTest.php +++ /dev/null @@ -1,240 +0,0 @@ - 1, 'type' => 9, 'username' => '1-9-a'], - ['id' => 2, 'type' => 8, 'username' => '2-8-a'], - ['id' => 3, 'type' => 7, 'username' => '3-7-a'], - ]; - } - - public function testTransaction() - { - $testData = self::$testData; - $connect = Db::connect(); - - $connect->table('test_tran_a')->startTrans(); - $connect->table('test_tran_a')->insertAll($testData); - $connect->table('test_tran_a')->rollback(); - - $this->assertEmpty($connect->table('test_tran_a')->column('*')); - - $connect->execute('TRUNCATE TABLE `test_tran_a`;'); - $connect->table('test_tran_a')->startTrans(); - $connect->table('test_tran_a')->insertAll($testData); - $connect->table('test_tran_a')->commit(); - $this->assertEquals($testData, $connect->table('test_tran_a')->column('*')); - $connect->table('test_tran_a')->startTrans(); - $connect->table('test_tran_a')->where('id', '=', 2)->update([ - 'username' => '2-8-b', - ]); - $connect->table('test_tran_a')->commit(); - $this->assertEquals( - '2-8-b', - $connect->table('test_tran_a')->where('id', '=', 2)->value('username') - ); - } - - public function testBreakReconnect() - { - $testData = self::$testData; - // 初始化配置 - $config = Db::getConfig(); - $config['connections']['mysql']['break_reconnect'] = true; - $config['connections']['mysql']['break_match_str'] = [ - 'query execution was interrupted', - ]; - Db::setConfig($config); - // 初始化数据 - $connect = Db::connect(null, true); - $connect->table('test_tran_a')->insertAll($testData); - - $cid = query_mysql_connection_id($connect); - mysql_kill_connection('mysql_manage', $cid); - // 触发重连 - $connect->table('test_tran_a')->where('id', '=', 2)->value('username'); - - $newCid = query_mysql_connection_id($connect); - $this->assertNotEquals($cid, $newCid); - $cid = $newCid; - - // 事务前重连 - mysql_kill_connection('mysql_manage', $cid); - Db::table('test_tran_a')->startTrans(); - $connect->table('test_tran_a')->where('id', '=', 2)->update([ - 'username' => '2-8-b', - ]); - Db::table('test_tran_a')->commit(); - $newCid = query_mysql_connection_id($connect); - $this->assertNotEquals($cid, $newCid); - $cid = $newCid; - $this->assertEquals( - '2-8-b', - Db::table('test_tran_a')->where('id', '=', 2)->value('username') - ); - - // 事务中不能重连 - try { - Db::table('test_tran_a')->startTrans(); - $connect->table('test_tran_a')->where('id', '=', 2)->update([ - 'username' => '2-8-c', - ]); - mysql_kill_connection('mysql_manage', $cid); - $connect->table('test_tran_a')->where('id', '=', 3)->update([ - 'username' => '3-7-b', - ]); - Db::table('test_tran_a')->commit(); - } catch (Throwable|Exception $exception) { - try { - Db::table('test_tran_a')->rollback(); - } catch (Exception $rollbackException) { - // Ignore exception - $this->proxyAssertMatchesRegularExpression( - '~(server has gone away)~', - $rollbackException->getMessage() - ); - } - // Ignore exception - $this->proxyAssertMatchesRegularExpression( - '~(server has gone away)~', - $exception->getMessage() - ); - } - // 预期应该没有发生任何更改 - $this->assertEquals( - '2-8-b', - Db::table('test_tran_a')->where('id', '=', 2)->value('username') - ); - $this->assertEquals( - '3-7-a', - Db::table('test_tran_a')->where('id', '=', 3)->value('username') - ); - } - - public function testTransactionSavepoint() - { - $testData = self::$testData; - // 初始化数据 - $connect = Db::connect(null, true); - $connect->table('test_tran_a')->insertAll($testData); - - Db::table('test_tran_a')->transaction(function () use ($connect) { - $cid = query_mysql_connection_id($connect); - // tran 1 - Db::table('test_tran_a')->startTrans(); - $connect->table('test_tran_a')->where('id', '=', 2)->update([ - 'username' => '2-8-c', - ]); - Db::table('test_tran_a')->commit(); - // tran 2 - Db::table('test_tran_a')->startTrans(); - $connect->table('test_tran_a')->where('id', '=', 3)->update([ - 'username' => '3-7-b', - ]); - Db::table('test_tran_a')->commit(); - }); - - // 预期变化 - $this->assertEquals( - '2-8-c', - Db::table('test_tran_a')->where('id', '=', 2)->value('username') - ); - $this->assertEquals( - '3-7-b', - Db::table('test_tran_a')->where('id', '=', 3)->value('username') - ); - } - - public function testTransactionSavepointBreakReconnect() - { - $testData = self::$testData; - // 初始化配置 - $config = Db::getConfig(); - $config['connections']['mysql']['break_reconnect'] = true; - $config['connections']['mysql']['break_match_str'] = [ - 'query execution was interrupted', - ]; - Db::setConfig($config); - // 初始化数据 - $connect = Db::connect(null, true); - $connect->table('test_tran_a')->insertAll($testData); - - // 事务中不能重连 - try { - // tran 0 - Db::table('test_tran_a')->startTrans(); - $cid = query_mysql_connection_id($connect); - // tran 1 - Db::table('test_tran_a')->startTrans(); - $connect->table('test_tran_a')->where('id', '=', 2)->update([ - 'username' => '2-8-c', - ]); - Db::table('test_tran_a')->commit(); - // kill - mysql_kill_connection('mysql_manage', $cid); - // tran 2 - Db::table('test_tran_a')->startTrans(); - $connect->table('test_tran_a')->where('id', '=', 3)->update([ - 'username' => '3-7-b', - ]); - Db::table('test_tran_a')->commit(); - // tran 0 - Db::table('test_tran_a')->commit(); - } catch (Throwable|Exception $exception) { - try { - Db::table('test_tran_a')->rollback(); - } catch (Exception $rollbackException) { - // Ignore exception - $this->proxyAssertMatchesRegularExpression( - '~(server has gone away)~', - $rollbackException->getMessage() - ); - } - // Ignore exception - $this->proxyAssertMatchesRegularExpression( - '~(server has gone away)~', - $exception->getMessage() - ); - } - // 预期应该没有发生任何更改 - $this->assertEquals( - '2-8-a', - Db::table('test_tran_a')->where('id', '=', 2)->value('username') - ); - $this->assertEquals( - '3-7-a', - Db::table('test_tran_a')->where('id', '=', 3)->value('username') - ); - } -} diff --git a/tests/orm/DbTransactionTestBase.php b/tests/orm/DbTransactionTestBase.php new file mode 100644 index 00000000..35d19184 --- /dev/null +++ b/tests/orm/DbTransactionTestBase.php @@ -0,0 +1,272 @@ + 1, 'type' => 9, 'username' => '1-9-a'], + ['id' => 2, 'type' => 8, 'username' => '2-8-a'], + ['id' => 3, 'type' => 7, 'username' => '3-7-a'], + ]; + } + + public function setUp(): void + { + parent::setUp(); + + // Db::listen(function ($sql, $time) { + // echo "SQL: $sql [$time ms]\n"; + // }); + $this->db->execute('TRUNCATE TABLE test_tran_a;'); + } + + protected function reconnect(): ConnectionInterface + { + return $this->db = Db::connect($this->connectName, true); + } + + protected static function insertAll(ConnectionInterface $db, string $table, array $data): void + { + if ($db instanceof Pgsql) { + foreach ($data as $datum) { + $db->table($table)->insert($datum); + } + } else { + $db->table($table)->insertAll($data); + } + } + + public function testTransaction() + { + $this->db->query('SELECT 1;'); + $this->db->table('test_tran_a')->startTrans(); + self::insertAll($this->db, 'test_tran_a', $this->provideTestData()); + $this->db->table('test_tran_a')->rollback(); + + $this->assertEmpty($this->db->table('test_tran_a')->column('*')); + + $this->db->execute('TRUNCATE TABLE test_tran_a;'); + $this->db->table('test_tran_a')->startTrans(); + self::insertAll($this->db, 'test_tran_a', $this->provideTestData()); + $this->db->table('test_tran_a')->commit(); + $this->assertEquals($this->provideTestData(), $this->db->table('test_tran_a')->column('*')); + $this->db->table('test_tran_a')->startTrans(); + $this->db->table('test_tran_a')->where('id', '=', 2)->update([ + 'username' => '2-8-b', + ]); + $this->db->table('test_tran_a')->commit(); + $this->assertEquals( + '2-8-b', + $this->db->table('test_tran_a')->where('id', '=', 2)->value('username') + ); + } + + public function testBreakReconnect() + { + // 初始化配置 + $oldConfig = Db::getConfig(); + $config = $oldConfig; + $config['connections'][$this->connectName]['break_reconnect'] = true; + $config['connections'][$this->connectName]['break_match_str'] = [ + 'query execution was interrupted', + 'no connection to the server', + ]; + Db::setConfig($config); + + $this->reconnect(); + try { + // 初始化数据 + self::insertAll($this->db, 'test_tran_a', $this->provideTestData()); + + $cid = query_connection_id($this->db); + kill_connection($this->connectName . '_manage', $cid); + // 触发重连 + $this->db->table('test_tran_a')->where('id', '=', 2)->value('username'); + + $newCid = query_connection_id($this->db); + $this->assertNotEquals($cid, $newCid); + $cid = $newCid; + + // 事务前重连 + kill_connection($this->connectName . '_manage', $cid); + $this->db->table('test_tran_a')->startTrans(); + $this->db->table('test_tran_a')->where('id', '=', 2)->update([ + 'username' => '2-8-b', + ]); + $this->db->table('test_tran_a')->commit(); + $newCid = query_connection_id($this->db); + $this->assertNotEquals($cid, $newCid); + $cid = $newCid; + $this->assertEquals( + '2-8-b', + $this->db->table('test_tran_a')->where('id', '=', 2)->value('username') + ); + + // 事务中不能重连 + try { + $this->db->table('test_tran_a')->startTrans(); + $this->db->table('test_tran_a')->where('id', '=', 2)->update([ + 'username' => '2-8-c', + ]); + kill_connection($this->connectName . '_manage', $cid); + $this->db->table('test_tran_a')->where('id', '=', 3)->update([ + 'username' => '3-7-b', + ]); + $this->db->table('test_tran_a')->commit(); + } catch (Throwable|Exception $exception) { + try { + $this->db->table('test_tran_a')->rollback(); + } catch (Exception $rollbackException) { + // Ignore exception + $this->proxyAssertMatchesRegularExpression( + $this->connectName === 'mysql' ? '~(server has gone away)~' : '~(no connection to the server)~', + $rollbackException->getMessage() + ); + } + // Ignore exception + $this->proxyAssertMatchesRegularExpression( + $this->connectName === 'mysql' ? '~(server has gone away)~' : '~(no connection to the server)~', + $exception->getMessage() + ); + } + // 预期应该没有发生任何更改 + $this->assertEquals( + '2-8-b', + $this->db->table('test_tran_a')->where('id', '=', 2)->value('username') + ); + $this->assertEquals( + '3-7-a', + $this->db->table('test_tran_a')->where('id', '=', 3)->value('username') + ); + } finally { + Db::setConfig($oldConfig); + $this->reconnect(); + } + } + + public function testTransactionSavepoint() + { + // 初始化数据 + $oldConnect = $this->db; + $oldConnect->query('select 1;'); + $newConnect = $this->reconnect(); + $newConnect->query('select 1;'); + self::assertNotEquals(spl_object_id($oldConnect), spl_object_id($newConnect)); + + self::insertAll($newConnect, 'test_tran_a', $this->provideTestData()); + + try { + $this->db->table('test_tran_a')->transaction(function () use ($newConnect, $oldConnect) { + // tran 1 + $newConnect->table('test_tran_a')->startTrans(); + $oldConnect->table('test_tran_a')->where('id', '=', 2)->update([ + 'username' => '2-8-c', + ]); + $newConnect->table('test_tran_a')->commit(); + // tran 2 + $newConnect->table('test_tran_a')->startTrans(); + $oldConnect->table('test_tran_a')->where('id', '=', 3)->update([ + 'username' => '3-7-b', + ]); + $newConnect->table('test_tran_a')->commit(); + }); + } finally { + $oldConnect->close(); + } + + // 预期变化 + $this->assertEquals( + '2-8-c', + $newConnect->table('test_tran_a')->where('id', '=', 2)->value('username') + ); + $this->assertEquals( + '3-7-b', + $newConnect->table('test_tran_a')->where('id', '=', 3)->value('username') + ); + } + + public function testTransactionSavepointBreakReconnect() + { + // 初始化配置 + $oldConfig = Db::getConfig(); + $config = $oldConfig; + $config['connections'][$this->connectName]['break_reconnect'] = true; + $config['connections'][$this->connectName]['break_match_str'] = [ + 'query execution was interrupted', + 'no connection to the server', + ]; + Db::setConfig($config); + // 初始化数据 + $oldConnect = $this->db; + $oldConnect->query('select 1;'); + $newConnect = $this->reconnect(); + $newConnect->query('select 1;'); + self::assertNotEquals(spl_object_id($oldConnect->getPdo()), spl_object_id($newConnect->getPdo())); + self::insertAll($newConnect, 'test_tran_a', $this->provideTestData()); + + // 事务中不能重连 + try { + // tran 0 + $newConnect->table('test_tran_a')->startTrans(); + $cid = query_connection_id($newConnect); + // tran 1 + $oldConnect->table('test_tran_a')->startTrans(); + $newConnect->table('test_tran_a')->where('id', '=', 2)->update([ + 'username' => '2-8-c', + ]); + $oldConnect->table('test_tran_a')->commit(); + // kill + kill_connection($this->connectName . '_manage', $cid); + // tran 2 + $oldConnect->table('test_tran_a')->startTrans(); + $newConnect->table('test_tran_a')->where('id', '=', 3)->update([ + 'username' => '3-7-b', + ]); + $oldConnect->table('test_tran_a')->commit(); + // tran 0 + $newConnect->table('test_tran_a')->commit(); + } catch (Throwable|Exception $exception) { + try { + $newConnect->table('test_tran_a')->rollback(); + } catch (Exception $rollbackException) { + // Ignore exception + $this->proxyAssertMatchesRegularExpression( + $this->connectName === 'mysql' ? '~(server has gone away)~' : '~(no connection to the server)~', + $rollbackException->getMessage() + ); + } + // Ignore exception + $this->proxyAssertMatchesRegularExpression( + $this->connectName === 'mysql' ? '~(server has gone away)~' : '~(no connection to the server)~', + $exception->getMessage() + ); + } finally { + $oldConnect->close(); + } + // 预期应该没有发生任何更改 + $this->assertEquals( + '2-8-a', + $this->db->table('test_tran_a')->where('id', '=', 2)->value('username') + ); + $this->assertEquals( + '3-7-a', + $this->db->table('test_tran_a')->where('id', '=', 3)->value('username') + ); + } +} diff --git a/tests/orm/ModelFieldTypeTest.php b/tests/orm/ModelFieldTypeBase.php similarity index 76% rename from tests/orm/ModelFieldTypeTest.php rename to tests/orm/ModelFieldTypeBase.php index 2843b092..0df0670a 100644 --- a/tests/orm/ModelFieldTypeTest.php +++ b/tests/orm/ModelFieldTypeBase.php @@ -3,40 +3,45 @@ namespace tests\orm; -use PHPUnit\Framework\TestCase; use tests\stubs\FieldTypeModel; use tests\stubs\TestFieldJsonDTO; use tests\stubs\TestFieldPhpDTO; -use think\facade\Db; +use tests\TestCaseBase; -class ModelFieldTypeTest extends TestCase +class ModelFieldTypeBase extends TestCaseBase { - public static function setUpBeforeClass(): void + protected function provideTestData(): array { - Db::execute('DROP TABLE IF EXISTS `test_field_type`;'); - Db::execute( - << 1, 't_json' => '{"num1": 1, "str1": "a"}', 't_php' => (string) (new TestFieldPhpDTO(1, 'a')), 'bigint' => '0'], ['id' => 2, 't_json' => '{"num1": 2, "str1": "b"}', 't_php' => (string) (new TestFieldPhpDTO(2, 'b')), 'bigint' => '244791959321042944'], ['id' => 3, 't_json' => '{"num1": 3, "str1": "c"}', 't_php' => (string) (new TestFieldPhpDTO(3, 'c')), 'bigint' => '18374686479671623679'], ]; + } - (new FieldTypeModel())->insertAll($data); + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); - $result = Db::table('test_field_type')->select(); + self::initModelSupport(); + } + + public function testInitData(): array + { + $this->db->execute('TRUNCATE TABLE test_field_type;'); + + $data = $this->provideTestData(); + self::compatibleModelInsertAll(new FieldTypeModel(), $data); + + return $data; + } + + /** + * @depends testInitData + */ + public function testFieldTypeSelect(array $data) + { + $result = $this->db->table('test_field_type')->setFieldType(['bigint' => 'string'])->select(); $this->assertNotEmpty($result->count()); foreach ($data as $index => $item) { $this->assertEquals($item, $result[$index]); @@ -52,7 +57,7 @@ public function testFieldTypeSelect() } /** - * @depends testFieldTypeSelect + * @depends testInitData */ public function testFieldReadAndWrite() { @@ -69,9 +74,6 @@ public function testFieldReadAndWrite() $this->assertEquals($result->id, $result->t_php->getId()); } - /** - * @depends testFieldTypeSelect - */ public function testFieldReadInvalid() { diff --git a/tests/orm/ModelOneToOneTest.php b/tests/orm/ModelOneToOneBase.php similarity index 52% rename from tests/orm/ModelOneToOneTest.php rename to tests/orm/ModelOneToOneBase.php index 5d4be715..9ab1ca70 100644 --- a/tests/orm/ModelOneToOneTest.php +++ b/tests/orm/ModelOneToOneBase.php @@ -3,40 +3,29 @@ namespace tests\orm; -use PHPUnit\Framework\TestCase; use tests\stubs\ProfileModel; use tests\stubs\UserModel; -use think\facade\Db; +use tests\TestCaseBase; /** * 模型一对一关联 */ -class ModelOneToOneTest extends TestCase +abstract class ModelOneToOneBase extends TestCaseBase { public static function setUpBeforeClass(): void { - $sqlList = [ - 'DROP TABLE IF EXISTS `test_user`;', - 'CREATE TABLE `test_user` ( - `id` int NOT NULL AUTO_INCREMENT, - `account` varchar(255) NOT NULL DEFAULT "", - PRIMARY KEY (`id`) - ) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4;', - 'DROP TABLE IF EXISTS `test_profile`;', - 'CREATE TABLE `test_profile` ( - `id` int NOT NULL AUTO_INCREMENT, - `uid` int NOT NULL, - `email` varchar(255) NOT NULL DEFAULT "", - `nickname` varchar(255) NOT NULL DEFAULT "", - `update_time` datetime NOT NULL, - `delete_time` datetime DEFAULT NULL, - `create_time` datetime NOT NULL, - PRIMARY KEY (`id`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;', - ]; - foreach ($sqlList as $sql) { - Db::execute($sql); - } + parent::setUpBeforeClass(); + + self::initModelSupport(); + } + + public function setUp(): void + { + parent::setUp(); + + // 每个测试执行前重置测试数据 + $this->db->execute('TRUNCATE TABLE orm_test_user;'); + $this->db->execute('TRUNCATE TABLE orm_test_profile;'); } /** @@ -49,7 +38,8 @@ public function testBindAttr() $user = new UserModel(); $user->account = 'thinkphp'; - $user->profile = new ProfileModel(['email' => $email, 'nickname' => $nickname]); + $profile = new ProfileModel(['email' => $email, 'nickname' => $nickname]); + $user->profile = $profile; $user->together(['profile'])->save(); $userID = $user->id; diff --git a/tests/orm/MysqlDbJsonFieldsTest.php b/tests/orm/MysqlDbJsonFieldsTest.php new file mode 100644 index 00000000..40696af4 --- /dev/null +++ b/tests/orm/MysqlDbJsonFieldsTest.php @@ -0,0 +1,9 @@ +