From 05e1dd4c9b54bed489380ff75e5381cf2ff69098 Mon Sep 17 00:00:00 2001 From: PurHur Date: Fri, 28 Aug 2026 06:49:02 +0000 Subject: [PATCH] Stdlib: register ext/sqlsrv connect/query/errors builtins (#6577) Add phase-1 sqlsrv module so function_exists probes succeed and invalid connect returns false with structured sqlsrv_errors() when the Microsoft ODBC driver is absent (php-src ext/sqlsrv). Co-authored-by: Cursor --- docs/capabilities.md | 38 +--- ext/sqlsrv/Module.php | 50 +++++ ext/sqlsrv/SqlsrvExtensionPolicy.php | 36 ++++ ext/sqlsrv/VmSqlsrvConnection.php | 85 ++++++++ ext/sqlsrv/VmSqlsrvCore.php | 158 +++++++++++++++ ext/sqlsrv/sqlsrv_builtins.php | 186 ++++++++++++++++++ lib/ExtensionRegistry.php | 3 +- script/capability-matrix.php | 1 + .../cases/stdlib/sqlsrv_functions_exist.phpt | 37 ++++ 9 files changed, 560 insertions(+), 34 deletions(-) create mode 100644 ext/sqlsrv/Module.php create mode 100644 ext/sqlsrv/SqlsrvExtensionPolicy.php create mode 100644 ext/sqlsrv/VmSqlsrvConnection.php create mode 100644 ext/sqlsrv/VmSqlsrvCore.php create mode 100644 ext/sqlsrv/sqlsrv_builtins.php create mode 100644 test/compliance/cases/stdlib/sqlsrv_functions_exist.phpt diff --git a/docs/capabilities.md b/docs/capabilities.md index 96bfdd1499b..0cdeefea2d5 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -138,39 +138,6 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand. | `ctype_space` | yes | yes | yes | ctype | JIT PHPT | | `ctype_upper` | yes | yes | yes | ctype | JIT PHPT | | `ctype_xdigit` | yes | yes | yes | ctype | JIT PHPT | -| `curl_close` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #6322) | -| `curl_copy_handle` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #20495) | -| `curl_errno` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #3325) | -| `curl_error` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #3325) | -| `curl_escape` | yes | yes | yes | curl | JIT PHPT; AOT PHPT | -| `curl_exec` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #3325) | -| `curl_file_create` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #6790) | -| `curl_getinfo` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #3325) | -| `curl_init` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #6322); JIT PHPT | -| `curl_multi_add_handle` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #3721) | -| `curl_multi_close` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #3721) | -| `curl_multi_errno` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #20495) | -| `curl_multi_exec` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #3721) | -| `curl_multi_getcontent` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #3721) | -| `curl_multi_info_read` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #20495) | -| `curl_multi_init` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #3721) | -| `curl_multi_remove_handle` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #3721) | -| `curl_multi_select` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #3721) | -| `curl_multi_setopt` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #20495) | -| `curl_multi_strerror` | yes | yes | yes | curl | | -| `curl_pause` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #20494) | -| `curl_reset` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #20494) | -| `curl_setopt` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #6322); JIT PHPT; AOT PHPT | -| `curl_setopt_array` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #6695) | -| `curl_share_close` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #6322) | -| `curl_share_errno` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #20531) | -| `curl_share_init` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #6322) | -| `curl_share_setopt` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #6322) | -| `curl_share_strerror` | yes | yes | yes | curl | | -| `curl_strerror` | yes | yes | yes | curl | | -| `curl_unescape` | yes | yes | yes | curl | JIT PHPT | -| `curl_upkeep` | yes | no | no | curl | not implemented for JIT in this compiler build (issue #20977) | -| `curl_version` | yes | yes | yes | curl | | | `current` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `date` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `date_add` | yes | yes | yes | standard | | @@ -1044,6 +1011,11 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand. | `spl_object_hash` | yes | yes | yes | standard | AOT PHPT | | `spl_object_id` | yes | yes | yes | standard | | | `sprintf` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | +| `sqlsrv_close` | yes | no | no | sqlsrv | not implemented for JIT in this compiler build (issue #6577) | +| `sqlsrv_connect` | yes | no | no | sqlsrv | not implemented for JIT in this compiler build (issue #6577) | +| `sqlsrv_errors` | yes | no | no | sqlsrv | not implemented for JIT in this compiler build (issue #6577) | +| `sqlsrv_fetch_array` | yes | no | no | sqlsrv | not implemented for JIT in this compiler build (issue #6577) | +| `sqlsrv_query` | yes | no | no | sqlsrv | not implemented for JIT in this compiler build (issue #6577) | | `sqrt` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `srand` | yes | yes | yes | standard | JIT PHPT | | `sscanf` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | diff --git a/ext/sqlsrv/Module.php b/ext/sqlsrv/Module.php new file mode 100644 index 00000000000..d5163e9c4d2 --- /dev/null +++ b/ext/sqlsrv/Module.php @@ -0,0 +1,50 @@ +vmContext); + } + + public function getFunctions(): array + { + if (!SqlsrvExtensionPolicy::advertisesBuiltins()) { + return []; + } + + require_once __DIR__.'/sqlsrv_builtins.php'; + + return [ + new sqlsrv_connect(), + new sqlsrv_close(), + new sqlsrv_query(), + new sqlsrv_fetch_array(), + new sqlsrv_errors(), + ]; + } +} diff --git a/ext/sqlsrv/SqlsrvExtensionPolicy.php b/ext/sqlsrv/SqlsrvExtensionPolicy.php new file mode 100644 index 00000000000..1efab76fee4 --- /dev/null +++ b/ext/sqlsrv/SqlsrvExtensionPolicy.php @@ -0,0 +1,36 @@ + */ + private static array $state = []; + + public static function registerClass(Context $ctx): void + { + if (isset($ctx->classes[self::CLASS_LC])) { + return; + } + $entry = new ClassEntry(self::CLASS_NAME); + $entry->isInternal = true; + $ctx->classes[self::CLASS_LC] = $entry; + } + + /** + * @param mixed $native host sqlsrv resource when bridged + */ + public static function wrap(mixed $native, Context $ctx): Variable + { + self::registerClass($ctx); + $object = new ObjectEntry($ctx->classes[self::CLASS_LC]); + $object->constructed = true; + self::$state[$object->id] = [ + 'closed' => false, + 'native' => $native, + ]; + $var = new Variable(Variable::TYPE_OBJECT); + $var->object($object); + + return $var; + } + + public static function isLive(ObjectEntry $object): bool + { + return isset(self::$state[$object->id]) && !self::$state[$object->id]['closed']; + } + + public static function native(ObjectEntry $object): mixed + { + return self::$state[$object->id]['native'] ?? null; + } + + public static function close(ObjectEntry $object): bool + { + if (!isset(self::$state[$object->id]) || self::$state[$object->id]['closed']) { + return false; + } + $native = self::$state[$object->id]['native']; + if (SqlsrvExtensionPolicy::hasNativeDriver() && \is_resource($native)) { + \sqlsrv_close($native); + } + self::$state[$object->id]['closed'] = true; + unset(self::$state[$object->id]); + + return true; + } + + public static function requireLive(ObjectEntry $object, string $fn): ObjectEntry + { + if (!self::isLive($object)) { + throw new \TypeError($fn.'(): supplied resource is not a valid sqlsrv connection resource'); + } + + return $object; + } +} diff --git a/ext/sqlsrv/VmSqlsrvCore.php b/ext/sqlsrv/VmSqlsrvCore.php new file mode 100644 index 00000000000..8ad6cadfb92 --- /dev/null +++ b/ext/sqlsrv/VmSqlsrvCore.php @@ -0,0 +1,158 @@ + */ + private static array $errors = []; + + public static function clearErrors(): void + { + self::$errors = []; + } + + public static function pushError(string $state, int $code, string $message): void + { + self::$errors[] = [ + 'state' => $state, + 'code' => $code, + 'message' => $message, + ]; + } + + /** + * @return list + */ + public static function peekErrors(): array + { + return self::$errors; + } + + public static function connect(string $serverName, array $connectionInfo, Context $ctx): Variable|false + { + self::clearErrors(); + if (!SqlsrvExtensionPolicy::hasNativeDriver()) { + self::pushError( + 'IMSSP', + -49, + 'This extension requires the Microsoft ODBC Driver for SQL Server to communicate with SQL Server' + ); + + return false; + } + + $native = \sqlsrv_connect($serverName, $connectionInfo); + if (false === $native) { + self::importHostErrors(); + + return false; + } + + return VmSqlsrvConnection::wrap($native, $ctx); + } + + public static function importHostErrors(): void + { + self::$errors = []; + $hostErrors = \sqlsrv_errors(); + if (!\is_array($hostErrors)) { + return; + } + foreach ($hostErrors as $row) { + if (!\is_array($row)) { + continue; + } + $state = (string) ($row['SQLSTATE'] ?? $row[0] ?? 'HY000'); + $code = (int) ($row['code'] ?? $row[1] ?? 0); + $message = (string) ($row['message'] ?? $row[2] ?? ''); + self::pushError($state, $code, $message); + } + } + + public static function buildErrorsVariable(Variable $returnVar): void + { + if ([] === self::$errors) { + $returnVar->null(); + + return; + } + $outer = new HashTable(); + $index = 0; + foreach (self::$errors as $err) { + $row = new HashTable(); + $v0 = new Variable(); + $v0->string($err['state']); + $row->add('0', $v0); + $vState = new Variable(); + $vState->string($err['state']); + $row->add('SQLSTATE', $vState); + $v1 = new Variable(); + $v1->int($err['code']); + $row->add('1', $v1); + $vCode = new Variable(); + $vCode->int($err['code']); + $row->add('code', $vCode); + $v2 = new Variable(); + $v2->string($err['message']); + $row->add('2', $v2); + $vMsg = new Variable(); + $vMsg->string($err['message']); + $row->add('message', $vMsg); + $slot = new Variable(); + $slot->array($row); + $outer->add((string) $index, $slot); + ++$index; + } + $returnVar->array($outer); + } + + /** + * @param array $connectionInfo + */ + public static function coerceConnectionInfo(Variable $var, string $fn): array + { + $var = $var->resolveIndirect(); + if (Variable::TYPE_NULL === $var->type) { + return []; + } + if (Variable::TYPE_ARRAY !== $var->type) { + throw new \TypeError(\sprintf( + '%s(): Argument #2 ($connectionInfo) must be of type array, %s given', + $fn, + match ($var->type) { + Variable::TYPE_STRING => 'string', + Variable::TYPE_INTEGER => 'int', + Variable::TYPE_OBJECT => 'object', + default => 'mixed', + } + )); + } + $out = []; + foreach ($var->toArray()->iterateKeyed(true) as [$keyVar, $slot]) { + $key = $keyVar->toString(); + if (Variable::TYPE_STRING === $slot->type) { + $out[$key] = $slot->toString(); + } elseif (Variable::TYPE_INTEGER === $slot->type) { + $out[$key] = $slot->toInt(); + } elseif (Variable::TYPE_BOOLEAN === $slot->type) { + $out[$key] = $slot->toBool(); + } elseif (Variable::TYPE_NULL === $slot->type) { + $out[$key] = null; + } else { + $out[$key] = $slot->toString(); + } + } + + return $out; + } +} diff --git a/ext/sqlsrv/sqlsrv_builtins.php b/ext/sqlsrv/sqlsrv_builtins.php new file mode 100644 index 00000000000..cb2b517a9fd --- /dev/null +++ b/ext/sqlsrv/sqlsrv_builtins.php @@ -0,0 +1,186 @@ +getName().'() is not implemented for JIT in this compiler build (issue #6577)'); + } + + protected function requireConnection(Variable $var, string $fn, int $argNum): ObjectEntry + { + $var = $var->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $var->type) { + throw new \TypeError(\sprintf( + '%s(): Argument #%d must be a valid sqlsrv connection resource, %s given', + $fn, + $argNum, + match ($var->type) { + Variable::TYPE_NULL => 'null', + Variable::TYPE_STRING => 'string', + Variable::TYPE_INTEGER => 'int', + default => 'mixed', + } + )); + } + + return VmSqlsrvConnection::requireLive($var->toObject(), $fn); + } +} + +/** sqlsrv_connect() — php-src ext/sqlsrv/php_sqlsrv.c (#6577). */ +final class sqlsrv_connect extends SqlsrvFunction +{ + public function __construct() + { + parent::__construct('sqlsrv_connect'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + if ($argc < 1 || $argc > 2) { + throw new \ArgumentCountError(\sprintf( + 'sqlsrv_connect() expects between 1 and 2 arguments, %d given', + $argc + )); + } + if (null === $frame->returnVar) { + return; + } + $serverName = VmString::coerceStringBuiltinArg($frame->calledArgs[0], 'sqlsrv_connect', 1, 'serverName'); + $connectionInfo = []; + if (2 === $argc) { + $connectionInfo = VmSqlsrvCore::coerceConnectionInfo($frame->calledArgs[1], 'sqlsrv_connect'); + } + $ctx = $frame->vmContext ?? throw new \LogicException('sqlsrv_connect() requires a VM context'); + $result = VmSqlsrvCore::connect($serverName, $connectionInfo, $ctx); + if (false === $result) { + $frame->returnVar->bool(false); + + return; + } + $frame->returnVar->copyFrom($result); + } +} + +/** sqlsrv_close() — php-src ext/sqlsrv (#6577). */ +final class sqlsrv_close extends SqlsrvFunction +{ + public function __construct() + { + parent::__construct('sqlsrv_close'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + if (1 !== $argc) { + throw new \ArgumentCountError(\sprintf( + 'sqlsrv_close() expects exactly 1 argument, %d given', + $argc + )); + } + if (null === $frame->returnVar) { + return; + } + $conn = $this->requireConnection($frame->calledArgs[0], 'sqlsrv_close', 1); + $frame->returnVar->bool(VmSqlsrvConnection::close($conn)); + } +} + +/** sqlsrv_query() — php-src ext/sqlsrv (#6577). */ +final class sqlsrv_query extends SqlsrvFunction +{ + public function __construct() + { + parent::__construct('sqlsrv_query'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + if ($argc < 2 || $argc > 3) { + throw new \ArgumentCountError(\sprintf( + 'sqlsrv_query() expects between 2 and 3 arguments, %d given', + $argc + )); + } + if (null === $frame->returnVar) { + return; + } + $this->requireConnection($frame->calledArgs[0], 'sqlsrv_query', 1); + VmSqlsrvCore::clearErrors(); + VmSqlsrvCore::pushError('IMSSP', -49, 'This extension requires the Microsoft ODBC Driver for SQL Server to communicate with SQL Server'); + $frame->returnVar->bool(false); + } +} + +/** sqlsrv_fetch_array() — php-src ext/sqlsrv (#6577). */ +final class sqlsrv_fetch_array extends SqlsrvFunction +{ + public function __construct() + { + parent::__construct('sqlsrv_fetch_array'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + if ($argc < 1 || $argc > 3) { + throw new \ArgumentCountError(\sprintf( + 'sqlsrv_fetch_array() expects between 1 and 3 arguments, %d given', + $argc + )); + } + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->bool(false); + } +} + +/** sqlsrv_errors() — php-src ext/sqlsrv (#6577). */ +final class sqlsrv_errors extends SqlsrvFunction +{ + public function __construct() + { + parent::__construct('sqlsrv_errors'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + if ($argc > 1) { + throw new \ArgumentCountError(\sprintf( + 'sqlsrv_errors() expects at most 1 argument, %d given', + $argc + )); + } + if (null === $frame->returnVar) { + return; + } + if (SqlsrvExtensionPolicy::hasNativeDriver() && 0 === $argc) { + VmSqlsrvCore::importHostErrors(); + } + VmSqlsrvCore::buildErrorsVariable($frame->returnVar); + } +} diff --git a/lib/ExtensionRegistry.php b/lib/ExtensionRegistry.php index ec891174005..782d58ec3c5 100644 --- a/lib/ExtensionRegistry.php +++ b/lib/ExtensionRegistry.php @@ -17,7 +17,7 @@ * The entries are literal `new` expressions on purpose: the AOT compiler resolves these statically, * and instantiating from a string would leave every module unreferenced and uncompiled. * - * 82 extensions, all default-enabled — matching current behaviour, where every build pays for + * 83 extensions, all default-enabled — matching current behaviour, where every build pays for * every extension. Selecting a subset is the next step and will filter on * {@see \PHPCompiler\Module::isDefaultEnabled}. */ @@ -102,6 +102,7 @@ public static function defaultModules(): array new \PHPCompiler\ext\gnupg\Module(), new \PHPCompiler\ext\pspell\Module(), new \PHPCompiler\ext\mysqli\Module(), + new \PHPCompiler\ext\sqlsrv\Module(), new \PHPCompiler\ext\tidy\Module(), new \PHPCompiler\ext\pdo\Module(), new \PHPCompiler\ext\phar\Module(), diff --git a/script/capability-matrix.php b/script/capability-matrix.php index ebb78500d96..a240bf5040c 100755 --- a/script/capability-matrix.php +++ b/script/capability-matrix.php @@ -52,6 +52,7 @@ function collectCapabilities(string $root): array 'gnupg' => new PHPCompiler\ext\gnupg\Module(), 'pspell' => new PHPCompiler\ext\pspell\Module(), 'odbc' => new PHPCompiler\ext\odbc\Module(), + 'sqlsrv' => new PHPCompiler\ext\sqlsrv\Module(), 'dba' => new PHPCompiler\ext\dba\Module(), 'mailparse' => new PHPCompiler\ext\mailparse\Module(), 'redis' => new PHPCompiler\ext\redis\Module(), diff --git a/test/compliance/cases/stdlib/sqlsrv_functions_exist.phpt b/test/compliance/cases/stdlib/sqlsrv_functions_exist.phpt new file mode 100644 index 00000000000..ee538901536 --- /dev/null +++ b/test/compliance/cases/stdlib/sqlsrv_functions_exist.phpt @@ -0,0 +1,37 @@ +--TEST-- +ext/sqlsrv Phase 1 builtins registered; invalid connect returns false + sqlsrv_errors (#6577) +--FILE-- + 'test', 'UID' => 'sa', 'PWD' => 'x']); +echo 'connect=', var_export($conn, true), "\n"; +$errors = sqlsrv_errors(); +echo 'errors_is_array=', var_export(is_array($errors), true), "\n"; +echo 'errors_count=', count($errors), "\n"; +if (is_array($errors) && isset($errors[0]['SQLSTATE'])) { + echo 'sqlstate=', $errors[0]['SQLSTATE'], "\n"; + echo 'code=', $errors[0]['code'], "\n"; +} +?> +--EXPECT-- +sqlsrv_connect=true +sqlsrv_query=true +sqlsrv_fetch_array=true +sqlsrv_errors=true +sqlsrv_close=true +extension_loaded=true +connect=false +errors_is_array=true +errors_count=1 +sqlstate=IMSSP +code=-49