From bd06cf3e3f991298d209e84f727888fe685d94d4 Mon Sep 17 00:00:00 2001 From: David Puglielli Date: Thu, 5 Sep 2019 15:51:52 -0700 Subject: [PATCH] 5.7.0-preview merge (#1031) --- .travis.yml | 6 +- CHANGELOG.md | 44 ++ Dockerfile-msphpsql | 48 +- Linux-mac-install.md | 32 +- README.md | 8 +- appveyor.yml | 6 +- azure-pipelines.yml | 312 ++++++++++++ buildscripts/buildtools.py | 75 ++- media/os_development.PNG | Bin 0 -> 21753 bytes media/os_production.PNG | Bin 0 -> 22158 bytes media/php_versions.PNG | Bin 0 -> 11432 bytes media/sql_server.PNG | Bin 0 -> 17235 bytes source/packagize.sh | 14 +- source/pdo_sqlsrv/config.m4 | 2 +- source/pdo_sqlsrv/config.w32 | 2 +- source/pdo_sqlsrv/pdo_dbh.cpp | 124 +++-- source/pdo_sqlsrv/pdo_init.cpp | 3 +- source/pdo_sqlsrv/pdo_parser.cpp | 2 +- source/pdo_sqlsrv/pdo_stmt.cpp | 101 ++-- source/pdo_sqlsrv/pdo_util.cpp | 14 +- source/pdo_sqlsrv/php_pdo_sqlsrv.h | 2 +- source/pdo_sqlsrv/php_pdo_sqlsrv_int.h | 5 +- source/pdo_sqlsrv/template.rc | 2 +- source/shared/FormattedPrint.cpp | 2 +- source/shared/FormattedPrint.h | 2 +- source/shared/StringFunctions.cpp | 2 +- source/shared/StringFunctions.h | 2 +- source/shared/core_conn.cpp | 40 +- source/shared/core_init.cpp | 2 +- source/shared/core_results.cpp | 4 +- source/shared/core_sqlsrv.h | 121 ++++- source/shared/core_stmt.cpp | 465 ++++++++++++------ source/shared/core_stream.cpp | 28 +- source/shared/core_util.cpp | 251 +++++++++- source/shared/globalization.h | 2 +- source/shared/interlockedatomic.h | 2 +- source/shared/interlockedatomic_gcc.h | 2 +- source/shared/interlockedslist.h | 2 +- source/shared/localization.hpp | 2 +- source/shared/localizationimpl.cpp | 23 +- source/shared/msodbcsql.h | 8 +- source/shared/sal_def.h | 2 +- source/shared/typedefs_for_linux.h | 2 +- source/shared/version.h | 8 +- source/shared/xplat.h | 2 +- source/shared/xplat_intsafe.h | 2 +- source/shared/xplat_winerror.h | 2 +- source/shared/xplat_winnls.h | 2 +- source/sqlsrv/config.m4 | 2 +- source/sqlsrv/config.w32 | 2 +- source/sqlsrv/conn.cpp | 19 +- source/sqlsrv/init.cpp | 4 +- source/sqlsrv/php_sqlsrv.h | 2 +- source/sqlsrv/php_sqlsrv_int.h | 6 +- source/sqlsrv/stmt.cpp | 31 +- source/sqlsrv/template.rc | 2 +- source/sqlsrv/util.cpp | 18 +- test/functional/output.py | 37 +- .../pdo_sqlsrv/MsCommon_mid-refactor.inc | 23 + .../pdo_sqlsrv/PDO29_ConnInterface.phpt | 8 + .../pdo_sqlsrv/PDO32_StmtInterface.phpt | 8 + .../pdo_sqlsrv/PDO81_MemoryCheck.phpt | 60 ++- ...y_encoding_error_bound_by_name_errors.phpt | 164 ++++++ .../pdo_sqlsrv/pdo_569_query_varcharmax.phpt | 106 ++++ .../pdo_sqlsrv/pdo_929_language_option.phpt | 54 ++ .../pdo_sqlsrv/pdo_azure_ad_access_token.phpt | 42 +- .../pdo_azure_ad_authentication.phpt | 36 +- .../pdo_sqlsrv/pdo_batch_query.phpt | 203 ++++++++ .../pdo_sqlsrv/pdo_construct_attr_errors.phpt | 140 ++++++ .../pdo_sqlsrv/pdo_data_classification.phpt | 322 ++++++++++++ .../pdo_sqlsrv/pdo_empty_result_error.phpt | 3 + .../pdo_sqlsrv/pdo_escape_braces.phpt | 71 +++ .../pdo_sqlsrv/pdo_fetch_column_twice.phpt | 151 ++++++ .../pdo_fetch_datetime_time_as_objects.phpt | 54 +- .../pdo_fetch_datetime_time_nulls.phpt | 1 - .../pdo_fetch_variants_diff_styles.phpt | 2 +- .../pdo_insert_fetch_invalid_utf16.phpt | 86 ++++ .../pdo_insert_fetch_utf8stream.phpt | 106 ++++ .../pdo_sqlsrv/pdo_insert_fetch_utf8text.phpt | 233 +++++++++ .../pdo_sqlsrv/pdo_output_decimal.phpt | 4 +- .../pdo_sqlsrv/pdo_output_decimal_errors.phpt | 128 +++++ .../pdo_sqlsrv/pdo_test_non_LOB_types.phpt | 78 +++ test/functional/pdo_sqlsrv/pdo_utf8_conn.phpt | 2 +- .../pdo_sqlsrv/pdo_warning_errors.phpt | 107 ++++ .../pdo_sqlsrv/pdostatement_fetchAll.phpt | 2 +- .../pdostatement_fetch_orientation.phpt | 16 +- .../pdo_sqlsrv/pdostatement_fetch_style.phpt | 2 +- .../pdostatement_format_money_types.phpt | 14 +- .../pdo_sqlsrv/pdostatement_get_set_attr.phpt | 108 ++-- test/functional/setup/build_ksp.py | 122 ----- test/functional/setup/ksp_app.c | 305 ------------ test/functional/setup/myKSP.c | 132 ----- test/functional/setup/run_ksp.py | 57 --- test/functional/sqlsrv/0075.phpt | 34 +- test/functional/sqlsrv/MsCommon.inc | 20 + test/functional/sqlsrv/TC81_MemoryCheck.phpt | 8 +- .../sqlsrv/sqlsrv_378_out_param_error.phpt | 77 ++- .../sqlsrv/sqlsrv_ae_insert_datetime.phpt | 68 +-- .../sqlsrv/sqlsrv_ae_insert_retrieve.phpt | 16 +- .../sqlsrv_ae_insert_retrieve_fixed_size.phpt | 25 +- ...lsrv_ae_output_param_sqltype_datetime.phpt | 3 +- ...qlsrv_ae_output_param_sqltype_numeric.phpt | 3 +- ...sqlsrv_ae_output_param_sqltype_string.phpt | 3 +- .../sqlsrv/sqlsrv_azure_ad_access_token.phpt | 42 +- .../sqlsrv_azure_ad_authentication.phpt | 45 +- .../functional/sqlsrv/sqlsrv_batch_query.phpt | 199 ++++++++ test/functional/sqlsrv/sqlsrv_connStr.phpt | 111 +---- .../sqlsrv/sqlsrv_data_classification.phpt | 312 ++++++++++++ .../functional/sqlsrv/sqlsrv_data_to_str.phpt | 14 +- .../sqlsrv/sqlsrv_empty_result_error.phpt | 2 +- .../sqlsrv/sqlsrv_escape_braces.phpt | 70 +++ .../sqlsrv_statement_format_money_types.phpt | 16 +- .../sqlsrv/srv_007_login_timeout.phpt | 41 +- ...8_sqlsrv_clientbuffermaxkbsize_option.phpt | 3 +- .../sqlsrv/srv_569_query_varcharmax.phpt | 123 +++++ .../sqlsrv/srv_570_fetch_varbinary.phpt | 143 ++++++ .../sqlsrv/test_error_encoding.phpt | 52 +- ...t_error_encoding_with_language_option.phpt | 55 +++ test/functional/sqlsrv/test_largeData.phpt | 157 +++--- test/functional/sqlsrv/test_scrollable.phpt | 22 +- .../sqlsrv/test_sqlsrv_phptype_stream.phpt | Bin 10383 -> 10421 bytes 121 files changed, 5087 insertions(+), 1432 deletions(-) create mode 100644 azure-pipelines.yml create mode 100644 media/os_development.PNG create mode 100644 media/os_production.PNG create mode 100644 media/php_versions.PNG create mode 100644 media/sql_server.PNG create mode 100644 test/functional/pdo_sqlsrv/pdo_035_binary_encoding_error_bound_by_name_errors.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_569_query_varcharmax.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_929_language_option.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_batch_query.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_construct_attr_errors.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_data_classification.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_escape_braces.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_fetch_column_twice.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_insert_fetch_invalid_utf16.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_insert_fetch_utf8stream.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_insert_fetch_utf8text.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_output_decimal_errors.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_test_non_LOB_types.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_warning_errors.phpt delete mode 100644 test/functional/setup/build_ksp.py delete mode 100644 test/functional/setup/ksp_app.c delete mode 100644 test/functional/setup/myKSP.c delete mode 100644 test/functional/setup/run_ksp.py create mode 100644 test/functional/sqlsrv/sqlsrv_batch_query.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_data_classification.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_escape_braces.phpt create mode 100644 test/functional/sqlsrv/srv_569_query_varcharmax.phpt create mode 100644 test/functional/sqlsrv/srv_570_fetch_varbinary.phpt create mode 100644 test/functional/sqlsrv/test_error_encoding_with_language_option.phpt diff --git a/.travis.yml b/.travis.yml index 6d5141ed..0618bebf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,10 +20,10 @@ env: - TEST_PHP_SQL_PWD=Password123 before_install: - - docker pull microsoft/mssql-server-linux:2017-latest + - docker pull mcr.microsoft.com/mssql/server:2019-CTP2.4-ubuntu install: - - docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Password123' -p 1433:1433 --name=$TEST_PHP_SQL_SERVER -d microsoft/mssql-server-linux:2017-latest + - docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Password123' -p 1433:1433 --name=$TEST_PHP_SQL_SERVER -d mcr.microsoft.com/mssql/server:2019-CTP2.4-ubuntu - docker build --build-arg PHPSQLDIR=$PHPSQLDIR -t msphpsql-dev -f Dockerfile-msphpsql . before_script: @@ -43,7 +43,7 @@ script: - docker exec client bash -c 'for f in ./test/functional/pdo_sqlsrv/*.out; do ls $f 2>/dev/null; cat $f 2>/dev/null; done || true' - docker exec client python ./test/functional/setup/cleanup_dbs.py -dbname $SQLSRV_DBNAME - docker exec client python ./test/functional/setup/cleanup_dbs.py -dbname $PDOSQLSRV_DBNAME - - docker exec client coveralls -e ./source/shared/ --gcov-options '\-lp' + - docker exec client coveralls -i ./source/ -e ./source/shared/ -e ./test/ --gcov-options '\-lp' - docker stop client - docker ps -a diff --git a/CHANGELOG.md b/CHANGELOG.md index b7916f19..38271771 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,50 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) +## 5.7.0-preview - 2019-09-05 +Updated PECL release packages. Here is the list of updates: + +### Added +- Support for PHP 7.4 RC 1 +- Support for Linux Ubuntu 19.04 and Debian 10 +- Feature Request [#929](https://github.com/microsoft/msphpsql/issues/929) - new [Language option](https://github.com/microsoft/msphpsql/wiki/Features#language) - Pull Request [#930](https://github.com/microsoft/msphpsql/pull/930) +- [Data Classification Sensitivity Metadata Retrieval](https://github.com/microsoft/msphpsql/wiki/Features#data-classification-sensitivity-metadata), which requires [MS ODBC Driver 17.2+](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server) and [SQL Server 2019 release candidate](https://docs.microsoft.com/sql/sql-server/sql-server-ver15-release-notes?view=sqlallproducts-allversions#-release-candidate-rc) + +### Removed +- Dropped support for Ubuntu 18.10 + +### Fixed +- Issue [#570](https://github.com/microsoft/msphpsql/issues/570) - Fixed fetching varbinary data using client buffer with sqlsrv +- Pull Request [#972](https://github.com/microsoft/msphpsql/pull/972) - Removed redundant calls to retrieve the number of columns or rows in the current query result set +- Pull Request [#978](https://github.com/microsoft/msphpsql/pull/978) - PDO_SQLSRV implementation of PDO::getColumnMeta now references cached metadata rather than making an ODBC call every time +- Pull Request [#979](https://github.com/microsoft/msphpsql/pull/979) - Added support for data classification Sensitivity metadata retrieval +- Pull Request [#985](https://github.com/microsoft/msphpsql/pull/985) - Fixed memory issues with data classification data structures +- Issue [#432](https://github.com/microsoft/msphpsql/issues/432) - Having any invalid UTF-8 name in the connection string will no longer invoke misleading error messages +- Issue [#909](https://github.com/microsoft/msphpsql/issues/909) - Fixed potential exception with locale issues in macOS +- Pull Request [#992](https://github.com/microsoft/msphpsql/pull/992) - Produced the correct error when requesting Data Classification metadata with ODBC drivers prior to 17 +- Pull Request [#1001](https://github.com/microsoft/msphpsql/pull/1001) - Fixed compilation issue with PHP 7.4 alpha +- Pull Request [#1004](https://github.com/microsoft/msphpsql/pull/1004) - Fixed another compilation issue with PHP 7.4 alpha +- Pull Request [#1008](https://github.com/microsoft/msphpsql/pull/1008) - Improved data caching when fetching datetime objects +- Pull Request [#1011](https://github.com/microsoft/msphpsql/pull/1011) - Fixed a potential buffer overflow when parsing for escaped braces in the connection string +- Pull Request [#1015](https://github.com/microsoft/msphpsql/pull/1015) - Fixed compilation issues and addressed various memory leaks detected by PHP 7.4 beta 1 + +### Limitations +- No support for inout / output params when using sql_variant type +- No support for inout / output params when formatting decimal values +- In Linux and macOS, setlocale() only takes effect if it is invoked before the first connection. Attempting to set the locale after connecting will not work +- Always Encrypted requires [MS ODBC Driver 17+](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server) + - Only Windows Certificate Store and Azure Key Vault are supported. Custom Keystores are not yet supported + - Issue [#716](https://github.com/Microsoft/msphpsql/issues/716) - With Always Encrypted enabled, named parameters in subqueries are not supported + - [Always Encrypted limitations](https://docs.microsoft.com/sql/connect/php/using-always-encrypted-php-drivers#limitations-of-the-php-drivers-when-using-always-encrypted) + +### Known Issues +- Data Classification metadata retrieval is not compatible with ODBC Driver 17.4.1 +- Connection pooling on Linux or macOS is not recommended with [unixODBC](http://www.unixodbc.org/) < 2.3.7 +- When pooling is enabled in Linux or macOS + - unixODBC <= 2.3.4 (Linux and macOS) might not return proper diagnostic information, such as error messages, warnings and informative messages + - due to this unixODBC bug, fetch large data (such as xml, binary) as streams as a workaround. See the examples [here](https://github.com/Microsoft/msphpsql/wiki/Features#pooling) +- With ColumnEncryption enabled, calling stored procedures with XML parameters does not work (Issue [#674](https://github.com/Microsoft/msphpsql/issues/674)) + ## 5.6.1 - 2019-03-19 Updated PECL release packages. Here is the list of updates: diff --git a/Dockerfile-msphpsql b/Dockerfile-msphpsql index ab289479..4960b584 100644 --- a/Dockerfile-msphpsql +++ b/Dockerfile-msphpsql @@ -1,15 +1,17 @@ -#Download base image ubuntu 16.04 +# Download base image ubuntu 18.04 -FROM ubuntu:16.04 +FROM ubuntu:18.04 # Update Ubuntu Software repository RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && \ + apt-get install -y software-properties-common && \ + add-apt-repository ppa:ondrej/php -y && \ apt-get -y install \ - apt-transport-https \ + apt-transport-https \ apt-utils \ autoconf \ curl \ - libcurl3 \ + libcurl4 \ g++ \ gcc \ git \ @@ -17,18 +19,26 @@ RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && \ libxml2-dev \ locales \ make \ - php7.0 \ - php7.0-dev \ + php7.3 \ + php7.3-dev \ python-pip \ re2c \ unixodbc-dev \ - unzip && apt-get clean - + unzip && apt-get clean && \ + curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && \ + curl https://packages.microsoft.com/config/ubuntu/18.04/prod.list > /etc/apt/sources.list.d/mssql-release.list && \ + apt-get -y update && \ + export ACCEPT_EULA=Y && apt-get -y install msodbcsql17 mssql-tools && \ + update-alternatives --set php /usr/bin/php7.3 + ARG PHPSQLDIR=/REPO/msphpsql-dev ENV TEST_PHP_SQL_SERVER sql ENV TEST_PHP_SQL_UID sa ENV TEST_PHP_SQL_PWD Password123 +# update PATH after ODBC driver and tools are installed +ENV PATH="/opt/mssql-tools/bin:${PATH}" + # add locale iso-8859-1 RUN sed -i 's/# en_US ISO-8859-1/en_US ISO-8859-1/g' /etc/locale.gen RUN locale-gen en_US @@ -37,19 +47,12 @@ RUN locale-gen en_US RUN locale-gen en_US.UTF-8 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8' -#install ODBC driver -RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - -RUN curl https://packages.microsoft.com/config/ubuntu/16.04/prod.list > /etc/apt/sources.list.d/mssql-release.list - -RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && ACCEPT_EULA=Y apt-get install -y msodbcsql17 mssql-tools -ENV PATH="/opt/mssql-tools/bin:${PATH}" - -#install coveralls (upgrade both pip and requests first) +# install coveralls (upgrade both pip and requests first) RUN python -m pip install --upgrade pip RUN python -m pip install --upgrade requests RUN python -m pip install cpp-coveralls -#Either Install git / download zip (One can see other strategies : https://ryanfb.github.io/etc/2015/07/29/git_strategies_for_docker.html ) +# Either Install git / download zip (One can see other strategies : https://ryanfb.github.io/etc/2015/07/29/git_strategies_for_docker.html ) #One option is to get source from zip file of repository. #another option is to copy source to build directory on image RUN mkdir -p $PHPSQLDIR @@ -59,14 +62,17 @@ WORKDIR $PHPSQLDIR/source/ RUN chmod +x ./packagize.sh RUN /bin/bash -c "./packagize.sh" -RUN echo "extension = pdo_sqlsrv.so" >> /etc/php/7.0/cli/conf.d/20-pdo_sqlsrv.ini -RUN echo "extension = sqlsrv.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"` +RUN echo "; priority=20\nextension=sqlsrv.so\n" > /etc/php/7.3/mods-available/sqlsrv.ini +RUN echo "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/7.3/mods-available/pdo_sqlsrv.ini WORKDIR $PHPSQLDIR/source/sqlsrv -RUN phpize && ./configure LDFLAGS="-lgcov" CXXFLAGS="-O0 --coverage" && make && make install +RUN /usr/bin/phpize && ./configure LDFLAGS="-lgcov" CXXFLAGS="-O0 --coverage" && make && make install WORKDIR $PHPSQLDIR/source/pdo_sqlsrv -RUN phpize && ./configure LDFLAGS="-lgcov" CXXFLAGS="-O0 --coverage" && make && make install +RUN /usr/bin/phpize && ./configure LDFLAGS="-lgcov" CXXFLAGS="-O0 --coverage" && make && make install + +RUN phpenmod sqlsrv pdo_sqlsrv +RUN php --ri sqlsrv && php --ri pdo_sqlsrv # set name of sql server host to use WORKDIR $PHPSQLDIR/test/functional/pdo_sqlsrv diff --git a/Linux-mac-install.md b/Linux-mac-install.md index e34f07b0..2a96c89c 100644 --- a/Linux-mac-install.md +++ b/Linux-mac-install.md @@ -5,13 +5,13 @@ These instructions install PHP 7.3 by default. Note that some supported Linux di ## Contents of this page: -- [Installing the drivers on Ubuntu 16.04, 18.04, and 18.10](#installing-the-drivers-on-ubuntu-1604-1804-and-1810) +- [Installing the drivers on Ubuntu 16.04, 18.04, and 19.04](#installing-the-drivers-on-ubuntu-1604-1804-and-1904) - [Installing the drivers on Red Hat 7](#installing-the-drivers-on-red-hat-7) -- [Installing the drivers on Debian 8 and 9](#installing-the-drivers-on-debian-8-and-9) +- [Installing the drivers on Debian 8, 9 and 10](#installing-the-drivers-on-debian-8-9-and-10) - [Installing the drivers on Suse 12 and 15](#installing-the-drivers-on-suse-12-and-15) - [Installing the drivers on macOS Sierra, High Sierra, and Mojave](#installing-the-drivers-on-macos-sierra-high-sierra-and-mojave) -## Installing the drivers on Ubuntu 16.04, 18.04, and 18.10 +## Installing the drivers on Ubuntu 16.04, 18.04, and 19.04 > [!NOTE] > To install PHP 7.1 or 7.2, replace 7.3 with 7.1 or 7.2 in the following commands. @@ -31,10 +31,13 @@ Install the ODBC driver for Ubuntu by following the instructions on the [Linux a sudo pecl install sqlsrv sudo pecl install pdo_sqlsrv sudo su -echo extension=pdo_sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/30-pdo_sqlsrv.ini -echo extension=sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/20-sqlsrv.ini +printf "; priority=20\nextension=sqlsrv.so\n" > /etc/php/7.3/mods-available/sqlsrv.ini +printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/7.3/mods-available/pdo_sqlsrv.ini exit +sudo phpenmod -v 7.3 sqlsrv pdo_sqlsrv ``` +If there is only one PHP version in the system then the last step can be simplified to `phpenmod sqlsrv pdo_sqlsrv`. + ### Step 4. Install Apache and configure driver loading ``` sudo su @@ -42,8 +45,6 @@ apt-get install libapache2-mod-php7.3 apache2 a2dismod mpm_event a2enmod mpm_prefork a2enmod php7.3 -echo "extension=pdo_sqlsrv.so" >> /etc/php/7.3/apache2/conf.d/30-pdo_sqlsrv.ini -echo "extension=sqlsrv.so" >> /etc/php/7.3/apache2/conf.d/20-sqlsrv.ini exit ``` ### Step 5. Restart Apache and test the sample script @@ -91,8 +92,8 @@ exit An issue in PECL may prevent correct installation of the latest version of the drivers even if you have upgraded GCC. To install, download the packages and compile manually (similar steps for pdo_sqlsrv): ``` pecl download sqlsrv -tar xvzf sqlsrv-5.6.1.tgz -cd sqlsrv-5.6.1/ +tar xvzf sqlsrv-5.7.0.tgz +cd sqlsrv-5.7.0/ phpize ./configure --with-php-config=/usr/bin/php-config make @@ -116,7 +117,7 @@ sudo apachectl restart ``` To test your installation, see [Testing your installation](#testing-your-installation) at the end of this document. -## Installing the drivers on Debian 8 and 9 +## Installing the drivers on Debian 8, 9 and 10 > [!NOTE] > To install PHP 7.1 or 7.2, replace 7.3 in the following commands with 7.1 or 7.2. @@ -145,10 +146,13 @@ locale-gen sudo pecl install sqlsrv sudo pecl install pdo_sqlsrv sudo su -echo extension=pdo_sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/30-pdo_sqlsrv.ini -echo extension=sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/20-sqlsrv.ini +printf "; priority=20\nextension=sqlsrv.so\n" > /etc/php/7.3/mods-available/sqlsrv.ini +printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/7.3/mods-available/pdo_sqlsrv.ini exit +sudo phpenmod -v 7.3 sqlsrv pdo_sqlsrv ``` +If there is only one PHP version in the system then the last step can be simplified to `phpenmod sqlsrv pdo_sqlsrv`. + ### Step 4. Install Apache and configure driver loading ``` sudo su @@ -156,8 +160,6 @@ apt-get install libapache2-mod-php7.3 apache2 a2dismod mpm_event a2enmod mpm_prefork a2enmod php7.3 -echo "extension=pdo_sqlsrv.so" >> /etc/php/7.3/apache2/conf.d/30-pdo_sqlsrv.ini -echo "extension=sqlsrv.so" >> /etc/php/7.3/apache2/conf.d/20-sqlsrv.ini ``` ### Step 5. Restart Apache and test the sample script ``` @@ -168,7 +170,7 @@ To test your installation, see [Testing your installation](#testing-your-install ## Installing the drivers on Suse 12 and 15 > [!NOTE] -> In the following instructions, replace with your version of Suse - if you are using Suse Enterprise Linux 15, it will be SLE_15 or SLE_15_SP1, and similarly for other versions. Not all versions of PHP are available for all versions of Suse Linux - please refer to `http://download.opensuse.org/repositories/devel:/languages:/php` to see which versions of Suse have the default version PHP available, or to `http://download.opensuse.org/repositories/devel:/languages:/php:/` to see which other versions of PHP are available for which versions of Suse. +> In the following instructions, replace with your version of Suse - if you are using Suse Enterprise Linux 15, it will be SLE_15 or SLE_15_SP1. For Suse 12, use SLE_12_SP4 (or above if applicable). Not all versions of PHP are available for all versions of Suse Linux - please refer to `http://download.opensuse.org/repositories/devel:/languages:/php` to see which versions of Suse have the default version PHP available, or to `http://download.opensuse.org/repositories/devel:/languages:/php:/` to see which other versions of PHP are available for which versions of Suse. > [!NOTE] > Packages for PHP 7.3 are not available for Suse 12. diff --git a/README.md b/README.md index 67183a90..842a99b1 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,16 @@ Thank you for taking the time to participate in our last survey. You can continu ### Status of Most Recent Builds -| AppVeyor (Windows) | Travis CI (Linux) | Coverage (Windows) | Coverage (Linux) | -|--------------------------|--------------------------|---------------------------------------|-------------------------------------------| -| [![av-image][]][av-site] | [![tv-image][]][tv-site] | [![Coverage Codecov][]][codecov-site] | [![Coverage Coveralls][]][coveralls-site] | +Azure Pipelines | AppVeyor (Windows) | Travis CI (Linux) | Coverage (Windows) | Coverage (Linux) | +|---------------------|--------------------------|--------------------------|---------------------------------------|-------------------------------------------| +| [![az-image][]][az-site] | [![av-image][]][av-site] | [![tv-image][]][tv-site] | [![Coverage Codecov][]][codecov-site] | [![Coverage Coveralls][]][coveralls-site] | [av-image]: https://ci.appveyor.com/api/projects/status/vo4rfei6lxlamrnc?svg=true [av-site]: https://ci.appveyor.com/project/msphpsql/msphpsql/branch/dev [tv-image]: https://travis-ci.org/microsoft/msphpsql.svg?branch=dev [tv-site]: https://travis-ci.org/microsoft/msphpsql/ +[az-site]: https://dev.azure.com/sqlclientdrivers-ci/msphpsql/_build/latest?definitionId=6&branchName=dev +[az-image]: https://dev.azure.com/sqlclientdrivers-ci/msphpsql/_apis/build/status/Microsoft.msphpsql?branchName=dev [Coverage Coveralls]: https://coveralls.io/repos/github/microsoft/msphpsql/badge.svg?branch=dev [coveralls-site]: https://coveralls.io/github/microsoft/msphpsql?branch=dev [Coverage Codecov]: https://codecov.io/gh/microsoft/msphpsql/branch/dev/graph/badge.svg diff --git a/appveyor.yml b/appveyor.yml index ee9f2eb3..ae779335 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -81,10 +81,10 @@ install: } Else { $env:PHP_VERSION=$env:PHP_MAJOR_VER + '.' + $env:PHP_MINOR_VER; } - - echo Downloading MSODBCSQL 17.2 + - echo Downloading MSODBCSQL 17 # AppVeyor build works are x64 VMs and 32-bit ODBC driver cannot be installed on it - - ps: (new-object net.webclient).DownloadFile('https://download.microsoft.com/download/E/6/B/E6BFDC7A-5BCD-4C51-9912-635646DA801E/en-US/msodbcsql_17.2.0.1_x64.msi', 'c:\projects\msodbcsql_17.2.0.1_x64.msi') - - cmd /c start /wait msiexec /i "c:\projects\msodbcsql_17.2.0.1_x64.msi" /q IACCEPTMSODBCSQLLICENSETERMS=YES ADDLOCAL=ALL + - ps: (new-object net.webclient).DownloadFile('https://download.microsoft.com/download/E/6/B/E6BFDC7A-5BCD-4C51-9912-635646DA801E/en-US/msodbcsql_17.4.1.1_x64.msi', 'c:\projects\msodbcsql_17.4.1.1_x64.msi') + - cmd /c start /wait msiexec /i "c:\projects\msodbcsql_17.4.1.1_x64.msi" /q IACCEPTMSODBCSQLLICENSETERMS=YES ADDLOCAL=ALL - echo Checking the version of MSODBCSQL - reg query "HKLM\SOFTWARE\ODBC\odbcinst.ini\ODBC Driver 17 for SQL Server" - dir %WINDIR%\System32\msodbcsql*.dll diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 00000000..3f048788 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,312 @@ +# Add steps that build, run tests, deploy, and more: +# https://aka.ms/yaml + +variables: + phpVersion: 7.3 + server: 'localhost,1433' + host: 'sql1' + sqlsrv_db: 'sqlsrv_testdb' + pdo_sqlsrv_db: 'pdo_sqlsrv_testdb' + uid: 'sa' + pwd: 'Password456!' + +trigger: +- dev + +jobs: +- job: macOS + pool: + vmImage: 'macOS-10.13' + steps: + - checkout: self + clean: true + fetchDepth: 1 + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.6' + architecture: 'x64' + + - script: | + brew tap + brew tap homebrew/core + brew install autoconf automake libtool + brew install php@$(phpVersion) + php -v + displayName: 'Install PHP' + + - script: | + echo ready to build extensions + cd $(Build.SourcesDirectory)/source + chmod a+x packagize.sh + ./packagize.sh + + cd $(Build.SourcesDirectory)/source/sqlsrv + ls -al + phpize && ./configure && make && sudo make install + cp run-tests.php $(Build.SourcesDirectory)/test/functional/sqlsrv + + cd $(Build.SourcesDirectory)/source/pdo_sqlsrv + ls -al + phpize && ./configure && make && sudo make install + cp run-tests.php $(Build.SourcesDirectory)/test/functional/pdo_sqlsrv + + echo extension=pdo_sqlsrv.so >> `php --ini | grep "Loaded Configuration File" | sed -e "s|.*:\s*||"` + echo extension=sqlsrv.so >> `php --ini | grep "Loaded Configuration File" | sed -e "s|.*:\s*||"` + + php --ri sqlsrv + php --ri pdo_sqlsrv + displayName: 'Build and install drivers' + +- job: Linux + pool: + vmImage: 'ubuntu-16.04' + steps: + - checkout: self + clean: true + fetchDepth: 1 + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.6' + architecture: 'x64' + + - script: | + sudo update-alternatives --set php /usr/bin/php$(phpVersion) + sudo update-alternatives --set phar /usr/bin/phar$(phpVersion) + sudo update-alternatives --set phpdbg /usr/bin/phpdbg$(phpVersion) + sudo update-alternatives --set php-cgi /usr/bin/php-cgi$(phpVersion) + sudo update-alternatives --set phar.phar /usr/bin/phar.phar$(phpVersion) + php -version + displayName: 'Use PHP version $(phpVersion)' + + - script: | + echo install ODBC and dependencies + sudo apt-get purge unixodbc + sudo apt autoremove + sudo curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - + curl https://packages.microsoft.com/config/ubuntu/16.04/prod.list > mssql-release.list + sudo mv mssql-release.list /etc/apt/sources.list.d/ + sudo apt-get update + sudo ACCEPT_EULA=Y apt-get install msodbcsql17 mssql-tools + echo 'export PATH="$PATH:/opt/mssql-tools/bin"' >> ~/.bash_profile + echo 'export PATH="$PATH:/opt/mssql-tools/bin"' >> ~/.bashrc + source ~/.bashrc + sudo apt-get install unixodbc-dev + odbcinst --j + odbcinst -q -d -n "ODBC Driver 17 for SQL Server" + displayName: 'Install prerequisites' + + - script: | + docker pull mcr.microsoft.com/mssql/server:2017-latest + docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=$(pwd)' -p 1433:1433 -h $(host) --name=$(host) -d mcr.microsoft.com/mssql/server:2017-latest + docker ps -a + sleep 5 + docker exec -t $(host) /opt/mssql-tools/bin/sqlcmd -S $(server) -U $(uid) -P $(pwd) -Q 'select @@Version' + displayName: 'Run SQL Server for Linux' + + - script: | + sudo sed -i 's/# en_US ISO-8859-1/en_US ISO-8859-1/g' /etc/locale.gen + sudo locale-gen en_US + sudo locale-gen en_US.UTF-8 + export LANG='en_US.UTF-8' + export LANGUAGE='en_US:en' + export LC_ALL='en_US.UTF-8' + displayName: 'Generate locales for testing' + + - script: | + echo setting env variables + export TEST_PHP_SQL_SERVER='$(server)' + export TEST_PHP_SQL_UID='$(uid)' + export TEST_PHP_SQL_PWD='$(pwd)' + + cd $(Build.SourcesDirectory)/test/functional/setup + python ./setup_dbs.py -dbname $(sqlsrv_db) + python ./setup_dbs.py -dbname $(pdo_sqlsrv_db) + displayName: 'Set up test databases' + + - script: | + echo ready to build extensions + cd $(Build.SourcesDirectory)/source + chmod a+x packagize.sh + ./packagize.sh + + dest=`php --ini | grep "Scan for additional .ini files" | sudo sed -e "s|.*:\s*||"`/ + + cd $(Build.SourcesDirectory)/source/sqlsrv + ls -al + phpize && ./configure && make && sudo make install + cp run-tests.php $(Build.SourcesDirectory)/test/functional/sqlsrv + echo extension=sqlsrv.so >> 20-sqlsrv.ini + + echo copying sqlsrv to $dest + sudo cp 20-sqlsrv.ini $dest + + cd $(Build.SourcesDirectory)/source/pdo_sqlsrv + ls -al + phpize && ./configure && make && sudo make install + cp run-tests.php $(Build.SourcesDirectory)/test/functional/pdo_sqlsrv + echo extension=pdo_sqlsrv.so >> 30-pdo_sqlsrv.ini + + echo copying pdo_sqlsrv to $dest + sudo cp 30-pdo_sqlsrv.ini $dest + + php --ri sqlsrv + php --ri pdo_sqlsrv + displayName: 'Build and install drivers' + + - script: | + cd $(Build.SourcesDirectory)/test/functional/sqlsrv + sed -i -e 's/TARGET_SERVER/'"$(server)"'/g' MsSetup.inc + sed -i -e 's/TARGET_DATABASE/'"$(sqlsrv_db)"'/g' MsSetup.inc + sed -i -e 's/TARGET_USERNAME/'"$(uid)"'/g' MsSetup.inc + sed -i -e 's/TARGET_PASSWORD/'"$(pwd)"'/g' MsSetup.inc + + php run-tests.php -P ./*.phpt 2>&1 | tee ../sqlsrv.log + displayName: 'Run sqlsrv functional tests' + + - script: | + cd $(Build.SourcesDirectory)/test/functional/pdo_sqlsrv + sed -i -e 's/TARGET_SERVER/'"$(server)"'/g' MsSetup.inc + sed -i -e 's/TARGET_DATABASE/'"$(pdo_sqlsrv_db)"'/g' MsSetup.inc + sed -i -e 's/TARGET_USERNAME/'"$(uid)"'/g' MsSetup.inc + sed -i -e 's/TARGET_PASSWORD/'"$(pwd)"'/g' MsSetup.inc + + php run-tests.php -P ./*.phpt 2>&1 | tee ../pdo_sqlsrv.log + displayName: 'Run pdo_sqlsrv functional tests' + + - script: | + cd $(Build.SourcesDirectory)/test/functional/ + for f in sqlsrv/*.diff; do ls $f 2>/dev/null; cat $f 2>/dev/null; echo ''; done || true + for f in pdo_sqlsrv/*.diff; do ls $f 2>/dev/null; cat $f 2>/dev/null; echo ''; done || true + python output.py + ls -l *.xml + displayName: 'Processing test results' + + - task: PublishTestResults@2 + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '*.xml' + failTaskOnFailedTests: true + searchFolder: '$(Build.SourcesDirectory)/test/functional/' + + - script: | + docker stop $(host) + docker rm $(host) + displayName: 'Stop SQL Server for Linux' + condition: always() + +- job: Windows + pool: + vmImage: 'vs2017-win2016' + steps: + - checkout: self + clean: true + fetchDepth: 1 + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.6' + architecture: 'x64' + + - script: | + dir C:\tools\php\php* + dir C:\tools\php\ext\ + echo extension_dir=C:\tools\php\ext >> C:\tools\php\php.ini + php --ini + php -v + displayName: 'Check PHP' + + - powershell: | + cd $(Build.SourcesDirectory)\test\functional\sqlsrv + (Get-Content .\MsSetup.inc) | ForEach-Object { $_ -replace "TARGET_SERVER", "$(host)" -replace "TARGET_DATABASE", "$(sqlsrv_db)" -replace "TARGET_USERNAME", "$(uid)" -replace "TARGET_PASSWORD", "$(pwd)" } | Set-Content .\MsSetup.inc + Select-String $(host) .\MsSetup.inc + Select-String $(sqlsrv_db) .\MsSetup.inc + cd $(Build.SourcesDirectory)\test\functional\pdo_sqlsrv + (Get-Content .\MsSetup.inc) | ForEach-Object { $_ -replace "TARGET_SERVER", "$(host)" -replace "TARGET_DATABASE", "$(pdo_sqlsrv_db)" -replace "TARGET_USERNAME", "$(uid)" -replace "TARGET_PASSWORD", "$(pwd)" } | Set-Content .\MsSetup.inc + Select-String $(host) .\MsSetup.inc + Select-String $(pdo_sqlsrv_db) .\MsSetup.inc + displayName: 'Update connection credentials' + condition: false + + - powershell: | + $client = New-Object Net.WebClient + $client.DownloadFile('https://download.microsoft.com/download/E/6/B/E6BFDC7A-5BCD-4C51-9912-635646DA801E/en-US/msodbcsql_17.3.1.1_x64.msi', 'msodbcsql_17.3.1.1_x64.msi') + $client.DownloadFile('https://download.microsoft.com/download/D/5/E/D5EEF288-A277-45C8-855B-8E2CB7E25B96/x64/msodbcsql.msi', 'msodbcsql_13.1.msi') + $client.DownloadFile('https://download.microsoft.com/download/4/C/C/4CC1A229-3C56-4A7F-A3BA-F903C73E5895/EN/x64/MsSqlCmdLnUtils.msi', 'MsSqlCmdLnUtils.msi') + dir *.msi + displayName: 'Download ODBC msi and sql tools msi' + condition: false + + - script: | + msiexec /i "msodbcsql_17.3.1.1_x64.msi" /q IACCEPTMSODBCSQLLICENSETERMS=YES ADDLOCAL=ALL + reg query "HKLM\SOFTWARE\ODBC\odbcinst.ini\ODBC Driver 17 for SQL Server" + dir %WINDIR%\System32\msodbcsql*.dll + displayName: 'Install ODBC driver' + condition: false + + # TOFIX: Install ODBC 13.1 because of SQLCMD 15 installation bug -- this step should be removed later + - script: msiexec /i "msodbcsql_13.1.msi" /q IACCEPTMSODBCSQLLICENSETERMS=YES ADDLOCAL=ALL + condition: false + + # FOR SOME REASON the set up did not set the PATH + - script: | + msiexec /i "MsSqlCmdLnUtils.msi" /qn IACCEPTMSSQLCMDLNUTILSLICENSETERMS=YES + displayName: 'Install SQL command line utilities version 15' + condition: false + + - powershell: | + $client = New-Object Net.WebClient + $client.Headers.Add("user-agent", "azure pipeline build") + $client.DownloadFile("https://windows.php.net/downloads/releases/sha256sum.txt", "sha256sum.txt") + $env:VERSION=type sha256sum.txt | where { $_ -match "php-($(phpVersion)\.\d+)-src" } | foreach { $matches[1] } + Write-Host "Latest PHP $(phpVersion) is ${env:VERSION}" + cd $(Build.SourcesDirectory)/buildscripts/ + python builddrivers.py --PHPVER=${env:VERSION} --DRIVER=sqlsrv --ARCH=x64 --THREAD=nts --SOURCE=$(Build.SourcesDirectory)/source --TESTING --NO_RENAME + dir *sqlsrv*.dll + python builddrivers.py --PHPVER=${env:VERSION} --DRIVER=pdo_sqlsrv --ARCH=x64 --THREAD=nts --SOURCE=$(Build.SourcesDirectory)/source --TESTING --NO_RENAME + cp php-sdk\phpdev\vc15\x64\php-${env:VERSION}-src\run-tests.php $(Build.SourcesDirectory)\test\functional\sqlsrv + cp php-sdk\phpdev\vc15\x64\php-${env:VERSION}-src\run-tests.php $(Build.SourcesDirectory)\test\functional\pdo_sqlsrv + dir *sqlsrv*.dll + cp *sqlsrv*.dll C:\tools\php\ext\ + displayName: 'Build drivers (separately) for the latest version of PHP $(phpVersion)' + + - script: | + echo extension=php_sqlsrv.dll >> C:\tools\php\php.ini + echo extension=php_pdo_sqlsrv.dll >> C:\tools\php\php.ini + php --ri sqlsrv + php --ri pdo_sqlsrv + displayName: 'Load drivers' + + - script: | + docker pull microsoft/mssql-server-windows-developer + docker run -d --name sqlcontainer -h $(host) -p 1433:1433 -e sa_password=$(pwd) -e ACCEPT_EULA=Y microsoft/mssql-server-windows-developer + docker ps -a + displayName: 'Run SQL Server for Windows Server' + condition: false + + - script: | + set path=C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;%path% + sqlcmd -S $(host) -U $(uid) -P $(pwd) -Q "SELECT @@Version" + set TEST_PHP_SQL_SERVER=$(host) + set TEST_PHP_SQL_UID=$(uid) + set TEST_PHP_SQL_PWD=$(pwd) + cd $(Build.SourcesDirectory)\test\functional\setup + python setup_dbs.py -dbname $(sqlsrv_db) + python setup_dbs.py -dbname $(pdo_sqlsrv_db) + displayName: 'Set up test databases' + condition: false + + - script: | + cd $(Build.SourcesDirectory)\test\functional\sqlsrv + php run-tests.php -P sqlsrv_client_info.phpt + cd $(Build.SourcesDirectory)\test\functional\pdo_sqlsrv + php run-tests.php -P pdo_getAttribute_clientInfo.phpt + displayName: 'Smoke testing' + condition: false + + - script: | + docker stop sqlcontainer + docker rm sqlcontainer + displayName: 'Stop SQL Server for Windows Server' + condition: false \ No newline at end of file diff --git a/buildscripts/buildtools.py b/buildscripts/buildtools.py index 59f8b8f5..17713f24 100644 --- a/buildscripts/buildtools.py +++ b/buildscripts/buildtools.py @@ -42,7 +42,8 @@ class BuildUtil(object): self.thread = thread.lower() self.no_rename = no_rename self.debug_enabled = debug_enabled - + self.vc = '' + def major_version(self): """Return the major version number based on the PHP version.""" return self.phpver[0:3] @@ -66,17 +67,56 @@ class BuildUtil(object): version = self.version_label() return 'php_' + driver + '_' + version + '_' + self.thread + suffix - def compiler_version(self): - """Return the appropriate compiler version based on PHP version.""" - VC = 'vc14' - version = self.version_label() - if version >= '72': # Compiler version for PHP 7.2 or above - VC = 'vc15' - return VC + def determine_compiler(self, sdk_dir, vs_ver): + """Return the compiler version using vswhere.exe.""" + vswhere = os.path.join(sdk_dir, 'php-sdk', 'bin', 'vswhere.exe') + if not os.path.exists(vswhere): + print('Could not find ' + vswhere) + exit(1) - def phpsrc_root(self, sdk_dir): + # If both VS 2017 and VS 2019 are installed, if we check only version 15, + # both versions are returned. + # For example, temp.txt would have the following values (in this order): + # 16.1.29009.5 + # 15.9.28307.344 + # But if only VS 2017 is present, temp.txt will only have one value like this: + # 15.9.28307.344 + # Likewise, if only VS 2019 is present, temp.txt contains only the one for 16. + # We can achieve the above by checking for version [15,16), in which case + # even if both compilers are present, it only returns one. If only VS 2019 + # exists, temp.txt is empty + command = '{0} -version [{1},{2}) -property installationVersion '.format(vswhere, vs_ver, vs_ver + 1) + os.system(command + ' > temp.txt') + + # Read the first line from temp.txt + with open('temp.txt', 'r') as f: + ver = f.readline() + print('Version: ' + ver) + vc = ver[:2] + if vc == '15': + return 'vc15' + else: # For VS2019, it's 'vs' instead of 'vc' + return 'vs16' + + def compiler_version(self, sdk_dir): + """Return the appropriate compiler version based on PHP version.""" + if self.vc is '': + VC = 'vc14' + version = self.version_label() + if version >= '72': # Compiler version for PHP 7.2 or above + VC = 'vc15' + if version == '74': + # Compiler version for PHP 7.4 or above + # Can be compiled using VS 2017 or VS 2019 + print('Checking compiler versions...') + VC = self.determine_compiler(sdk_dir, 15) + self.vc = VC + print('Compiler: ' + self.vc) + return self.vc + + def phpsrc_root(self, sdk_dir): """Return the path to the PHP source folder based on *sdk_dir*.""" - vc = self.compiler_version() + vc = self.compiler_version(sdk_dir) return os.path.join(sdk_dir, 'php-sdk', 'phpdev', vc, self.arch, 'php-'+self.phpver+'-src') def build_abs_path(self, sdk_dir): @@ -97,6 +137,10 @@ class BuildUtil(object): def remove_old_builds(self, sdk_dir): """Remove the extensions, e.g. the driver subfolders in php-7.*-src\ext.""" + if not os.path.exists(os.path.join(sdk_dir, 'php-sdk')): + print('No old builds to be removed...') + return + print('Removing old builds...') phpsrc = self.phpsrc_root(sdk_dir) @@ -117,6 +161,10 @@ class BuildUtil(object): """Remove all binaries and source code in the Release* or Debug* folders according to the current configuration """ + if not os.path.exists(os.path.join(sdk_dir, 'php-sdk')): + print('No old builds to be removed...') + return + print('Removing previous build...') build_dir = self.build_abs_path(sdk_dir) if not os.path.exists(build_dir): @@ -282,7 +330,7 @@ class BuildUtil(object): else: # pdo_sqlsrv cmd_line = ' --enable-pdo --with-pdo-sqlsrv=shared ' + cmd_line - cmd_line = 'cscript configure.js --disable-all --enable-cli --enable-cgi --enable-embed' + cmd_line + cmd_line = 'cscript configure.js --disable-all --enable-cli --enable-cgi --enable-json --enable-embed' + cmd_line if self.thread == 'nts': cmd_line = cmd_line + ' --disable-zts' return cmd_line @@ -370,13 +418,16 @@ class BuildUtil(object): os.system('git clone https://github.com/OSTC/php-sdk-binary-tools.git --branch master --single-branch --depth 1 ' + phpSDK) os.chdir(phpSDK) os.system('git pull ') + print('Done cloning the latest php SDK...') # Move the generated batch file to phpSDK for the php starter script + print('Moving the sdk bath file over...') sdk_batch_file = os.path.join(phpSDK, batch_file) if os.path.exists(sdk_batch_file): os.remove(sdk_batch_file) shutil.move(os.path.join(work_dir, batch_file), phpSDK) + print('Checking if source exists...') sdk_source = os.path.join(phpSDK, 'Source') # Sometimes, for various reasons, the Source folder from previous build # might exist in phpSDK. If so, remove it first @@ -386,7 +437,7 @@ class BuildUtil(object): shutil.move(source_dir, phpSDK) # Invoke phpsdk--.bat - vc = self.compiler_version() + vc = self.compiler_version(sdk_dir) starter_script = 'phpsdk-' + vc + '-' + self.arch + '.bat' print('Running starter script: ', starter_script) os.system(starter_script + ' -t ' + batch_file) diff --git a/media/os_development.PNG b/media/os_development.PNG new file mode 100644 index 0000000000000000000000000000000000000000..5bd527901fe02cc98ea93ced0f231a351c8f0e57 GIT binary patch literal 21753 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 Aw*UYD literal 0 HcmV?d00001 diff --git a/media/php_versions.PNG b/media/php_versions.PNG new file mode 100644 index 0000000000000000000000000000000000000000..98416fe81b028903d03f43422c9c075c7b95481d GIT binary patch literal 11432 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 Wpb1Zi;;$iNUocmuIH=4u% literal 0 HcmV?d00001 diff --git a/source/packagize.sh b/source/packagize.sh index dc9051b3..637aefc1 100644 --- a/source/packagize.sh +++ b/source/packagize.sh @@ -1,11 +1,11 @@ #!/bin/bash -if [ "$1" == "" ]; then - cp -rf $PWD/shared $PWD/sqlsrv - cp -rf $PWD/shared $PWD/pdo_sqlsrv +if [[ -z $1 ]]; then + cp -rf "$PWD"/shared "$PWD"/sqlsrv + cp -rf "$PWD"/shared "$PWD"/pdo_sqlsrv else [[ -d $1 ]] || { echo "No such path!"; exit 1; } - cp -rf $PWD/sqlsrv $1 - cp -rf $PWD/pdo_sqlsrv $1 - cp -rf $PWD/shared $1/sqlsrv - cp -rf $PWD/shared $1/pdo_sqlsrv + cp -rf "$PWD"/sqlsrv "$1" + cp -rf "$PWD"/pdo_sqlsrv "$1" + cp -rf "$PWD"/shared "$1"/sqlsrv + cp -rf "$PWD"/shared "$1"/pdo_sqlsrv fi diff --git a/source/pdo_sqlsrv/config.m4 b/source/pdo_sqlsrv/config.m4 index 68dbea0e..75961ad7 100644 --- a/source/pdo_sqlsrv/config.m4 +++ b/source/pdo_sqlsrv/config.m4 @@ -4,7 +4,7 @@ dnl dnl Contents: the code that will go into the configure script, indicating options, dnl external libraries and includes, and what source files are to be compiled. dnl -dnl Microsoft Drivers 5.6 for PHP for SQL Server +dnl Microsoft Drivers 5.7 for PHP for SQL Server dnl Copyright(c) Microsoft Corporation dnl All rights reserved. dnl MIT License diff --git a/source/pdo_sqlsrv/config.w32 b/source/pdo_sqlsrv/config.w32 index 95bff567..1d37593b 100644 --- a/source/pdo_sqlsrv/config.w32 +++ b/source/pdo_sqlsrv/config.w32 @@ -3,7 +3,7 @@ // // Contents: JScript build configuration used by buildconf.bat // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/pdo_sqlsrv/pdo_dbh.cpp b/source/pdo_sqlsrv/pdo_dbh.cpp index 669bc62b..2a0ea479 100644 --- a/source/pdo_sqlsrv/pdo_dbh.cpp +++ b/source/pdo_sqlsrv/pdo_dbh.cpp @@ -3,7 +3,7 @@ // // Contents: Implements the PDO object for PDO_SQLSRV // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -48,6 +48,7 @@ const char AttachDBFileName[] = "AttachDbFileName"; const char Authentication[] = "Authentication"; const char ColumnEncryption[] = "ColumnEncryption"; const char ConnectionPooling[] = "ConnectionPooling"; +const char Language[] = "Language"; const char ConnectRetryCount[] = "ConnectRetryCount"; const char ConnectRetryInterval[] = "ConnectRetryInterval"; const char Database[] = "Database"; @@ -86,7 +87,8 @@ enum PDO_STMT_OPTIONS { PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE, PDO_STMT_OPTION_FETCHES_DATETIME_TYPE, PDO_STMT_OPTION_FORMAT_DECIMALS, - PDO_STMT_OPTION_DECIMAL_PLACES + PDO_STMT_OPTION_DECIMAL_PLACES, + PDO_STMT_OPTION_DATA_CLASSIFICATION }; // List of all the statement options supported by this driver. @@ -103,6 +105,7 @@ const stmt_option PDO_STMT_OPTS[] = { { NULL, 0, PDO_STMT_OPTION_FETCHES_DATETIME_TYPE, std::unique_ptr( new stmt_option_fetch_datetime ) }, { NULL, 0, PDO_STMT_OPTION_FORMAT_DECIMALS, std::unique_ptr( new stmt_option_format_decimals ) }, { NULL, 0, PDO_STMT_OPTION_DECIMAL_PLACES, std::unique_ptr( new stmt_option_decimal_places ) }, + { NULL, 0, PDO_STMT_OPTION_DATA_CLASSIFICATION, std::unique_ptr( new stmt_option_data_classification ) }, { NULL, 0, SQLSRV_STMT_OPTION_INVALID, std::unique_ptr{} }, }; @@ -241,6 +244,15 @@ const connection_option PDO_CONN_OPTS[] = { CONN_ATTR_BOOL, conn_null_func::func }, + { + PDOConnOptionNames::Language, + sizeof( PDOConnOptionNames::Language ), + SQLSRV_CONN_OPTION_LANGUAGE, + ODBCConnOptions::Language, + sizeof( ODBCConnOptions::Language ), + CONN_ATTR_STRING, + conn_str_append_func::func + }, { PDOConnOptionNames::Driver, sizeof(PDOConnOptionNames::Driver), @@ -1126,6 +1138,7 @@ int pdo_sqlsrv_dbh_set_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout case PDO_ATTR_EMULATE_PREPARES: case PDO_ATTR_CURSOR: case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: + case SQLSRV_ATTR_DATA_CLASSIFICATION: { THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_STMT_LEVEL_ATTR ); } @@ -1183,7 +1196,8 @@ int pdo_sqlsrv_dbh_get_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout // Statement level only case PDO_ATTR_EMULATE_PREPARES: case PDO_ATTR_CURSOR: - case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: + case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: + case SQLSRV_ATTR_DATA_CLASSIFICATION: { THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_STMT_LEVEL_ATTR ); } @@ -1584,70 +1598,75 @@ namespace { // Maps the PDO driver specific statement option/attribute constants to the core layer // statement option/attribute constants. -void add_stmt_option_key( _Inout_ sqlsrv_context& ctx, _In_ size_t key, _Inout_ HashTable* options_ht, - _Inout_ zval* data TSRMLS_DC ) +void add_stmt_option_key(_Inout_ sqlsrv_context& ctx, _In_ size_t key, _Inout_ HashTable* options_ht, + _Inout_ zval* data TSRMLS_DC) { - zend_ulong option_key = -1; - switch( key ) { - - case PDO_ATTR_CURSOR: - option_key = SQLSRV_STMT_OPTION_SCROLLABLE; - break; - - case SQLSRV_ATTR_ENCODING: - option_key = PDO_STMT_OPTION_ENCODING; - break; + zend_ulong option_key = -1; + switch (key) { - case SQLSRV_ATTR_QUERY_TIMEOUT: - option_key = SQLSRV_STMT_OPTION_QUERY_TIMEOUT; - break; + case PDO_ATTR_CURSOR: + option_key = SQLSRV_STMT_OPTION_SCROLLABLE; + break; - case PDO_ATTR_STATEMENT_CLASS: - break; + case SQLSRV_ATTR_ENCODING: + option_key = PDO_STMT_OPTION_ENCODING; + break; - case SQLSRV_ATTR_DIRECT_QUERY: - option_key = PDO_STMT_OPTION_DIRECT_QUERY; - break; + case SQLSRV_ATTR_QUERY_TIMEOUT: + option_key = SQLSRV_STMT_OPTION_QUERY_TIMEOUT; + break; - case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: - option_key = PDO_STMT_OPTION_CURSOR_SCROLL_TYPE; - break; + case PDO_ATTR_STATEMENT_CLASS: + break; - case SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE: - option_key = PDO_STMT_OPTION_CLIENT_BUFFER_MAX_KB_SIZE; - break; + case SQLSRV_ATTR_DIRECT_QUERY: + option_key = PDO_STMT_OPTION_DIRECT_QUERY; + break; - case PDO_ATTR_EMULATE_PREPARES: - option_key = PDO_STMT_OPTION_EMULATE_PREPARES; - break; + case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: + option_key = PDO_STMT_OPTION_CURSOR_SCROLL_TYPE; + break; - case SQLSRV_ATTR_FETCHES_NUMERIC_TYPE: - option_key = PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE; - break; + case SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE: + option_key = PDO_STMT_OPTION_CLIENT_BUFFER_MAX_KB_SIZE; + break; - case SQLSRV_ATTR_FETCHES_DATETIME_TYPE: - option_key = PDO_STMT_OPTION_FETCHES_DATETIME_TYPE; - break; + case PDO_ATTR_EMULATE_PREPARES: + option_key = PDO_STMT_OPTION_EMULATE_PREPARES; + break; - case SQLSRV_ATTR_FORMAT_DECIMALS: - option_key = PDO_STMT_OPTION_FORMAT_DECIMALS; - break; + case SQLSRV_ATTR_FETCHES_NUMERIC_TYPE: + option_key = PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE; + break; - case SQLSRV_ATTR_DECIMAL_PLACES: - option_key = PDO_STMT_OPTION_DECIMAL_PLACES; - break; + case SQLSRV_ATTR_FETCHES_DATETIME_TYPE: + option_key = PDO_STMT_OPTION_FETCHES_DATETIME_TYPE; + break; - default: - CHECK_CUSTOM_ERROR( true, ctx, PDO_SQLSRV_ERROR_INVALID_STMT_OPTION ) { - throw core::CoreException(); - } - break; + case SQLSRV_ATTR_FORMAT_DECIMALS: + option_key = PDO_STMT_OPTION_FORMAT_DECIMALS; + break; + + case SQLSRV_ATTR_DECIMAL_PLACES: + option_key = PDO_STMT_OPTION_DECIMAL_PLACES; + break; + + case SQLSRV_ATTR_DATA_CLASSIFICATION: + option_key = PDO_STMT_OPTION_DATA_CLASSIFICATION; + break; + + default: + CHECK_CUSTOM_ERROR(true, ctx, PDO_SQLSRV_ERROR_INVALID_STMT_OPTION) + { + throw core::CoreException(); + } + break; } // if a PDO handled option makes it through (such as PDO_ATTR_STATEMENT_CLASS, just skip it - if( option_key != -1 ) { - zval_add_ref( data ); - core::sqlsrv_zend_hash_index_update(ctx, options_ht, option_key, data TSRMLS_CC ); + if (option_key != -1) { + zval_add_ref(data); + core::sqlsrv_zend_hash_index_update(ctx, options_ht, option_key, data TSRMLS_CC); } } @@ -1673,7 +1692,6 @@ void validate_stmt_options( _Inout_ sqlsrv_context& ctx, _Inout_ zval* stmt_opti ZEND_HASH_FOREACH_KEY_VAL( options_ht, int_key, key, data ) { int type = HASH_KEY_NON_EXISTENT; - int result = 0; type = key ? HASH_KEY_IS_STRING : HASH_KEY_IS_LONG; CHECK_CUSTOM_ERROR(( type != HASH_KEY_IS_LONG ), ctx, PDO_SQLSRV_ERROR_INVALID_STMT_OPTION ) { throw core::CoreException(); diff --git a/source/pdo_sqlsrv/pdo_init.cpp b/source/pdo_sqlsrv/pdo_init.cpp index bee4cfc5..8f374093 100644 --- a/source/pdo_sqlsrv/pdo_init.cpp +++ b/source/pdo_sqlsrv/pdo_init.cpp @@ -3,7 +3,7 @@ // // Contents: initialization routines for PDO_SQLSRV // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -294,6 +294,7 @@ namespace { { "SQLSRV_ATTR_FETCHES_DATETIME_TYPE", SQLSRV_ATTR_FETCHES_DATETIME_TYPE }, { "SQLSRV_ATTR_FORMAT_DECIMALS" , SQLSRV_ATTR_FORMAT_DECIMALS }, { "SQLSRV_ATTR_DECIMAL_PLACES" , SQLSRV_ATTR_DECIMAL_PLACES }, + { "SQLSRV_ATTR_DATA_CLASSIFICATION" , SQLSRV_ATTR_DATA_CLASSIFICATION }, // used for the size for output parameters: PDO::PARAM_INT and PDO::PARAM_BOOL use the default size of int, // PDO::PARAM_STR uses the size of the string in the variable diff --git a/source/pdo_sqlsrv/pdo_parser.cpp b/source/pdo_sqlsrv/pdo_parser.cpp index edd333e4..6a96ace2 100644 --- a/source/pdo_sqlsrv/pdo_parser.cpp +++ b/source/pdo_sqlsrv/pdo_parser.cpp @@ -5,7 +5,7 @@ // // Copyright Microsoft Corporation // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index 773ad953..71c42690 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -3,7 +3,7 @@ // // Contents: Implements the PDOStatement object for the PDO_SQLSRV // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -220,7 +220,7 @@ void meta_data_free( _Inout_ field_meta_data* meta ) sqlsrv_free( meta ); } -zval convert_to_zval( _In_ SQLSRV_PHPTYPE sqlsrv_php_type, _Inout_ void** in_val, _In_opt_ SQLLEN field_len ) +zval convert_to_zval(_Inout_ sqlsrv_stmt* stmt, _In_ SQLSRV_PHPTYPE sqlsrv_php_type, _Inout_ void** in_val, _In_opt_ SQLLEN field_len ) { zval out_zval; ZVAL_UNDEF(&out_zval); @@ -264,15 +264,8 @@ zval convert_to_zval( _In_ SQLSRV_PHPTYPE sqlsrv_php_type, _Inout_ void** in_val break; } case SQLSRV_PHPTYPE_DATETIME: - if (*in_val == NULL) { - - ZVAL_NULL(&out_zval); - } - else { - - out_zval = *(reinterpret_cast(*in_val)); - sqlsrv_free(*in_val); - } + convert_datetime_string_to_zval(stmt, static_cast(*in_val), field_len, out_zval); + sqlsrv_free(*in_val); break; case SQLSRV_PHPTYPE_NULL: ZVAL_NULL(&out_zval); @@ -478,7 +471,6 @@ int pdo_sqlsrv_stmt_describe_col( _Inout_ pdo_stmt_t *stmt, _In_ int colno TSRML // Set the name column_data->name = zend_string_init( (const char*)core_meta_data->field_name.get(), core_meta_data->field_name_len, 0 ); - core_meta_data->field_name.reset(); // Set the maxlen column_data->maxlen = ( core_meta_data->field_precision > 0 ) ? core_meta_data->field_precision : core_meta_data->field_size; @@ -593,12 +585,26 @@ int pdo_sqlsrv_stmt_execute( _Inout_ pdo_stmt_t *stmt TSRMLS_DC ) if ( execReturn == SQL_NO_DATA ) { stmt->column_count = 0; stmt->row_count = 0; + driver_stmt->column_count = 0; + driver_stmt->row_count = 0; } else { - stmt->column_count = core::SQLNumResultCols( driver_stmt TSRMLS_CC ); + if (driver_stmt->column_count == ACTIVE_NUM_COLS_INVALID) { + stmt->column_count = core::SQLNumResultCols( driver_stmt TSRMLS_CC ); + driver_stmt->column_count = stmt->column_count; + } + else { + stmt->column_count = driver_stmt->column_count; + } - // return the row count regardless if there are any rows or not - stmt->row_count = core::SQLRowCount( driver_stmt TSRMLS_CC ); + if (driver_stmt->row_count == ACTIVE_NUM_ROWS_INVALID) { + // return the row count regardless if there are any rows or not + stmt->row_count = core::SQLRowCount( driver_stmt TSRMLS_CC ); + driver_stmt->row_count = stmt->row_count; + } + else { + stmt->row_count = driver_stmt->row_count; + } } // workaround for a bug in the PDO driver manager. It is fairly simple to crash the PDO driver manager with @@ -692,11 +698,16 @@ int pdo_sqlsrv_stmt_fetch( _Inout_ pdo_stmt_t *stmt, _In_ enum pdo_fetch_orienta SQLSMALLINT odbc_fetch_ori = pdo_fetch_ori_to_odbc_fetch_ori( ori ); bool data = core_sqlsrv_fetch( driver_stmt, odbc_fetch_ori, offset TSRMLS_CC ); - // support for the PDO rowCount method. Since rowCount doesn't call a method, PDO relies on us to fill the - // pdo_stmt_t::row_count member - if( driver_stmt->past_fetch_end || driver_stmt->cursor_type != SQL_CURSOR_FORWARD_ONLY ) { + // support for the PDO rowCount method. Since rowCount doesn't call a + // method, PDO relies on us to fill the pdo_stmt_t::row_count member + // The if condition was changed from + // `driver_stmt->past_fetch_end || driver_stmt->cursor_type != SQL_CURSOR_FORWARD_ONLY` + // because it caused SQLRowCount to be called at each fetch if using a non-forward cursor + // which is unnecessary and a performance hit + if( driver_stmt->past_fetch_end || driver_stmt->cursor_type == SQL_CURSOR_DYNAMIC) { stmt->row_count = core::SQLRowCount( driver_stmt TSRMLS_CC ); + driver_stmt->row_count = stmt->row_count; // a row_count of -1 means no rows, but we change it to 0 if( stmt->row_count == -1 ) { @@ -815,11 +826,11 @@ int pdo_sqlsrv_stmt_get_col_data( _Inout_ pdo_stmt_t *stmt, _In_ int colno, core_sqlsrv_get_field( driver_stmt, colno, sqlsrv_php_type, false, *(reinterpret_cast(ptr)), reinterpret_cast( len ), true, &sqlsrv_phptype_out TSRMLS_CC ); - if ( ptr ) { - zval* zval_ptr = reinterpret_cast( sqlsrv_malloc( sizeof( zval ))); - *zval_ptr = convert_to_zval( sqlsrv_phptype_out, reinterpret_cast( ptr ), *len ); - *ptr = reinterpret_cast( zval_ptr ); - *len = sizeof( zval ); + if (ptr) { + zval* zval_ptr = reinterpret_cast(sqlsrv_malloc(sizeof(zval))); + *zval_ptr = convert_to_zval(driver_stmt, sqlsrv_phptype_out, reinterpret_cast(ptr), *len); + *ptr = reinterpret_cast(zval_ptr); + *len = sizeof(zval); } return 1; @@ -894,6 +905,10 @@ int pdo_sqlsrv_stmt_set_attr( _Inout_ pdo_stmt_t *stmt, _In_ zend_long attr, _In core_sqlsrv_set_decimal_places(driver_stmt, val TSRMLS_CC); break; + case SQLSRV_ATTR_DATA_CLASSIFICATION: + driver_stmt->data_classification = (zend_is_true(val)) ? true : false; + break; + default: THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_INVALID_STMT_ATTR ); break; @@ -993,6 +1008,12 @@ int pdo_sqlsrv_stmt_get_attr( _Inout_ pdo_stmt_t *stmt, _In_ zend_long attr, _In break; } + case SQLSRV_ATTR_DATA_CLASSIFICATION: + { + ZVAL_BOOL(return_value, driver_stmt->data_classification); + break; + } + default: THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_INVALID_STMT_ATTR ); break; @@ -1047,12 +1068,28 @@ int pdo_sqlsrv_stmt_get_col_meta( _Inout_ pdo_stmt_t *stmt, _In_ zend_long colno // initialize the array to nothing, as PDO requires us to create it core::sqlsrv_array_init( *driver_stmt, return_value TSRMLS_CC ); - sqlsrv_malloc_auto_ptr core_meta_data; - - core_meta_data = core_sqlsrv_field_metadata( driver_stmt, (SQLSMALLINT) colno TSRMLS_CC ); + field_meta_data* core_meta_data; + // metadata should have been saved earlier + SQLSRV_ASSERT(colno < driver_stmt->current_meta_data.size(), "pdo_sqlsrv_stmt_get_col_meta: Metadata vector out of sync with column numbers"); + core_meta_data = driver_stmt->current_meta_data[colno]; + // add the following fields: flags, native_type, driver:decl_type, table - add_assoc_long( return_value, "flags", 0 ); + if (driver_stmt->data_classification) { + core_sqlsrv_sensitivity_metadata(driver_stmt); + + // initialize the column data classification array + zval data_classification; + ZVAL_UNDEF(&data_classification); + core::sqlsrv_array_init(*driver_stmt, &data_classification TSRMLS_CC ); + + data_classification::fill_column_sensitivity_array(driver_stmt, (SQLSMALLINT)colno, &data_classification); + + add_assoc_zval(return_value, "flags", &data_classification); + } + else { + add_assoc_long(return_value, "flags", 0); + } // get the name of the data type char field_type_name[SQL_SERVER_IDENT_SIZE_MAX] = {'\0'}; @@ -1090,16 +1127,13 @@ int pdo_sqlsrv_stmt_get_col_meta( _Inout_ pdo_stmt_t *stmt, _In_ zend_long colno if( stmt->columns && stmt->columns[colno].param_type == PDO_PARAM_ZVAL ) { add_assoc_long( return_value, "pdo_type", pdo_type ); } - - // this will ensure that the field_name field, which is an auto pointer gets freed. - (*core_meta_data).~field_meta_data(); } catch( core::CoreException& ) { - + zval_ptr_dtor(return_value); return FAILURE; } catch(...) { - + zval_ptr_dtor(return_value); DIE( "pdo_sqlsrv_stmt_get_col_meta: Unknown exception occurred while retrieving metadata." ); } @@ -1146,6 +1180,9 @@ int pdo_sqlsrv_stmt_next_rowset( _Inout_ pdo_stmt_t *stmt TSRMLS_DC ) // return the row count regardless if there are any rows or not stmt->row_count = core::SQLRowCount( driver_stmt TSRMLS_CC ); + + driver_stmt->column_count = stmt->column_count; + driver_stmt->row_count = stmt->row_count; } catch( core::CoreException& ) { diff --git a/source/pdo_sqlsrv/pdo_util.cpp b/source/pdo_sqlsrv/pdo_util.cpp index e399fda1..6cfb43ac 100644 --- a/source/pdo_sqlsrv/pdo_util.cpp +++ b/source/pdo_sqlsrv/pdo_util.cpp @@ -3,7 +3,7 @@ // // Contents: Utility functions used by both connection or statement functions // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -449,6 +449,18 @@ pdo_error PDO_ERRORS[] = { SQLSRV_ERROR_AAD_MSI_UID_PWD_NOT_NULL, { IMSSP, (SQLCHAR*) "When using ActiveDirectoryMsi Authentication, PWD must be NULL. UID can be NULL, but if not, an empty string is not accepted.", -93, false} }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_PRE_EXECUTION, + { IMSSP, (SQLCHAR*) "The statement must be executed to retrieve Data Classification Sensitivity Metadata.", -94, false} + }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_NOT_AVAILABLE, + { IMSSP, (SQLCHAR*) "Failed to retrieve Data Classification Sensitivity Metadata. If the driver and the server both support the Data Classification feature, check whether the query returns columns with classification information.", -95, false} + }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, + { IMSSP, (SQLCHAR*) "Failed to retrieve Data Classification Sensitivity Metadata: %1!s!", -96, true} + }, { UINT_MAX, {} } }; diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv.h b/source/pdo_sqlsrv/php_pdo_sqlsrv.h index 3e013f95..a6403219 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv.h @@ -6,7 +6,7 @@ // // Contents: Declarations for the extension // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h index 4b50bbc7..6a616da0 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h @@ -6,7 +6,7 @@ // // Contents: Internal declarations for the extension // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -79,7 +79,8 @@ enum PDO_SQLSRV_ATTR { SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, SQLSRV_ATTR_FETCHES_DATETIME_TYPE, SQLSRV_ATTR_FORMAT_DECIMALS, - SQLSRV_ATTR_DECIMAL_PLACES + SQLSRV_ATTR_DECIMAL_PLACES, + SQLSRV_ATTR_DATA_CLASSIFICATION }; // valid set of values for TransactionIsolation connection option diff --git a/source/pdo_sqlsrv/template.rc b/source/pdo_sqlsrv/template.rc index fdbeaa57..2db6a2e9 100644 --- a/source/pdo_sqlsrv/template.rc +++ b/source/pdo_sqlsrv/template.rc @@ -3,7 +3,7 @@ // // Contents: Version resource // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/FormattedPrint.cpp b/source/shared/FormattedPrint.cpp index b664159c..185489d7 100644 --- a/source/shared/FormattedPrint.cpp +++ b/source/shared/FormattedPrint.cpp @@ -6,7 +6,7 @@ // Contents: Contains functions for handling Windows format strings // and UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/FormattedPrint.h b/source/shared/FormattedPrint.h index 907b1647..32f65ebb 100644 --- a/source/shared/FormattedPrint.h +++ b/source/shared/FormattedPrint.h @@ -4,7 +4,7 @@ // Contents: Contains functions for handling Windows format strings // and UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/StringFunctions.cpp b/source/shared/StringFunctions.cpp index 6aac5a05..ac0a8239 100644 --- a/source/shared/StringFunctions.cpp +++ b/source/shared/StringFunctions.cpp @@ -3,7 +3,7 @@ // // Contents: Contains functions for handling UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/StringFunctions.h b/source/shared/StringFunctions.h index a2bfceaa..0eb011c3 100644 --- a/source/shared/StringFunctions.h +++ b/source/shared/StringFunctions.h @@ -3,7 +3,7 @@ // // Contents: Contains functions for handling UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/core_conn.cpp b/source/shared/core_conn.cpp index 3d195fe9..cea5a3dd 100644 --- a/source/shared/core_conn.cpp +++ b/source/shared/core_conn.cpp @@ -3,7 +3,7 @@ // // Contents: Core routines that use connection handles shared between sqlsrv and pdo_sqlsrv // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -386,7 +386,7 @@ SQLRETURN core_odbc_connect( _Inout_ sqlsrv_conn* conn, _Inout_ std::string& con // We only support UTF-8 encoding for connection string. // Convert our UTF-8 connection string to UTF-16 before connecting with SQLDriverConnnectW - wconn_string = utf16_string_from_mbcs_string( SQLSRV_ENCODING_UTF8, conn_str.c_str(), static_cast( conn_str.length() ), &wconn_len ); + wconn_string = utf16_string_from_mbcs_string( SQLSRV_ENCODING_UTF8, conn_str.c_str(), static_cast( conn_str.length() ), &wconn_len, true ); CHECK_CUSTOM_ERROR( wconn_string == 0, conn, SQLSRV_ERROR_CONNECT_STRING_ENCODING_TRANSLATE, get_last_error_message()) { @@ -702,22 +702,32 @@ void core_sqlsrv_get_client_info( _Inout_ sqlsrv_conn* conn, _Out_ zval *client_ bool core_is_conn_opt_value_escaped( _Inout_ const char* value, _Inout_ size_t value_len ) { - // if the value is already quoted, then only analyse the part inside the quotes and return it as - // unquoted since we quote it when adding it to the connection string. - if( value_len > 0 && value[0] == '{' && value[value_len - 1] == '}' ) { - ++value; + if (value_len == 0) { + return true; + } + + if (value_len == 1) { + return (value[0] != '}'); + } + + const char *pstr = value; + if (value_len > 0 && value[0] == '{' && value[value_len - 1] == '}') { + pstr = ++value; value_len -= 2; } - // check to make sure that all right braces are escaped + + const char *pch = strchr(pstr, '}'); size_t i = 0; - while( ( value[i] != '}' || ( value[i] == '}' && value[i+1] == '}' )) && i < value_len ) { - // skip both braces - if( value[i] == '}' ) - ++i; - ++i; - } - if( i < value_len && value[i] == '}' ) { - return false; + + while (pch != NULL && i < value_len) { + i = pch - pstr + 1; + + if (i == value_len || (i < value_len && pstr[i] != '}')) { + return false; + } + + i++; // skip the brace + pch = strchr(pch + 2, '}'); // continue searching } return true; diff --git a/source/shared/core_init.cpp b/source/shared/core_init.cpp index 504a145b..63d08ce9 100644 --- a/source/shared/core_init.cpp +++ b/source/shared/core_init.cpp @@ -3,7 +3,7 @@ // // Contents: common initialization routines shared by PDO and sqlsrv // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/core_results.cpp b/source/shared/core_results.cpp index 93427bd7..1fbe005f 100644 --- a/source/shared/core_results.cpp +++ b/source/shared/core_results.cpp @@ -3,7 +3,7 @@ // // Contents: Result sets // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -243,14 +243,12 @@ std::string getUTF8StringFromString( _In_z_ const SQLWCHAR* source ) { // convert to regular character string first char c_str[4] = ""; - mbstate_t mbs; SQLLEN i = 0; std::string str; while ( source[i] ) { memset( c_str, 0, sizeof( c_str ) ); - DWORD rc; int cch = 0; errno_t err = mplat_wctomb_s( &cch, c_str, sizeof( c_str ), source[i++] ); if ( cch > 0 && err == ERROR_SUCCESS ) diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index 886d5e82..a9e281c4 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -6,7 +6,7 @@ // // Contents: Core routines and constants shared by the Microsoft Drivers for PHP for SQL Server // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -170,7 +170,6 @@ OACR_WARNING_POP // constants for maximums in SQL Server const int SS_MAXCOLNAMELEN = 128; const int SQL_SERVER_MAX_FIELD_SIZE = 8000; -const int SQL_SERVER_MAX_PRECISION = 38; const int SQL_SERVER_MAX_TYPE_SIZE = 0; const int SQL_SERVER_MAX_PARAMS = 2100; const int SQL_SERVER_MAX_MONEY_SCALE = 4; @@ -182,6 +181,11 @@ const int SQL_MAX_ERROR_MESSAGE_LENGTH = SQL_MAX_MESSAGE_LENGTH * 2; // max size of a date time string when converting from a DateTime object to a string const int MAX_DATETIME_STRING_LEN = 256; +// identifier for whether or not we have obtained the number of rows and columns +// of a result +const short ACTIVE_NUM_COLS_INVALID = -99; +const long ACTIVE_NUM_ROWS_INVALID = -99; + // precision and scale for the date time types between servers const int SQL_SERVER_2005_DEFAULT_DATETIME_PRECISION = 23; const int SQL_SERVER_2005_DEFAULT_DATETIME_SCALE = 3; @@ -993,8 +997,6 @@ class sqlsrv_context { SQLSRV_ENCODING encoding_; // encoding of the context }; -const int SQLSRV_OS_VISTA_OR_LATER = 6; // major version for Vista - // maps an IANA encoding to a code page struct sqlsrv_encoding { @@ -1115,6 +1117,7 @@ enum SQLSRV_STMT_OPTIONS { SQLSRV_STMT_OPTION_DATE_AS_STRING, SQLSRV_STMT_OPTION_FORMAT_DECIMALS, SQLSRV_STMT_OPTION_DECIMAL_PLACES, + SQLSRV_STMT_OPTION_DATA_CLASSIFICATION, // Driver specific connection options SQLSRV_STMT_OPTION_DRIVER_SPECIFIC = 1000, @@ -1131,6 +1134,7 @@ const char Authentication[] = "Authentication"; const char Driver[] = "Driver"; const char CharacterSet[] = "CharacterSet"; const char ConnectionPooling[] = "ConnectionPooling"; +const char Language[] = "Language"; const char ColumnEncryption[] = "ColumnEncryption"; const char ConnectRetryCount[] = "ConnectRetryCount"; const char ConnectRetryInterval[] = "ConnectRetryInterval"; @@ -1163,6 +1167,7 @@ enum SQLSRV_CONN_OPTIONS { SQLSRV_CONN_OPTION_ACCESS_TOKEN, SQLSRV_CONN_OPTION_CHARACTERSET, SQLSRV_CONN_OPTION_CONN_POOLING, + SQLSRV_CONN_OPTION_LANGUAGE, SQLSRV_CONN_OPTION_DATABASE, SQLSRV_CONN_OPTION_ENCRYPT, SQLSRV_CONN_OPTION_FAILOVER_PARTNER, @@ -1314,6 +1319,11 @@ struct stmt_option_decimal_places : public stmt_option_functor { virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* opt, _In_ zval* value_z TSRMLS_DC ); }; +struct stmt_option_data_classification : public stmt_option_functor { + + virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* opt, _In_ zval* value_z TSRMLS_DC ); +}; + // used to hold the table for statment options struct stmt_option { @@ -1417,6 +1427,75 @@ struct sqlsrv_output_param { } }; +namespace data_classification { + // *** data classficiation metadata structures and helper methods -- to store and/or process the sensitivity classification data *** + struct name_id_pair; + struct sensitivity_metadata; + + void name_id_pair_free(name_id_pair * pair); + void parse_sensitivity_name_id_pairs(_Inout_ sqlsrv_stmt* stmt, _Inout_ USHORT& numpairs, _Inout_ std::vector>* pairs, _Inout_ unsigned char **pptr TSRMLS_CC); + void parse_column_sensitivity_props(_Inout_ sensitivity_metadata* meta, _Inout_ unsigned char **pptr); + USHORT fill_column_sensitivity_array(_Inout_ sqlsrv_stmt* stmt, _In_ SQLSMALLINT colno, _Inout_ zval *column_data TSRMLS_CC); + + struct name_id_pair { + UCHAR name_len; + sqlsrv_malloc_auto_ptr name; + UCHAR id_len; + sqlsrv_malloc_auto_ptr id; + + name_id_pair() : name_len(0), id_len(0) + { + } + + ~name_id_pair() + { + } + }; + + struct label_infotype_pair { + USHORT label_idx; + USHORT infotype_idx; + + label_infotype_pair() : label_idx(0), infotype_idx(0) + { + } + }; + + struct column_sensitivity { + USHORT num_pairs; + std::vector label_info_pairs; + + column_sensitivity() : num_pairs(0) + { + } + + ~column_sensitivity() + { + label_info_pairs.clear(); + } + }; + + struct sensitivity_metadata { + USHORT num_labels; + std::vector> labels; + USHORT num_infotypes; + std::vector> infotypes; + USHORT num_columns; + std::vector columns_sensitivity; + + sensitivity_metadata() : num_labels(0), num_infotypes(0), num_columns(0) + { + } + + ~sensitivity_metadata() + { + reset(); + } + + void reset(); + }; +} // namespace data_classification + // forward decls struct sqlsrv_result_set; struct field_meta_data; @@ -1427,6 +1506,9 @@ struct sqlsrv_stmt : public sqlsrv_context { void free_param_data( TSRMLS_D ); virtual void new_result_set( TSRMLS_D ); + // free sensitivity classification metadata + void clean_up_sensitivity_metadata(); + sqlsrv_conn* conn; // Connection that created this statement bool executed; // Whether the statement has been executed yet (used for error messages) @@ -1436,13 +1518,15 @@ struct sqlsrv_stmt : public sqlsrv_context { bool has_rows; // Has_rows is set if there are actual rows in the row set bool fetch_called; // Used by core_sqlsrv_get_field to return an informative error if fetch not yet called int last_field_index; // last field retrieved by core_sqlsrv_get_field - bool past_next_result_end; // core_sqlsrv_next_result sets this to true when the statement goes beyond the - // last results + bool past_next_result_end; // core_sqlsrv_next_result sets this to true when the statement goes beyond the last results + short column_count; // Number of columns in the current result set obtained from SQLNumResultCols + long row_count; // Number of rows in the current result set obtained from SQLRowCount unsigned long query_timeout; // maximum allowed statement execution time zend_long buffered_query_limit; // maximum allowed memory for a buffered query (measured in KB) bool date_as_string; // false by default but the user can set this to true to retrieve datetime values as strings bool format_decimals; // false by default but the user can set this to true to add the missing leading zeroes and/or control number of decimal digits to show short decimal_places; // indicates number of decimals shown in fetched results (-1 by default, which means no change to number of decimal digits) + bool data_classification; // false by default but the user can set this to true to retrieve data classification sensitivity metadata // holds output pointers for SQLBindParameter // We use a deque because it 1) provides the at/[] access in constant time, and 2) grows dynamically without moving @@ -1465,6 +1549,9 @@ struct sqlsrv_stmt : public sqlsrv_context { // meta data for current result set std::vector> current_meta_data; + // meta data for data classification + sqlsrv_malloc_auto_ptr current_sensitivity_metadata; + sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ error_callback e, _In_opt_ void* drv TSRMLS_DC ); virtual ~sqlsrv_stmt( void ); @@ -1536,6 +1623,7 @@ bool core_sqlsrv_send_stream_packet( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ); void core_sqlsrv_set_buffered_query_limit( _Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z TSRMLS_DC ); void core_sqlsrv_set_buffered_query_limit( _Inout_ sqlsrv_stmt* stmt, _In_ SQLLEN limit TSRMLS_DC ); void core_sqlsrv_set_decimal_places(_Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z TSRMLS_DC); +void core_sqlsrv_sensitivity_metadata( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ); //********************************************************************************************************************************* // Result Set @@ -1717,7 +1805,9 @@ struct sqlsrv_buffered_result_set : public sqlsrv_result_set { bool convert_string_from_utf16_inplace( _In_ SQLSRV_ENCODING encoding, _Inout_updates_z_(len) char** string, _Inout_ SQLLEN& len); bool validate_string( _In_ char* string, _In_ SQLLEN& len); bool convert_string_from_utf16( _In_ SQLSRV_ENCODING encoding, _In_reads_bytes_(cchInLen) const SQLWCHAR* inString, _In_ SQLINTEGER cchInLen, _Inout_updates_bytes_(cchOutLen) char** outString, _Out_ SQLLEN& cchOutLen ); -SQLWCHAR* utf16_string_from_mbcs_string( _In_ SQLSRV_ENCODING php_encoding, _In_reads_bytes_(mbcs_len) const char* mbcs_string, _In_ unsigned int mbcs_len, _Out_ unsigned int* utf16_len ); +SQLWCHAR* utf16_string_from_mbcs_string( _In_ SQLSRV_ENCODING php_encoding, _In_reads_bytes_(mbcs_len) const char* mbcs_string, _In_ unsigned int mbcs_len, _Out_ unsigned int* utf16_len, bool use_strict_conversion = false ); + +void convert_datetime_string_to_zval(_Inout_ sqlsrv_stmt* stmt, _In_opt_ char* input, _In_ SQLLEN length, _Inout_ zval& out_zval); //********************************************************************************************************************************* // Error handling routines and Predefined Errors @@ -1779,6 +1869,9 @@ enum SQLSRV_ERROR_CODES { SQLSRV_ERROR_EMPTY_ACCESS_TOKEN, SQLSRV_ERROR_INVALID_DECIMAL_PLACES, SQLSRV_ERROR_AAD_MSI_UID_PWD_NOT_NULL, + SQLSRV_ERROR_DATA_CLASSIFICATION_PRE_EXECUTION, + SQLSRV_ERROR_DATA_CLASSIFICATION_NOT_AVAILABLE, + SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, // Driver specific error codes starts from here. SQLSRV_ERROR_DRIVER_SPECIFIC = 1000, @@ -2402,7 +2495,7 @@ namespace core { inline void sqlsrv_add_index_zval( _Inout_ sqlsrv_context& ctx, _Inout_ zval* array, _In_ zend_ulong index, _In_ zval* value TSRMLS_DC) { - int zr = ::add_index_zval( array, index, value ); + int zr = add_index_zval( array, index, value ); CHECK_ZEND_ERROR( zr, ctx, SQLSRV_ERROR_ZEND_HASH ) { throw CoreException(); } @@ -2410,7 +2503,7 @@ namespace core { inline void sqlsrv_add_next_index_zval( _Inout_ sqlsrv_context& ctx, _Inout_ zval* array, _In_ zval* value TSRMLS_DC) { - int zr = ::add_next_index_zval( array, value ); + int zr = add_next_index_zval( array, value ); CHECK_ZEND_ERROR( zr, ctx, SQLSRV_ERROR_ZEND_HASH ) { throw CoreException(); } @@ -2443,6 +2536,14 @@ namespace core { } } + inline void sqlsrv_add_assoc_zval( _Inout_ sqlsrv_context& ctx, _Inout_ zval* array_z, _In_ const char* key, _In_ zval* val TSRMLS_DC ) + { + int zr = ::add_assoc_zval(array_z, key, val); + CHECK_ZEND_ERROR (zr, ctx, SQLSRV_ERROR_ZEND_HASH ) { + throw CoreException(); + } + } + inline void sqlsrv_array_init( _Inout_ sqlsrv_context& ctx, _Out_ zval* new_array TSRMLS_DC) { #if PHP_VERSION_ID < 70300 @@ -2520,7 +2621,7 @@ namespace core { } } - inline void sqlsrv_zend_hash_next_index_insert_mem( _Inout_ sqlsrv_context& ctx, _In_ HashTable* ht, _In_reads_bytes_(data_size) void* data, _In_ uint data_size TSRMLS_DC) + inline void sqlsrv_zend_hash_next_index_insert_mem( _Inout_ sqlsrv_context& ctx, _In_ HashTable* ht, _In_reads_bytes_(data_size) void* data, _In_ size_t data_size TSRMLS_DC) { int zr = (data = ::zend_hash_next_index_insert_mem(ht, data, data_size)) != NULL ? SUCCESS : FAILURE; CHECK_ZEND_ERROR(zr, ctx, SQLSRV_ERROR_ZEND_HASH) { diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index 9cac96a5..13bb2e5e 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -3,7 +3,7 @@ // // Contents: Core routines that use statement handles shared between sqlsrv and pdo_sqlsrv // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -140,10 +140,13 @@ sqlsrv_stmt::sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ error fetch_called( false ), last_field_index( -1 ), past_next_result_end( false ), + column_count( ACTIVE_NUM_COLS_INVALID ), + row_count( ACTIVE_NUM_ROWS_INVALID ), query_timeout( QUERY_TIMEOUT_INVALID ), date_as_string(false), format_decimals(false), // no formatting needed decimal_places(NO_CHANGE_DECIMAL_PLACES), // the default is no formatting to resultset required + data_classification(false), buffered_query_limit( sqlsrv_buffered_result_set::BUFFERED_QUERY_LIMIT_INVALID ), param_ind_ptrs( 10 ), // initially hold 10 elements, which should cover 90% of the cases and only take < 100 byte send_streams_at_exec( true ), @@ -189,6 +192,9 @@ sqlsrv_stmt::~sqlsrv_stmt( void ) current_results = NULL; } + // delete sensivity data + clean_up_sensitivity_metadata(); + invalidate(); zval_ptr_dtor( ¶m_input_strings ); zval_ptr_dtor( &output_params ); @@ -225,6 +231,8 @@ void sqlsrv_stmt::new_result_set( TSRMLS_D ) this->past_next_result_end = false; this->past_fetch_end = false; this->last_field_index = -1; + this->column_count = ACTIVE_NUM_COLS_INVALID; + this->row_count = ACTIVE_NUM_ROWS_INVALID; // delete any current results if( current_results ) { @@ -233,6 +241,9 @@ void sqlsrv_stmt::new_result_set( TSRMLS_D ) current_results = NULL; } + // delete sensivity data + clean_up_sensitivity_metadata(); + // create a new result set if( cursor_type == SQLSRV_CURSOR_BUFFERED ) { sqlsrv_malloc_auto_ptr result; @@ -246,6 +257,15 @@ void sqlsrv_stmt::new_result_set( TSRMLS_D ) } } +// free sensitivity classification metadata +void sqlsrv_stmt::clean_up_sensitivity_metadata() +{ + if (current_sensitivity_metadata) { + current_sensitivity_metadata->~sensitivity_metadata(); + current_sensitivity_metadata.reset(); + } +} + // core_sqlsrv_create_stmt // Common code to allocate a statement from either driver. Returns a valid driver statement object or // throws an exception if an error occurs. @@ -819,9 +839,17 @@ bool core_sqlsrv_fetch( _Inout_ sqlsrv_stmt* stmt, _In_ SQLSMALLINT fetch_orient CHECK_CUSTOM_ERROR( stmt->past_fetch_end, stmt, SQLSRV_ERROR_FETCH_PAST_END ) { throw core::CoreException(); } + // First time only if ( !stmt->fetch_called ) { - SQLSMALLINT has_fields = core::SQLNumResultCols( stmt TSRMLS_CC ); + SQLSMALLINT has_fields; + if (stmt->column_count != ACTIVE_NUM_COLS_INVALID) { + has_fields = stmt->column_count; + } else { + has_fields = core::SQLNumResultCols( stmt TSRMLS_CC ); + stmt->column_count = has_fields; + } + CHECK_CUSTOM_ERROR( has_fields == 0, stmt, SQLSRV_ERROR_NO_FIELDS ) { throw core::CoreException(); } @@ -939,7 +967,7 @@ field_meta_data* core_sqlsrv_field_metadata( _Inout_ sqlsrv_stmt* stmt, _In_ SQL } } - // Set the field name lenth + // Set the field name length meta_data->field_name_len = static_cast( field_name_len ); field_meta_data* result_field_meta_data = meta_data; @@ -947,6 +975,101 @@ field_meta_data* core_sqlsrv_field_metadata( _Inout_ sqlsrv_stmt* stmt, _In_ SQL return result_field_meta_data; } +void core_sqlsrv_sensitivity_metadata( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) +{ + sqlsrv_malloc_auto_ptr dcbuf; + SQLINTEGER dclen = 0; + SQLINTEGER dclenout = 0; + SQLHANDLE ird; + SQLRETURN r; + + try { + if (!stmt->data_classification) { + return; + } + + if (stmt->current_sensitivity_metadata) { + // Already cached, so return + return; + } + + CHECK_CUSTOM_ERROR(!stmt->executed, stmt, SQLSRV_ERROR_DATA_CLASSIFICATION_PRE_EXECUTION) { + throw core::CoreException(); + } + + // Reference: https://docs.microsoft.com/sql/connect/odbc/data-classification + // To retrieve sensitivity classfication data, the first step is to retrieve the IRD(Implementation Row Descriptor) handle by + // calling SQLGetStmtAttr with SQL_ATTR_IMP_ROW_DESC statement attribute + r = ::SQLGetStmtAttr(stmt->handle(), SQL_ATTR_IMP_ROW_DESC, (SQLPOINTER)&ird, SQL_IS_POINTER, 0); + CHECK_SQL_ERROR_OR_WARNING(r, stmt) { + LOG(SEV_ERROR, "core_sqlsrv_sensitivity_metadata: failed in getting Implementation Row Descriptor handle." ); + throw core::CoreException(); + } + + // First call to get dclen + r = ::SQLGetDescFieldW(ird, 0, SQL_CA_SS_DATA_CLASSIFICATION, dcbuf, 0, &dclen); + if (r != SQL_SUCCESS || dclen == 0) { + // log the error first + LOG(SEV_ERROR, "core_sqlsrv_sensitivity_metadata: failed in calling SQLGetDescFieldW first time." ); + + // If this fails, check if it is the "Invalid Descriptor Field error" + SQLRETURN rc; + SQLCHAR state[SQL_SQLSTATE_BUFSIZE] = {'\0'}; + SQLSMALLINT len; + rc = ::SQLGetDiagField(SQL_HANDLE_DESC, ird, 1, SQL_DIAG_SQLSTATE, state, SQL_SQLSTATE_BUFSIZE, &len TSRMLS_CC); + + CHECK_SQL_ERROR_OR_WARNING(rc, stmt) { + throw core::CoreException(); + } + + CHECK_CUSTOM_ERROR(!strcmp("HY091", reinterpret_cast(state)), stmt, SQLSRV_ERROR_DATA_CLASSIFICATION_NOT_AVAILABLE) { + throw core::CoreException(); + } + + CHECK_CUSTOM_ERROR(true, stmt, SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, "Check if ODBC driver or the server supports the Data Classification feature.") { + throw core::CoreException(); + } + } + + // Call again to read SQL_CA_SS_DATA_CLASSIFICATION data + dcbuf = static_cast(sqlsrv_malloc(dclen * sizeof(char))); + + r = ::SQLGetDescFieldW(ird, 0, SQL_CA_SS_DATA_CLASSIFICATION, dcbuf, dclen, &dclenout); + if (r != SQL_SUCCESS) { + LOG(SEV_ERROR, "core_sqlsrv_sensitivity_metadata: failed in calling SQLGetDescFieldW again." ); + + CHECK_CUSTOM_ERROR(true, stmt, SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, "SQLGetDescFieldW failed unexpectedly") { + throw core::CoreException(); + } + } + + // Start parsing the data (blob) + using namespace data_classification; + unsigned char *dcptr = dcbuf; + + sqlsrv_malloc_auto_ptr sensitivity_meta; + sensitivity_meta = new (sqlsrv_malloc(sizeof(sensitivity_metadata))) sensitivity_metadata(); + + // Parse the name id pairs for labels first then info types + parse_sensitivity_name_id_pairs(stmt, sensitivity_meta->num_labels, &sensitivity_meta->labels, &dcptr); + parse_sensitivity_name_id_pairs(stmt, sensitivity_meta->num_infotypes, &sensitivity_meta->infotypes, &dcptr); + + // Next parse the sensitivity properties + parse_column_sensitivity_props(sensitivity_meta, &dcptr); + + unsigned char *dcend = dcbuf; + dcend += dclen; + + CHECK_CUSTOM_ERROR(dcptr != dcend, stmt, SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, "Metadata parsing ends unexpectedly") { + throw core::CoreException(); + } + + stmt->current_sensitivity_metadata = sensitivity_meta; + sensitivity_meta.transferred(); + } catch (core::CoreException& e) { + throw e; + } +} // core_sqlsrv_get_field // Return the value of a column from ODBC @@ -1066,10 +1189,27 @@ void core_sqlsrv_get_field( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i bool core_sqlsrv_has_any_result( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) { - // Use SQLNumResultCols to determine if we have rows or not. - SQLSMALLINT num_cols = core::SQLNumResultCols( stmt TSRMLS_CC ); - // use SQLRowCount to determine if there is a rows status waiting - SQLLEN rows_affected = core::SQLRowCount( stmt TSRMLS_CC ); + SQLSMALLINT num_cols; + SQLLEN rows_affected; + + if (stmt->column_count != ACTIVE_NUM_COLS_INVALID) { + num_cols = stmt->column_count; + } + else { + // Use SQLNumResultCols to determine if we have rows or not + num_cols = core::SQLNumResultCols( stmt TSRMLS_CC ); + stmt->column_count = num_cols; + } + + if (stmt->row_count != ACTIVE_NUM_ROWS_INVALID) { + rows_affected = stmt->row_count; + } + else { + // Use SQLRowCount to determine if there is a rows status waiting + rows_affected = core::SQLRowCount( stmt TSRMLS_CC ); + stmt->row_count = rows_affected; + } + return (num_cols != 0) || (rows_affected > 0); } @@ -1475,6 +1615,16 @@ void stmt_option_decimal_places:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_op core_sqlsrv_set_decimal_places(stmt, value_z TSRMLS_CC); } +void stmt_option_data_classification:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /**/, _In_ zval* value_z TSRMLS_DC ) +{ + if (zend_is_true(value_z)) { + stmt->data_classification = true; + } + else { + stmt->data_classification = false; + } +} + // internal function to release the active stream. Called by each main API function // that will alter the statement and cancel any retrieval of data from a stream. void close_active_stream( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) @@ -1717,54 +1867,36 @@ void core_get_field_common( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i break; } - // get the date as a string (http://msdn2.microsoft.com/en-us/library/ms712387(VS.85).aspx) and - // convert it to a DateTime object and return the created object + // Reference: https://docs.microsoft.com/sql/odbc/reference/appendixes/sql-to-c-timestamp + // Retrieve the datetime data as a string, which may be cached for later use. + // The string is converted to a DateTime object only when it is required to + // be returned as a zval. case SQLSRV_PHPTYPE_DATETIME: { - char field_value_temp[MAX_DATETIME_STRING_LEN] = {'\0'}; - zval params[1]; - zval field_value_temp_z; - zval function_z; + sqlsrv_malloc_auto_ptr field_value_temp; + SQLLEN field_len_temp = 0; - ZVAL_UNDEF( &field_value_temp_z ); - ZVAL_UNDEF( &function_z ); - ZVAL_UNDEF( params ); + field_value_temp = static_cast(sqlsrv_malloc(MAX_DATETIME_STRING_LEN)); + memset(field_value_temp, '\0', MAX_DATETIME_STRING_LEN); - SQLRETURN r = stmt->current_results->get_data( field_index + 1, SQL_C_CHAR, field_value_temp, - MAX_DATETIME_STRING_LEN, field_len, true TSRMLS_CC ); + SQLRETURN r = stmt->current_results->get_data(field_index + 1, SQL_C_CHAR, field_value_temp, MAX_DATETIME_STRING_LEN, &field_len_temp, true TSRMLS_CC); - CHECK_CUSTOM_ERROR(( r == SQL_NO_DATA ), stmt, SQLSRV_ERROR_NO_DATA, field_index ) { + if (r == SQL_NO_DATA || field_len_temp == SQL_NULL_DATA) { + field_value_temp.reset(); + field_len_temp = 0; + } + + CHECK_CUSTOM_ERROR((r == SQL_NO_DATA), stmt, SQLSRV_ERROR_NO_DATA, field_index) { throw core::CoreException(); } - zval_auto_ptr return_value_z; - return_value_z = ( zval * )sqlsrv_malloc( sizeof( zval )); - ZVAL_UNDEF( return_value_z ); + field_value = field_value_temp; + field_value_temp.transferred(); + *field_len = field_len_temp; - if( *field_len == SQL_NULL_DATA ) { - ZVAL_NULL( return_value_z ); - field_value = reinterpret_cast( return_value_z.get()); - return_value_z.transferred(); - break; - } - - // Convert the string date to a DateTime object - core::sqlsrv_zval_stringl( &field_value_temp_z, field_value_temp, *field_len ); - core::sqlsrv_zval_stringl( &function_z, "date_create", sizeof("date_create") - 1 ); - params[0] = field_value_temp_z; - - if( call_user_function( EG( function_table ), NULL, &function_z, return_value_z, 1, - params TSRMLS_CC ) == FAILURE) { - THROW_CORE_ERROR(stmt, SQLSRV_ERROR_DATETIME_CONVERSION_FAILED); - } - - field_value = reinterpret_cast( return_value_z.get()); - return_value_z.transferred(); - zend_string_free( Z_STR( field_value_temp_z )); - zend_string_free( Z_STR( function_z )); break; } - + // create a stream wrapper around the field and return that object to the PHP script. calls to fread // on the stream will result in calls to SQLGetData. This is handled in stream.cpp. See that file // for how these fields are used. @@ -2288,142 +2420,151 @@ void format_decimal_numbers(_In_ SQLSMALLINT decimals_places, _In_ SQLSMALLINT f void finalize_output_parameters( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) { - if( Z_ISUNDEF(stmt->output_params) ) + if (Z_ISUNDEF(stmt->output_params)) return; - HashTable* params_ht = Z_ARRVAL( stmt->output_params ); + HashTable* params_ht = Z_ARRVAL(stmt->output_params); zend_ulong index = -1; zend_string* key = NULL; void* output_param_temp = NULL; - ZEND_HASH_FOREACH_KEY_PTR( params_ht, index, key, output_param_temp ) { - sqlsrv_output_param* output_param = static_cast( output_param_temp ); - zval* value_z = Z_REFVAL_P( output_param->param_z ); - switch( Z_TYPE_P( value_z )) { - case IS_STRING: + try { + ZEND_HASH_FOREACH_KEY_PTR(params_ht, index, key, output_param_temp) { - // adjust the length of the string to the value returned by SQLBindParameter in the ind_ptr parameter - char* str = Z_STRVAL_P( value_z ); - SQLLEN str_len = stmt->param_ind_ptrs[output_param->param_num]; - if( str_len == 0 ) { - core::sqlsrv_zval_stringl( value_z, "", 0 ); - continue; - } - if( str_len == SQL_NULL_DATA ) { - zend_string_release( Z_STR_P( value_z )); - ZVAL_NULL( value_z ); - continue; - } - - // if there was more to output than buffer size to hold it, then throw a truncation error - int null_size = 0; - switch( output_param->encoding ) { - case SQLSRV_ENCODING_UTF8: - null_size = sizeof( SQLWCHAR ); // string isn't yet converted to UTF-8, still UTF-16 - break; - case SQLSRV_ENCODING_SYSTEM: - null_size = 1; - break; - case SQLSRV_ENCODING_BINARY: - null_size = 0; - break; - default: - SQLSRV_ASSERT( false, "Invalid encoding in output_param structure." ); - break; - } - CHECK_CUSTOM_ERROR( str_len > ( output_param->original_buffer_len - null_size ), stmt, - SQLSRV_ERROR_OUTPUT_PARAM_TRUNCATED, output_param->param_num + 1 ) { - throw core::CoreException(); - } - - // For ODBC 11+ see https://msdn.microsoft.com/en-us/library/jj219209.aspx - // A length value of SQL_NO_TOTAL for SQLBindParameter indicates that the buffer contains up to - // output_param->original_buffer_len data and is NULL terminated. - // The IF statement can be true when using connection pooling with unixODBC 2.3.4. - if ( str_len == SQL_NO_TOTAL ) + sqlsrv_output_param* output_param = static_cast(output_param_temp); + zval* value_z = Z_REFVAL_P(output_param->param_z); + switch (Z_TYPE_P(value_z)) { + case IS_STRING: { - str_len = output_param->original_buffer_len - null_size; - } - - if (output_param->encoding == SQLSRV_ENCODING_BINARY) { - // ODBC doesn't null terminate binary encodings, but PHP complains if a string isn't null terminated - // so we do that here if the length of the returned data is less than the original allocation. The - // original allocation null terminates the buffer already. - if (str_len < output_param->original_buffer_len) { - str[str_len] = '\0'; + // adjust the length of the string to the value returned by SQLBindParameter in the ind_ptr parameter + char* str = Z_STRVAL_P(value_z); + SQLLEN str_len = stmt->param_ind_ptrs[output_param->param_num]; + if (str_len == 0) { + core::sqlsrv_zval_stringl(value_z, "", 0); + continue; } - core::sqlsrv_zval_stringl(value_z, str, str_len); - } - else { - param_meta_data metaData = output_param->getMetaData(); - - if (output_param->encoding != SQLSRV_ENCODING_CHAR) { - char* outString = NULL; - SQLLEN outLen = 0; - bool result = convert_string_from_utf16(output_param->encoding, reinterpret_cast(str), int(str_len / sizeof(SQLWCHAR)), &outString, outLen ); - CHECK_CUSTOM_ERROR(!result, stmt, SQLSRV_ERROR_OUTPUT_PARAM_ENCODING_TRANSLATE, get_last_error_message()) { - throw core::CoreException(); - } - - if (stmt->format_decimals && (metaData.sql_type == SQL_DECIMAL || metaData.sql_type == SQL_NUMERIC)) { - format_decimal_numbers(NO_CHANGE_DECIMAL_PLACES, metaData.decimal_digits, outString, &outLen); - } - - core::sqlsrv_zval_stringl(value_z, outString, outLen); - sqlsrv_free(outString); + if (str_len == SQL_NULL_DATA) { + zend_string_release(Z_STR_P(value_z)); + ZVAL_NULL(value_z); + continue; } - else { - if (stmt->format_decimals && (metaData.sql_type == SQL_DECIMAL || metaData.sql_type == SQL_NUMERIC)) { - format_decimal_numbers(NO_CHANGE_DECIMAL_PLACES, metaData.decimal_digits, str, &str_len); - } + // if there was more to output than buffer size to hold it, then throw a truncation error + int null_size = 0; + switch (output_param->encoding) { + case SQLSRV_ENCODING_UTF8: + null_size = sizeof(SQLWCHAR); // string isn't yet converted to UTF-8, still UTF-16 + break; + case SQLSRV_ENCODING_SYSTEM: + null_size = 1; + break; + case SQLSRV_ENCODING_BINARY: + null_size = 0; + break; + default: + SQLSRV_ASSERT(false, "Invalid encoding in output_param structure."); + break; + } + CHECK_CUSTOM_ERROR(str_len > (output_param->original_buffer_len - null_size), stmt, + SQLSRV_ERROR_OUTPUT_PARAM_TRUNCATED, output_param->param_num + 1) + { + throw core::CoreException(); + } + + // For ODBC 11+ see https://msdn.microsoft.com/en-us/library/jj219209.aspx + // A length value of SQL_NO_TOTAL for SQLBindParameter indicates that the buffer contains up to + // output_param->original_buffer_len data and is NULL terminated. + // The IF statement can be true when using connection pooling with unixODBC 2.3.4. + if (str_len == SQL_NO_TOTAL) { + str_len = output_param->original_buffer_len - null_size; + } + + if (output_param->encoding == SQLSRV_ENCODING_BINARY) { + // ODBC doesn't null terminate binary encodings, but PHP complains if a string isn't null terminated + // so we do that here if the length of the returned data is less than the original allocation. The + // original allocation null terminates the buffer already. + if (str_len < output_param->original_buffer_len) { + str[str_len] = '\0'; + } core::sqlsrv_zval_stringl(value_z, str, str_len); } - } - } - break; - case IS_LONG: - // for a long or a float, simply check if NULL was returned and set the parameter to a PHP null if so - if( stmt->param_ind_ptrs[output_param->param_num] == SQL_NULL_DATA ) { - ZVAL_NULL( value_z ); - } - else if( output_param->is_bool ) { - convert_to_boolean( value_z ); - } - else { - ZVAL_LONG( value_z, static_cast( Z_LVAL_P( value_z ))); - } - break; - case IS_DOUBLE: - // for a long or a float, simply check if NULL was returned and set the parameter to a PHP null if so - if (stmt->param_ind_ptrs[output_param->param_num] == SQL_NULL_DATA) { - ZVAL_NULL(value_z); - } - else if (output_param->php_out_type == SQLSRV_PHPTYPE_INT) { - // first check if its value is out of range - double dval = Z_DVAL_P(value_z); - if (dval > INT_MAX || dval < INT_MIN) { - CHECK_CUSTOM_ERROR(true, stmt, SQLSRV_ERROR_DOUBLE_CONVERSION_FAILED) { - throw core::CoreException(); + else { + param_meta_data metaData = output_param->getMetaData(); + + if (output_param->encoding != SQLSRV_ENCODING_CHAR) { + char* outString = NULL; + SQLLEN outLen = 0; + bool result = convert_string_from_utf16(output_param->encoding, reinterpret_cast(str), int(str_len / sizeof(SQLWCHAR)), &outString, outLen); + CHECK_CUSTOM_ERROR(!result, stmt, SQLSRV_ERROR_OUTPUT_PARAM_ENCODING_TRANSLATE, get_last_error_message()) + { + throw core::CoreException(); + } + + if (stmt->format_decimals && (metaData.sql_type == SQL_DECIMAL || metaData.sql_type == SQL_NUMERIC)) { + format_decimal_numbers(NO_CHANGE_DECIMAL_PLACES, metaData.decimal_digits, outString, &outLen); + } + + core::sqlsrv_zval_stringl(value_z, outString, outLen); + sqlsrv_free(outString); + } + else { + if (stmt->format_decimals && (metaData.sql_type == SQL_DECIMAL || metaData.sql_type == SQL_NUMERIC)) { + format_decimal_numbers(NO_CHANGE_DECIMAL_PLACES, metaData.decimal_digits, str, &str_len); + } + + core::sqlsrv_zval_stringl(value_z, str, str_len); } } - // if the output param is a boolean, still convert to - // a long integer first to take care of rounding - convert_to_long(value_z); - if (output_param->is_bool) { - convert_to_boolean(value_z); - } } break; - default: - DIE( "Illegal or unknown output parameter type. This should have been caught in core_sqlsrv_bind_parameter." ); - break; - } - value_z = NULL; - } ZEND_HASH_FOREACH_END(); - + case IS_LONG: + // for a long or a float, simply check if NULL was returned and set the parameter to a PHP null if so + if (stmt->param_ind_ptrs[output_param->param_num] == SQL_NULL_DATA) { + ZVAL_NULL(value_z); + } + else if (output_param->is_bool) { + convert_to_boolean(value_z); + } + else { + ZVAL_LONG(value_z, static_cast(Z_LVAL_P(value_z))); + } + break; + case IS_DOUBLE: + // for a long or a float, simply check if NULL was returned and set the parameter to a PHP null if so + if (stmt->param_ind_ptrs[output_param->param_num] == SQL_NULL_DATA) { + ZVAL_NULL(value_z); + } + else if (output_param->php_out_type == SQLSRV_PHPTYPE_INT) { + // first check if its value is out of range + double dval = Z_DVAL_P(value_z); + if (dval > INT_MAX || dval < INT_MIN) { + CHECK_CUSTOM_ERROR(true, stmt, SQLSRV_ERROR_DOUBLE_CONVERSION_FAILED) + { + throw core::CoreException(); + } + } + // if the output param is a boolean, still convert to + // a long integer first to take care of rounding + convert_to_long(value_z); + if (output_param->is_bool) { + convert_to_boolean(value_z); + } + } + break; + default: + DIE("Illegal or unknown output parameter type. This should have been caught in core_sqlsrv_bind_parameter."); + break; + } + value_z = NULL; + } ZEND_HASH_FOREACH_END(); + } + catch (core::CoreException&) { + // empty the hash table due to exception caught + zend_hash_clean(Z_ARRVAL(stmt->output_params)); + throw; + } // empty the hash table since it's been processed - zend_hash_clean( Z_ARRVAL( stmt->output_params )); + zend_hash_clean(Z_ARRVAL(stmt->output_params)); return; } diff --git a/source/shared/core_stream.cpp b/source/shared/core_stream.cpp index d822d4a8..9ca8e8c0 100644 --- a/source/shared/core_stream.cpp +++ b/source/shared/core_stream.cpp @@ -3,7 +3,7 @@ // // Contents: Implementation of PHP streams for reading SQL Server data // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -44,7 +44,11 @@ int sqlsrv_stream_close( _Inout_ php_stream* stream, int /*close_handle*/ TSRMLS // read from a sqlsrv stream into the buffer provided by Zend. The parameters for binary vs. char are // set when sqlsrv_get_field is called by the user specifying which field type they want. -size_t sqlsrv_stream_read( _Inout_ php_stream* stream, _Out_writes_bytes_(count) char* buf, _Inout_ size_t count TSRMLS_DC ) +#if PHP_VERSION_ID >= 70400 +ssize_t sqlsrv_stream_read(_Inout_ php_stream* stream, _Out_writes_bytes_(count) char* buf, _Inout_ size_t count TSRMLS_DC) +#else +size_t sqlsrv_stream_read(_Inout_ php_stream* stream, _Out_writes_bytes_(count) char* buf, _Inout_ size_t count TSRMLS_DC) +#endif { SQLLEN read = 0; SQLSMALLINT c_type = SQL_C_CHAR; @@ -89,7 +93,8 @@ size_t sqlsrv_stream_read( _Inout_ php_stream* stream, _Out_writes_bytes_(count) break; } - SQLRETURN r = SQLGetData( ss->stmt->handle(), ss->field_index + 1, c_type, get_data_buffer, count /*BufferLength*/, &read ); + // Warnings will be handled below + SQLRETURN r = ss->stmt->current_results->get_data(ss->field_index + 1, c_type, get_data_buffer, count /*BufferLength*/, &read, false /*handle_warning*/ TSRMLS_CC); CHECK_SQL_ERROR( r, ss->stmt ) { stream->eof = 1; @@ -183,15 +188,20 @@ size_t sqlsrv_stream_read( _Inout_ php_stream* stream, _Out_writes_bytes_(count) return static_cast( read ); } - - catch( core::CoreException& ) { - + catch (core::CoreException&) { +#if PHP_VERSION_ID >= 70400 + return -1; +#else return 0; +#endif } - catch( ... ) { - - LOG( SEV_ERROR, "sqlsrv_stream_read: Unknown exception caught." ); + catch (...) { + LOG(SEV_ERROR, "sqlsrv_stream_read: Unknown exception caught."); +#if PHP_VERSION_ID >= 70400 + return -1; +#else return 0; +#endif } } diff --git a/source/shared/core_util.cpp b/source/shared/core_util.cpp index 515eb38b..9f2e0eea 100644 --- a/source/shared/core_util.cpp +++ b/source/shared/core_util.cpp @@ -5,7 +5,7 @@ // // Comments: Mostly error handling and some type handling // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -34,7 +34,7 @@ char last_err_msg[2048] = {'\0'}; // 2k to hold the error messages unsigned int convert_string_from_default_encoding( _In_ unsigned int php_encoding, _In_reads_bytes_(mbcs_len) char const* mbcs_in_string, _In_ unsigned int mbcs_len, _Out_writes_(utf16_len) __transfer( mbcs_in_string ) SQLWCHAR* utf16_out_string, - _In_ unsigned int utf16_len ); + _In_ unsigned int utf16_len, bool use_strict_conversion = false ); } // SQLSTATE for all internal errors @@ -172,11 +172,11 @@ bool convert_string_from_utf16( _In_ SQLSRV_ENCODING encoding, _In_reads_bytes_( // allocation of the destination string. An empty string passed in returns // failure since it's a failure case for convert_string_from_default_encoding. SQLWCHAR* utf16_string_from_mbcs_string( _In_ SQLSRV_ENCODING php_encoding, _In_reads_bytes_(mbcs_len) const char* mbcs_string, _In_ unsigned int mbcs_len, - _Out_ unsigned int* utf16_len ) + _Out_ unsigned int* utf16_len, bool use_strict_conversion ) { *utf16_len = (mbcs_len + 1); SQLWCHAR* utf16_string = reinterpret_cast( sqlsrv_malloc( *utf16_len * sizeof( SQLWCHAR ))); - *utf16_len = convert_string_from_default_encoding( php_encoding, mbcs_string, mbcs_len, utf16_string, *utf16_len ); + *utf16_len = convert_string_from_default_encoding( php_encoding, mbcs_string, mbcs_len, utf16_string, *utf16_len, use_strict_conversion ); if( *utf16_len == 0 ) { // we preserve the error and reset it because sqlsrv_free resets the last error @@ -189,6 +189,40 @@ SQLWCHAR* utf16_string_from_mbcs_string( _In_ SQLSRV_ENCODING php_encoding, _In_ return utf16_string; } +// Converts an input (assuming a datetime string) to a zval containing a PHP DateTime object. +// If the input is null, this simply returns a NULL zval. If anything wrong occurs during conversion, +// an exception will be thrown. +void convert_datetime_string_to_zval(_Inout_ sqlsrv_stmt* stmt, _In_opt_ char* input, _In_ SQLLEN length, _Inout_ zval& out_zval) +{ + if (input == NULL) { + ZVAL_NULL(&out_zval); + return; + } + + zval params[1]; + zval value_temp_z; + zval function_z; + + // Initialize all zval variables + ZVAL_UNDEF(&out_zval); + ZVAL_UNDEF(&value_temp_z); + ZVAL_UNDEF(&function_z); + ZVAL_UNDEF(params); + + // Convert the datetime string to a PHP DateTime object + core::sqlsrv_zval_stringl(&value_temp_z, input, length); + core::sqlsrv_zval_stringl(&function_z, "date_create", sizeof("date_create") - 1); + params[0] = value_temp_z; + + if (call_user_function(EG(function_table), NULL, &function_z, &out_zval, 1, + params TSRMLS_CC) == FAILURE) { + THROW_CORE_ERROR(stmt, SQLSRV_ERROR_DATETIME_CONVERSION_FAILED); + } + + zend_string_free(Z_STR(value_temp_z)); + zend_string_free(Z_STR(function_z)); +} + // call to retrieve an error from ODBC. This uses SQLGetDiagRec, so the // errno is 1 based. It returns it as an array with 3 members: // 1/SQLSTATE) sqlstate @@ -384,7 +418,7 @@ namespace { // to convert. unsigned int convert_string_from_default_encoding( _In_ unsigned int php_encoding, _In_reads_bytes_(mbcs_len) char const* mbcs_in_string, _In_ unsigned int mbcs_len, _Out_writes_(utf16_len) __transfer( mbcs_in_string ) SQLWCHAR* utf16_out_string, - _In_ unsigned int utf16_len ) + _In_ unsigned int utf16_len, bool use_strict_conversion ) { unsigned int win_encoding = CP_ACP; switch( php_encoding ) { @@ -399,8 +433,14 @@ unsigned int convert_string_from_default_encoding( _In_ unsigned int php_encodin win_encoding = php_encoding; break; } -#ifndef _WIN32 - unsigned int required_len = SystemLocale::ToUtf16( win_encoding, mbcs_in_string, mbcs_len, utf16_out_string, utf16_len ); +#ifndef _WIN32 + unsigned int required_len; + if (use_strict_conversion) { + required_len = SystemLocale::ToUtf16Strict( win_encoding, mbcs_in_string, mbcs_len, utf16_out_string, utf16_len ); + } + else { + required_len = SystemLocale::ToUtf16( win_encoding, mbcs_in_string, mbcs_len, utf16_out_string, utf16_len ); + } #else unsigned int required_len = MultiByteToWideChar( win_encoding, MB_ERR_INVALID_CHARS, mbcs_in_string, mbcs_len, utf16_out_string, utf16_len ); #endif // !_Win32 @@ -414,3 +454,200 @@ unsigned int convert_string_from_default_encoding( _In_ unsigned int php_encodin } } + + +namespace data_classification { + const char* DATA_CLASS = "Data Classification"; + const char* LABEL = "Label"; + const char* INFOTYPE = "Information Type"; + const char* NAME = "name"; + const char* ID = "id"; + + void convert_sensivity_field(_Inout_ sqlsrv_stmt* stmt, _In_ SQLSRV_ENCODING encoding, _In_ unsigned char *ptr, _In_ int len, _Inout_updates_bytes_(cchOutLen) char** field_name) + { + sqlsrv_malloc_auto_ptr temp_field_name; + int temp_field_len = len * sizeof(SQLWCHAR); + SQLLEN field_name_len = 0; + + if (len == 0) { + *field_name = reinterpret_cast(sqlsrv_malloc(1)); + *field_name[0] = '\0'; + return; + } + + temp_field_name = static_cast(sqlsrv_malloc((len + 1) * sizeof(SQLWCHAR))); + memset(temp_field_name, L'\0', len + 1); + memcpy_s(temp_field_name, temp_field_len, ptr, temp_field_len); + + bool converted = convert_string_from_utf16(encoding, temp_field_name, len, field_name, field_name_len); + + CHECK_CUSTOM_ERROR(!converted, stmt, SQLSRV_ERROR_FIELD_ENCODING_TRANSLATE, get_last_error_message()) { + throw core::CoreException(); + } + } + + void name_id_pair_free(_Inout_ name_id_pair* pair) + { + if (pair->name) { + pair->name.reset(); + } + if (pair->id) { + pair->id.reset(); + } + sqlsrv_free(pair); + } + + void parse_sensitivity_name_id_pairs(_Inout_ sqlsrv_stmt* stmt, _Inout_ USHORT& numpairs, _Inout_ std::vector>* pairs, _Inout_ unsigned char **pptr) + { + unsigned char *ptr = *pptr; + unsigned short npairs; + numpairs = npairs = *(reinterpret_cast(ptr)); + SQLSRV_ENCODING encoding = ((stmt->encoding() == SQLSRV_ENCODING_DEFAULT ) ? stmt->conn->encoding() : stmt->encoding()); + + pairs->reserve(numpairs); + + ptr += sizeof(unsigned short); + while (npairs--) { + int namelen, idlen; + unsigned char *nameptr, *idptr; + + sqlsrv_malloc_auto_ptr pair; + pair = new(sqlsrv_malloc(sizeof(name_id_pair))) name_id_pair(); + + sqlsrv_malloc_auto_ptr name; + sqlsrv_malloc_auto_ptr id; + + namelen = *ptr++; + nameptr = ptr; + + pair->name_len = namelen; + convert_sensivity_field(stmt, encoding, nameptr, namelen, (char**)&name); + pair->name = name; + + ptr += namelen * 2; + idlen = *ptr++; + idptr = ptr; + ptr += idlen * 2; + + pair->id_len = idlen; + convert_sensivity_field(stmt, encoding, idptr, idlen, (char**)&id); + pair->id = id; + + pairs->push_back(pair.get()); + pair.transferred(); + } + *pptr = ptr; + } + + void parse_column_sensitivity_props(_Inout_ sensitivity_metadata* meta, _Inout_ unsigned char **pptr) + { + unsigned char *ptr = *pptr; + unsigned short ncols; + + // Get number of columns + meta->num_columns = ncols = *(reinterpret_cast(ptr)); + + // Move forward + ptr += sizeof(unsigned short); + + while (ncols--) { + unsigned short npairs = *(reinterpret_cast(ptr)); + + ptr += sizeof(unsigned short); + + column_sensitivity column; + column.num_pairs = npairs; + + while (npairs--) { + label_infotype_pair pair; + + unsigned short labelidx, typeidx; + labelidx = *(reinterpret_cast(ptr)); + ptr += sizeof(unsigned short); + typeidx = *(reinterpret_cast(ptr)); + ptr += sizeof(unsigned short); + + pair.label_idx = labelidx; + pair.infotype_idx = typeidx; + + column.label_info_pairs.push_back(pair); + } + + meta->columns_sensitivity.push_back(column); + } + + *pptr = ptr; + } + + USHORT fill_column_sensitivity_array(_Inout_ sqlsrv_stmt* stmt, _In_ SQLSMALLINT colno, _Inout_ zval *return_array TSRMLS_CC) + { + sensitivity_metadata* meta = stmt->current_sensitivity_metadata; + if (meta == NULL) { + return 0; + } + + SQLSRV_ASSERT(colno >= 0 && colno < meta->num_columns, "fill_column_sensitivity_array: column number out of bounds"); + + zval data_classification; + ZVAL_UNDEF(&data_classification); + core::sqlsrv_array_init(*stmt, &data_classification TSRMLS_CC ); + + USHORT num_pairs = meta->columns_sensitivity[colno].num_pairs; + + if (num_pairs == 0) { + core::sqlsrv_add_assoc_zval(*stmt, return_array, DATA_CLASS, &data_classification TSRMLS_CC); + + return 0; + } + + zval sensitivity_properties; + ZVAL_UNDEF(&sensitivity_properties); + core::sqlsrv_array_init(*stmt, &sensitivity_properties TSRMLS_CC); + + for (USHORT j = 0; j < num_pairs; j++) { + zval label_array, infotype_array; + ZVAL_UNDEF(&label_array); + ZVAL_UNDEF(&infotype_array); + + core::sqlsrv_array_init(*stmt, &label_array TSRMLS_CC); + core::sqlsrv_array_init(*stmt, &infotype_array TSRMLS_CC); + + USHORT labelidx = meta->columns_sensitivity[colno].label_info_pairs[j].label_idx; + USHORT typeidx = meta->columns_sensitivity[colno].label_info_pairs[j].infotype_idx; + + char *label = meta->labels[labelidx]->name; + char *label_id = meta->labels[labelidx]->id; + char *infotype = meta->infotypes[typeidx]->name; + char *infotype_id = meta->infotypes[typeidx]->id; + + core::sqlsrv_add_assoc_string(*stmt, &label_array, NAME, label, 1 TSRMLS_CC); + core::sqlsrv_add_assoc_string(*stmt, &label_array, ID, label_id, 1 TSRMLS_CC); + + core::sqlsrv_add_assoc_zval(*stmt, &sensitivity_properties, LABEL, &label_array TSRMLS_CC); + + core::sqlsrv_add_assoc_string(*stmt, &infotype_array, NAME, infotype, 1 TSRMLS_CC); + core::sqlsrv_add_assoc_string(*stmt, &infotype_array, ID, infotype_id, 1 TSRMLS_CC); + + core::sqlsrv_add_assoc_zval(*stmt, &sensitivity_properties, INFOTYPE, &infotype_array TSRMLS_CC); + + // add the pair of sensitivity properties to data_classification + core::sqlsrv_add_next_index_zval(*stmt, &data_classification, &sensitivity_properties TSRMLS_CC ); + } + + // add data classfication as associative array + core::sqlsrv_add_assoc_zval(*stmt, return_array, DATA_CLASS, &data_classification TSRMLS_CC); + + return num_pairs; + } + + void sensitivity_metadata::reset() + { + std::for_each(labels.begin(), labels.end(), name_id_pair_free); + labels.clear(); + + std::for_each(infotypes.begin(), infotypes.end(), name_id_pair_free); + infotypes.clear(); + + columns_sensitivity.clear(); + } +} // namespace data_classification diff --git a/source/shared/globalization.h b/source/shared/globalization.h index 4ddccc52..f7e1afd3 100644 --- a/source/shared/globalization.h +++ b/source/shared/globalization.h @@ -4,7 +4,7 @@ // Contents: Contains functions for handling Windows format strings // and UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/interlockedatomic.h b/source/shared/interlockedatomic.h index f46e2b1d..12456143 100644 --- a/source/shared/interlockedatomic.h +++ b/source/shared/interlockedatomic.h @@ -4,7 +4,7 @@ // Contents: Contains a portable abstraction for interlocked, atomic // operations on int32_t and pointer types. // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/interlockedatomic_gcc.h b/source/shared/interlockedatomic_gcc.h index 8a1f3732..171c1ad2 100644 --- a/source/shared/interlockedatomic_gcc.h +++ b/source/shared/interlockedatomic_gcc.h @@ -4,7 +4,7 @@ // Contents: Contains a portable abstraction for interlocked, atomic // operations on int32_t and pointer types. // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/interlockedslist.h b/source/shared/interlockedslist.h index cd71452a..4bc04c1f 100644 --- a/source/shared/interlockedslist.h +++ b/source/shared/interlockedslist.h @@ -4,7 +4,7 @@ // Contents: Contains a portable abstraction for interlocked, singly // linked list. // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/localization.hpp b/source/shared/localization.hpp index a572ab02..cb624951 100644 --- a/source/shared/localization.hpp +++ b/source/shared/localization.hpp @@ -3,7 +3,7 @@ // // Contents: Contains portable classes for localization // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/localizationimpl.cpp b/source/shared/localizationimpl.cpp index e67bbf22..0e733826 100644 --- a/source/shared/localizationimpl.cpp +++ b/source/shared/localizationimpl.cpp @@ -5,7 +5,7 @@ // Must be included in one c/cpp file per binary // A build error will occur if this inclusion policy is not followed // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -280,9 +280,23 @@ bool EncodingConverter::Initialize() using namespace std; SystemLocale::SystemLocale( const char * localeName ) - : m_pLocale( new std::locale(localeName) ) - , m_uAnsiCP(CP_UTF8) + : m_uAnsiCP(CP_UTF8) + , m_pLocale(NULL) { + const char* DEFAULT_LOCALE = "en_US.UTF-8"; + + try { + m_pLocale = new std::locale(localeName); + } + catch(const std::exception& e) { + localeName = DEFAULT_LOCALE; + } + + if(!m_pLocale) { + m_pLocale = new std::locale(localeName); + } + + // Mapping from locale charset to codepage struct LocaleCP { const char* localeName; @@ -331,8 +345,7 @@ const SystemLocale & SystemLocale::Singleton() #if !defined(__GNUC__) || defined(NO_THREADSAFE_STATICS) #error "Relying on GCC's threadsafe initialization of local statics." #endif - // get locale from environment and set as default - static const SystemLocale s_Default(setlocale(LC_ALL, NULL)); + static const SystemLocale s_Default(setlocale(LC_CTYPE, NULL)); return s_Default; } diff --git a/source/shared/msodbcsql.h b/source/shared/msodbcsql.h index 2392c5e3..b0496f5d 100644 --- a/source/shared/msodbcsql.h +++ b/source/shared/msodbcsql.h @@ -20,7 +20,7 @@ // pecuniary loss) arising out of the use of or inability to use // this SDK, even if Microsoft has been advised of the possibility // of such damages. -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -144,7 +144,11 @@ // force column encryption #define SQL_CA_SS_FORCE_ENCRYPT (SQL_CA_SS_BASE+36) // indicate mandatory encryption for this parameter -#define SQL_CA_SS_MAX_USED (SQL_CA_SS_BASE+37) +// Data Classification +#define SQL_CA_SS_DATA_CLASSIFICATION (SQL_CA_SS_BASE+37) // retrieve data classification information + +#define SQL_CA_SS_MAX_USED (SQL_CA_SS_BASE+38) + // Defines for use with SQL_COPT_SS_INTEGRATED_SECURITY - Pre-Connect Option only #define SQL_IS_OFF 0L // Integrated security isn't used #define SQL_IS_ON 1L // Integrated security is used diff --git a/source/shared/sal_def.h b/source/shared/sal_def.h index 053fb199..7d4efb17 100644 --- a/source/shared/sal_def.h +++ b/source/shared/sal_def.h @@ -3,7 +3,7 @@ // // Contents: Contains the minimal definitions to build on non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/typedefs_for_linux.h b/source/shared/typedefs_for_linux.h index 390d03cc..82e33eba 100644 --- a/source/shared/typedefs_for_linux.h +++ b/source/shared/typedefs_for_linux.h @@ -1,7 +1,7 @@ //--------------------------------------------------------------------------------------------------------------------------------- // File: typedefs_for_linux.h // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/version.h b/source/shared/version.h index 7424d900..6bc7e796 100644 --- a/source/shared/version.h +++ b/source/shared/version.h @@ -4,7 +4,7 @@ // File: version.h // Contents: Version number constants // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -26,12 +26,12 @@ // Increase Minor with backward compatible new functionalities and API changes. // Increase Patch for backward compatible fixes. #define SQLVERSION_MAJOR 5 -#define SQLVERSION_MINOR 6 -#define SQLVERSION_PATCH 1 +#define SQLVERSION_MINOR 7 +#define SQLVERSION_PATCH 0 #define SQLVERSION_BUILD 0 // For previews, set this constant to 1. Otherwise, set it to 0 -#define PREVIEW 0 +#define PREVIEW 1 #define SEMVER_PRERELEASE // Semantic versioning build metadata, build meta data is not counted in precedence order. diff --git a/source/shared/xplat.h b/source/shared/xplat.h index 1b195639..a32cdda0 100644 --- a/source/shared/xplat.h +++ b/source/shared/xplat.h @@ -3,7 +3,7 @@ // // Contents: include for definition of Windows types for non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/xplat_intsafe.h b/source/shared/xplat_intsafe.h index 91db2652..90d2f150 100644 --- a/source/shared/xplat_intsafe.h +++ b/source/shared/xplat_intsafe.h @@ -4,7 +4,7 @@ // Contents: This module defines helper functions to prevent // integer overflow bugs. // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/xplat_winerror.h b/source/shared/xplat_winerror.h index 34e7ffb5..0ebd1f2a 100644 --- a/source/shared/xplat_winerror.h +++ b/source/shared/xplat_winerror.h @@ -3,7 +3,7 @@ // // Contents: Contains the minimal definitions to build on non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/xplat_winnls.h b/source/shared/xplat_winnls.h index bc81cf58..22bfedc9 100644 --- a/source/shared/xplat_winnls.h +++ b/source/shared/xplat_winnls.h @@ -3,7 +3,7 @@ // // Contents: Contains the minimal definitions to build on non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/config.m4 b/source/sqlsrv/config.m4 index 20d4d858..a8a4e89d 100644 --- a/source/sqlsrv/config.m4 +++ b/source/sqlsrv/config.m4 @@ -4,7 +4,7 @@ dnl dnl Contents: the code that will go into the configure script, indicating options, dnl external libraries and includes, and what source files are to be compiled. dnl -dnl Microsoft Drivers 5.6 for PHP for SQL Server +dnl Microsoft Drivers 5.7 for PHP for SQL Server dnl Copyright(c) Microsoft Corporation dnl All rights reserved. dnl MIT License diff --git a/source/sqlsrv/config.w32 b/source/sqlsrv/config.w32 index 887d1e77..33cac9bd 100644 --- a/source/sqlsrv/config.w32 +++ b/source/sqlsrv/config.w32 @@ -3,7 +3,7 @@ // // Contents: JScript build configuration used by buildconf.bat // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/conn.cpp b/source/sqlsrv/conn.cpp index 532934f7..5500da23 100644 --- a/source/sqlsrv/conn.cpp +++ b/source/sqlsrv/conn.cpp @@ -3,7 +3,7 @@ // // Contents: Routines that use connection handles // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -219,6 +219,7 @@ namespace SSStmtOptionNames { const char DATE_AS_STRING[] = "ReturnDatesAsStrings"; const char FORMAT_DECIMALS[] = "FormatDecimals"; const char DECIMAL_PLACES[] = "DecimalPlaces"; + const char DATA_CLASSIFICATION[] = "DataClassification"; } namespace SSConnOptionNames { @@ -233,6 +234,7 @@ const char Authentication[] = "Authentication"; const char CharacterSet[] = "CharacterSet"; const char ColumnEncryption[] = "ColumnEncryption"; const char ConnectionPooling[] = "ConnectionPooling"; +const char Language[] = "Language"; const char ConnectRetryCount[] = "ConnectRetryCount"; const char ConnectRetryInterval[] = "ConnectRetryInterval"; const char Database[] = "Database"; @@ -311,6 +313,12 @@ const stmt_option SS_STMT_OPTS[] = { SQLSRV_STMT_OPTION_DECIMAL_PLACES, std::unique_ptr( new stmt_option_decimal_places ) }, + { + SSStmtOptionNames::DATA_CLASSIFICATION, + sizeof( SSStmtOptionNames::DATA_CLASSIFICATION ), + SQLSRV_STMT_OPTION_DATA_CLASSIFICATION, + std::unique_ptr( new stmt_option_data_classification ) + }, { NULL, 0, SQLSRV_STMT_OPTION_INVALID, std::unique_ptr{} }, }; @@ -380,6 +388,15 @@ const connection_option SS_CONN_OPTS[] = { CONN_ATTR_BOOL, conn_null_func::func }, + { + SSConnOptionNames::Language, + sizeof(SSConnOptionNames::Language), + SQLSRV_CONN_OPTION_LANGUAGE, + ODBCConnOptions::Language, + sizeof(ODBCConnOptions::Language), + CONN_ATTR_STRING, + conn_str_append_func::func + }, { SSConnOptionNames::Driver, sizeof(SSConnOptionNames::Driver), diff --git a/source/sqlsrv/init.cpp b/source/sqlsrv/init.cpp index 80014524..2b394862 100644 --- a/source/sqlsrv/init.cpp +++ b/source/sqlsrv/init.cpp @@ -2,7 +2,7 @@ // File: init.cpp // Contents: initialization routines for the extension // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -219,7 +219,7 @@ zend_function_entry sqlsrv_functions[] = { PHP_FE( sqlsrv_client_info, sqlsrv_client_info_arginfo ) PHP_FE( sqlsrv_server_info, sqlsrv_server_info_arginfo ) PHP_FE( sqlsrv_cancel, sqlsrv_cancel_arginfo ) - PHP_FE( sqlsrv_free_stmt, sqlsrv_close_arginfo ) + PHP_FE( sqlsrv_free_stmt, sqlsrv_free_stmt_arginfo ) PHP_FE( sqlsrv_field_metadata, sqlsrv_field_metadata_arginfo ) PHP_FE( sqlsrv_send_stream_data, sqlsrv_send_stream_data_arginfo ) PHP_FE( SQLSRV_SQLTYPE_BINARY, sqlsrv_sqltype_size_arginfo ) diff --git a/source/sqlsrv/php_sqlsrv.h b/source/sqlsrv/php_sqlsrv.h index 4de7f5c9..36bb9a95 100644 --- a/source/sqlsrv/php_sqlsrv.h +++ b/source/sqlsrv/php_sqlsrv.h @@ -8,7 +8,7 @@ // // Comments: Also contains "internal" declarations shared across source files. // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/php_sqlsrv_int.h b/source/sqlsrv/php_sqlsrv_int.h index c294f465..3ebb179f 100644 --- a/source/sqlsrv/php_sqlsrv_int.h +++ b/source/sqlsrv/php_sqlsrv_int.h @@ -8,7 +8,7 @@ // // Comments: Also contains "internal" declarations shared across source files. // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -214,12 +214,12 @@ bool ss_error_handler( _Inout_ sqlsrv_context& ctx, _In_ unsigned int sqlsrv_err // returned in utf16_out_string. unsigned int convert_string_from_default_encoding( _In_ unsigned int php_encoding, _In_reads_bytes_(mbcs_len) char const* mbcs_in_string, _In_ unsigned int mbcs_len, _Out_writes_(utf16_len) __transfer(mbcs_in_string) wchar_t* utf16_out_string, - _In_ unsigned int utf16_len ); + _In_ unsigned int utf16_len, bool use_strict_conversion = false ); // create a wide char string from the passed in mbcs string. NULL is returned if the string // could not be created. No error is posted by this function. utf16_len is the number of // wchar_t characters, not the number of bytes. SQLWCHAR* utf16_string_from_mbcs_string( _In_ unsigned int php_encoding, _In_reads_bytes_(mbcs_len) const char* mbcs_string, - _In_ unsigned int mbcs_len, _Out_ unsigned int* utf16_len ); + _In_ unsigned int mbcs_len, _Out_ unsigned int* utf16_len, bool use_strict_conversion = false ); // *** internal error macros and functions *** bool handle_error( sqlsrv_context const* ctx, int log_subsystem, const char* function, diff --git a/source/sqlsrv/stmt.cpp b/source/sqlsrv/stmt.cpp index 9d60de25..bbb01190 100644 --- a/source/sqlsrv/stmt.cpp +++ b/source/sqlsrv/stmt.cpp @@ -3,7 +3,7 @@ // // Contents: Routines that use statement handles // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -42,7 +42,6 @@ unsigned int current_log_subsystem = LOG_STMT; // constants used as invalid types for type errors const zend_uchar PHPTYPE_INVALID = SQLSRV_PHPTYPE_INVALID; -const int SQLTYPE_INVALID = 0; const int SQLSRV_INVALID_PRECISION = -1; const SQLUINTEGER SQLSRV_INVALID_SIZE = (~1U); const int SQLSRV_INVALID_SCALE = -1; @@ -51,8 +50,6 @@ const int SQLSRV_SIZE_MAX_TYPE = -1; // constants for maximums in SQL Server const int SQL_SERVER_MAX_FIELD_SIZE = 8000; const int SQL_SERVER_MAX_PRECISION = 38; -const int SQL_SERVER_DEFAULT_PRECISION = 18; -const int SQL_SERVER_DEFAULT_SCALE = 0; // default class used when no class is specified by sqlsrv_fetch_object const char STDCLASS_NAME[] = "stdclass"; @@ -470,7 +467,6 @@ PHP_FUNCTION( sqlsrv_fetch_array ) PHP_FUNCTION( sqlsrv_field_metadata ) { sqlsrv_stmt* stmt = NULL; - SQLSMALLINT num_cols = -1; LOG_FUNCTION( "sqlsrv_field_metadata" ); @@ -481,6 +477,10 @@ PHP_FUNCTION( sqlsrv_field_metadata ) // get the number of fields in the resultset and its metadata if not exists SQLSMALLINT num_cols = get_resultset_meta_data(stmt); + if (stmt->data_classification) { + core_sqlsrv_sensitivity_metadata(stmt); + } + zval result_meta_data; ZVAL_UNDEF( &result_meta_data ); core::sqlsrv_array_init( *stmt, &result_meta_data TSRMLS_CC ); @@ -533,6 +533,10 @@ PHP_FUNCTION( sqlsrv_field_metadata ) core::sqlsrv_add_assoc_long( *stmt, &field_array, FieldMetaData::NULLABLE, core_meta_data->field_is_nullable TSRMLS_CC ); + if (stmt->data_classification) { + data_classification::fill_column_sensitivity_array(stmt, f, &field_array TSRMLS_CC); + } + // add this field's meta data to the result set meta data core::sqlsrv_add_next_index_zval( *stmt, &result_meta_data, &field_array TSRMLS_CC ); } @@ -1505,11 +1509,11 @@ void convert_to_zval( _Inout_ sqlsrv_stmt* stmt, _In_ SQLSRV_PHPTYPE sqlsrv_php_ Z_TRY_ADDREF( out_zval ); break; } - case SQLSRV_PHPTYPE_DATETIME: - { - out_zval = *( static_cast( in_val )); - break; - } + case SQLSRV_PHPTYPE_DATETIME: + { + convert_datetime_string_to_zval(stmt, static_cast(in_val), field_len, out_zval); + break; + } case SQLSRV_PHPTYPE_NULL: ZVAL_NULL(&out_zval); @@ -1791,7 +1795,12 @@ SQLSMALLINT get_resultset_meta_data(_Inout_ sqlsrv_stmt * stmt) if (num_cols == 0) { getMetaData = true; - num_cols = core::SQLNumResultCols(stmt TSRMLS_CC); + if (stmt->column_count == ACTIVE_NUM_COLS_INVALID) { + num_cols = core::SQLNumResultCols(stmt TSRMLS_CC); + stmt->column_count = num_cols; + } else { + num_cols = stmt->column_count; + } } try { diff --git a/source/sqlsrv/template.rc b/source/sqlsrv/template.rc index bbe47132..e95041ae 100644 --- a/source/sqlsrv/template.rc +++ b/source/sqlsrv/template.rc @@ -3,7 +3,7 @@ // // Contents: Version resource // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/util.cpp b/source/sqlsrv/util.cpp index cec66c3f..9e545b6a 100644 --- a/source/sqlsrv/util.cpp +++ b/source/sqlsrv/util.cpp @@ -5,7 +5,7 @@ // // Comments: Mostly error handling and some type handling // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -205,8 +205,8 @@ ss_error SS_ERRORS[] = { }, { - SS_SQLSRV_ERROR_ZEND_OBJECT_FAILED, - { IMSSP, (SQLCHAR*)"Failed to create an instance of class %1!s!.", -30, true } + SS_SQLSRV_ERROR_ZEND_OBJECT_FAILED, + { IMSSP, (SQLCHAR*)"Failed to create an instance of class %1!s!.", -30, true } }, { @@ -440,6 +440,18 @@ ss_error SS_ERRORS[] = { SQLSRV_ERROR_AAD_MSI_UID_PWD_NOT_NULL, { IMSSP, (SQLCHAR*) "When using ActiveDirectoryMsi Authentication, PWD must be NULL. UID can be NULL, but if not, an empty string is not accepted.", -118, false} }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_PRE_EXECUTION, + { IMSSP, (SQLCHAR*) "The statement must be executed to retrieve Data Classification Sensitivity Metadata.", -119, false} + }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_NOT_AVAILABLE, + { IMSSP, (SQLCHAR*) "Failed to retrieve Data Classification Sensitivity Metadata. If the driver and the server both support the Data Classification feature, check whether the query returns columns with classification information.", -120, false} + }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, + { IMSSP, (SQLCHAR*) "Failed to retrieve Data Classification Sensitivity Metadata: %1!s!", -121, true} + }, // terminate the list of errors/warnings { UINT_MAX, {} } diff --git a/test/functional/output.py b/test/functional/output.py index 94c3e5a8..99543bc3 100644 --- a/test/functional/output.py +++ b/test/functional/output.py @@ -5,13 +5,14 @@ # Requirement of python 3.4 to execute this script and required result log file(s) # are in the same location # Run with command line without options required. Example: py output.py -# This script parse output of PHP Native Test +# This script parse output of PHP Test logs # ############################################################################################# import os import stat import re +import argparse # This module appends an entry to the tests list, may include the test title. # Input: search_pattern - pattern to look for in the line of the log file @@ -46,12 +47,14 @@ def get_test_entry(search_pattern, line, index, tests_list, get_title = False): tests_list.append(entry) # Extract individual test results from the log file and -# enter it in the nativeresult.xml file. -# Input: logfile - the log file -# number - the number for this xml file -def gen_XML(logfile, number): +# enter it in the xml report file. +# Input: logfile - the test log file +# number - the number for this xml file (applicable if using the default report name) +# logfilename - use the log file name for the xml output file Instead +def gen_XML(logfile, number, logfilename): print('================================================') - print("\n" + os.path.splitext(logfile)[0] + "\n" ) + filename = os.path.splitext(logfile)[0] + print("\n" + filename + "\n" ) tests_list = [] with open(os.path.dirname(os.path.realpath(__file__)) + os.sep + logfile) as f: @@ -70,10 +73,16 @@ def gen_XML(logfile, number): print(line) print('================================================') - # Generating the nativeresult.xml file. - file = open('nativeresult' + str(number) + '.xml', 'w') + # Generating the xml report. + if logfilename is True: + file = open(filename + '.xml', 'w') + report = filename + else: + file = open('nativeresult' + str(number) + '.xml', 'w') + report = 'Native Tests' + file.write('' + os.linesep) - file.write('' + os.linesep) + file.write('' + os.linesep) index = 1 for test in tests_list: @@ -83,12 +92,18 @@ def gen_XML(logfile, number): # ----------------------- Main Function ----------------------- -# Display results on screen from result log file. +# Generate XML reports from test result log files. if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--LOGFILENAME', action='store_true', help="Generate XML files using log file names (default: False)") + + args = parser.parse_args() + logfilename = args.LOGFILENAME + num = 1 for f in os.listdir(os.path.dirname(os.path.realpath(__file__))): if f.endswith("log"): logfile = f - gen_XML(logfile, num) + gen_XML(logfile, num, logfilename) num = num + 1 diff --git a/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc b/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc index d327d859..969d1446 100644 --- a/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc +++ b/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc @@ -624,6 +624,29 @@ function IsDaasMode() return ($daasMode ? true : false); } +function isSQLAzure() +{ + // 'SQL Azure' indicates SQL Database or SQL Data Warehouse + // For details, https://docs.microsoft.com/sql/t-sql/functions/serverproperty-transact-sql + try { + $conn = connect(); + $tsql = "SELECT SERVERPROPERTY ('edition')"; + $stmt = $conn->query($tsql); + + $result = $stmt->fetch(PDO::FETCH_NUM); + $edition = $result[0]; + + if ($edition === "SQL Azure") { + return true; + } else { + return false; + } + } catch (Exception $e) { + echo $e->getMessage(); + die("Could not fetch server property."); + } +} + function isAzureDW() { // Check if running Azure Data Warehouse diff --git a/test/functional/pdo_sqlsrv/PDO29_ConnInterface.phpt b/test/functional/pdo_sqlsrv/PDO29_ConnInterface.phpt index 237a5e5b..49649bdc 100644 --- a/test/functional/pdo_sqlsrv/PDO29_ConnInterface.phpt +++ b/test/functional/pdo_sqlsrv/PDO29_ConnInterface.phpt @@ -46,6 +46,14 @@ function CheckInterface($conn) '__sleep' => true, 'inTransaction' => true, ); + + $phpver = substr(phpversion(), 0, 3); + if ($phpver >= '7.4') { + // Reference: https://wiki.php.net/rfc/custom_object_serialization + unset($expected['__wakeup']); + unset($expected['__sleep']); + } + $classname = get_class($conn); $methods = get_class_methods($classname); foreach ($methods as $k => $method) diff --git a/test/functional/pdo_sqlsrv/PDO32_StmtInterface.phpt b/test/functional/pdo_sqlsrv/PDO32_StmtInterface.phpt index 06ca54d1..1bc73c2f 100644 --- a/test/functional/pdo_sqlsrv/PDO32_StmtInterface.phpt +++ b/test/functional/pdo_sqlsrv/PDO32_StmtInterface.phpt @@ -49,6 +49,14 @@ function checkInterface($stmt) '__wakeup' => true, '__sleep' => true, ); + + $phpver = substr(phpversion(), 0, 3); + if ($phpver >= '7.4') { + // Reference: https://wiki.php.net/rfc/custom_object_serialization + unset($expected['__wakeup']); + unset($expected['__sleep']); + } + $classname = get_class($stmt); $methods = get_class_methods($classname); foreach ($methods as $k => $method) { diff --git a/test/functional/pdo_sqlsrv/PDO81_MemoryCheck.phpt b/test/functional/pdo_sqlsrv/PDO81_MemoryCheck.phpt index 5513290b..1153d6ac 100644 --- a/test/functional/pdo_sqlsrv/PDO81_MemoryCheck.phpt +++ b/test/functional/pdo_sqlsrv/PDO81_MemoryCheck.phpt @@ -11,6 +11,10 @@ PHPT_EXEC=true columnCount(); + $result = $stmt->fetchAll(); + $rowCount = count($result); + unset($result); + $stmt->closeCursor(); + unset($stmt); + if ($rowCount != $noRows) { + die("$rowCount rows retrieved instead of $noRows\n"); + } + break; + + case 5: // fetchObject + $stmt = ExecuteQueryEx($conn, $tsql, ($prepared ? false : true)); + $fldCount = $stmt->columnCount(); + while ($obj = $stmt->fetchObject()) { + unset($obj); + $rowCount++; + } + $stmt->closeCursor(); + unset($stmt); + if ($rowCount != $noRows) { + die("$rowCount rows retrieved instead of $noRows\n"); + } + break; + + case 6: // fetchColumn + $stmt = ExecuteQueryEx($conn, $tsql, ($prepared ? false : true)); + $fldCount = $stmt->columnCount(); + // Check for "false" to terminate because fetchColumn may return NULL + while (($result = $stmt->fetchColumn()) !== false) { + unset($result); + $rowCount++; + } + $stmt->closeCursor(); + unset($stmt); + if ($rowCount != $noRows) { + die("$rowCount rows retrieved instead of $noRows\n"); + } + break; + default: break; @@ -228,7 +286,7 @@ function Repro() { try { - MemCheck(20, 10, 15, 1, 3, 0); + MemCheck(_NUM_PASSES, _NUM_ROWS1, _NUM_ROWS2, 1, 6, 0); } catch (Exception $e) { diff --git a/test/functional/pdo_sqlsrv/pdo_035_binary_encoding_error_bound_by_name_errors.phpt b/test/functional/pdo_sqlsrv/pdo_035_binary_encoding_error_bound_by_name_errors.phpt new file mode 100644 index 00000000..39d30729 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_035_binary_encoding_error_bound_by_name_errors.phpt @@ -0,0 +1,164 @@ +--TEST-- +GitHub Issue #35 binary encoding error when binding by name +--DESCRIPTION-- +Based on pdo_035_binary_encoding_error_bound_by_name.phpt but this includes error checking for various encoding errors +--SKIPIF-- + +--FILE-- +prepare($sql); + $stmt->bindParam(1, $value, PDO::PARAM_INT, 0, PDO::SQLSRV_ENCODING_DEFAULT); + $stmt->setAttribute(constant('PDO::SQLSRV_ATTR_ENCODING'), PDO::SQLSRV_ENCODING_BINARY); + $stmt->bindParam(2, $input, PDO::PARAM_LOB); + $stmt->execute(); + echo "bindTypeNoEncoding: expected to fail!\n"; + } catch (PDOException $e) { + $error = '*An encoding was specified for parameter 1. Only PDO::PARAM_LOB and PDO::PARAM_STR can take an encoding option.'; + if (!fnmatch($error, $e->getMessage())) { + echo "Error message unexpected in bindTypeNoEncoding\n"; + var_dump($e->getMessage()); + } + } +} + +function bindDefaultEncoding($conn, $sql, $input) +{ + try { + $value = 1; + + $stmt = $conn->prepare($sql); + $stmt->bindParam(1, $value, PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_DEFAULT); + $stmt->setAttribute(constant('PDO::SQLSRV_ATTR_ENCODING'), PDO::SQLSRV_ENCODING_BINARY); + $stmt->bindParam(2, $input, PDO::PARAM_LOB); + $stmt->execute(); + echo "bindDefaultEncoding: expected to fail!\n"; + } catch (PDOException $e) { + $error = '*Invalid encoding specified for parameter 1.'; + if (!fnmatch($error, $e->getMessage())) { + echo "Error message unexpected in bindDefaultEncoding\n"; + var_dump($e->getMessage()); + } + } +} + +function insertData($conn, $sql, $input) +{ + try { + $value = 1; + + $stmt = $conn->prepare($sql); + $stmt->bindParam(1, $value); + // Specify binary encoding for the second parameter only such that the first + // parameter is unaffected + $stmt->bindParam(2, $input, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + $stmt->execute(); + } catch (PDOException $e) { + echo "Error unexpected in insertData\n"; + var_dump($e->getMessage()); + } +} + +function invalidEncoding1($conn, $sql) +{ + try { + $stmt = $conn->prepare($sql); + $stmt->bindColumn(1, $id, PDO::PARAM_INT, 0, PDO::SQLSRV_ENCODING_UTF8); + $stmt->execute(); + $stmt->fetch(PDO::FETCH_BOUND); + echo "invalidEncoding1: expected to fail!\n"; + } catch (PDOException $e) { + $error = '*An encoding was specified for column 1. Only PDO::PARAM_LOB and PDO::PARAM_STR column types can take an encoding option.'; + if (!fnmatch($error, $e->getMessage())) { + echo "Error message unexpected in invalidEncoding1\n"; + var_dump($e->getMessage()); + } + } +} + +function invalidEncoding2($conn, $sql) +{ + try { + $stmt = $conn->prepare($sql); + $stmt->bindColumn('Value', $val1, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_DEFAULT); + $stmt->execute(); + $stmt->fetch(PDO::FETCH_BOUND); + echo "invalidEncoding2: expected to fail!\n"; + } catch (PDOException $e) { + $error = '*Invalid encoding specified for column 1.'; + if (!fnmatch($error, $e->getMessage())) { + echo "Error message unexpected in invalidEncoding2\n"; + var_dump($e->getMessage()); + } + } +} + +function invalidEncoding3($conn, $sql) +{ + try { + $stmt = $conn->prepare($sql); + $stmt->bindColumn(1, $id, PDO::PARAM_STR, 0, "dummy"); + $stmt->execute(); + $stmt->fetch(PDO::FETCH_BOUND); + echo "invalidEncoding3: expected to fail!\n"; + } catch (PDOException $e) { + $error = '*An invalid type or value was given as bound column driver data for column 1. Only encoding constants such as PDO::SQLSRV_ENCODING_UTF8 may be used as bound column driver data.'; + if (!fnmatch($error, $e->getMessage())) { + echo "Error message unexpected in invalidEncoding3\n"; + var_dump($e->getMessage()); + } + } +} + +try { + require_once( "MsCommon_mid-refactor.inc" ); + + // Connect + $conn = connect(); + + // Create a table + $tableName = "testTableIssue35"; + createTable($conn, $tableName, array("ID" => "int", "Value" => "varbinary(max)")); + + // Insert data using bind parameters + $sql = "INSERT INTO $tableName VALUES (?, ?)"; + $message = "This is to test github issue 35."; + $value = base64_encode($message); + + // Errors expected + bindTypeNoEncoding($conn, $sql, $value); + bindDefaultEncoding($conn, $sql, $value); + + // No error expected + insertData($conn, $sql, $value); + + // Fetch data, but test several invalid encoding issues (errors expected) + $sql = "SELECT * FROM $tableName"; + invalidEncoding1($conn, $sql); + invalidEncoding2($conn, $sql); + invalidEncoding3($conn, $sql); + + // Now fetch it back + $stmt = $conn->prepare("SELECT Value FROM $tableName"); + $stmt->bindColumn('Value', $val1, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + $stmt->execute(); + $stmt->fetch(PDO::FETCH_BOUND); + var_dump($val1 === $value); + + // Close connection + dropTable($conn, $tableName); + unset($stmt); + unset($conn); + print "Done\n"; +} catch (PDOException $e) { + var_dump($e->errorInfo); +} +?> +--EXPECT-- +bool(true) +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_569_query_varcharmax.phpt b/test/functional/pdo_sqlsrv/pdo_569_query_varcharmax.phpt new file mode 100644 index 00000000..16947da0 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_569_query_varcharmax.phpt @@ -0,0 +1,106 @@ +--TEST-- +GitHub issue #569 - direct query on varchar max fields results in function sequence error +--DESCRIPTION-- +Verifies that the problem is no longer reproducible. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $tableName = 'pdoTestTable_569'; + dropTable($conn, $tableName); + + if ($qualified && strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + $tsql = "CREATE TABLE $tableName ([c1] varchar(max) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = deterministic, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = AEColumnKey))"; + } else { + $tsql = "CREATE TABLE $tableName ([c1] varchar(max))"; + } + $conn->exec($tsql); + + $input = 'some very large string'; + $tsql = "INSERT INTO $tableName (c1) VALUES (?)"; + $stmt = $conn->prepare($tsql); + $param = array($input); + $stmt->execute($param); + + $tsql = "SELECT * FROM $tableName"; + try { + $stmt = $conn->prepare($tsql); + $stmt->execute(); + } catch (PDOException $e) { + echo ("Failed to read from $tableName\n"); + echo $e->getMessage(); + } + + $row = $stmt->fetch(PDO::FETCH_NUM); + if ($row[0] !== $input) { + echo "Expected $input but got: "; + var_dump($row[0]); + } + + $tsql2 = "DELETE FROM $tableName"; + $rows = $conn->exec($tsql2); + if ($rows !== 1) { + echo 'Expected 1 row affected but got: '; + var_dump($rows); + } + + // Fetch from the empty table + try { + $stmt = $conn->prepare($tsql); + $stmt->execute(); + } catch (PDOException $e) { + echo ("Failed to read $tableName, now empty\n"); + echo $e->getMessage(); + } + + $result = $stmt->fetch(PDO::FETCH_NUM); + if ($result !== false) { + echo 'Expected bool(false) when fetching an empty table but got: '; + var_dump($result); + } + + // Fetch the same table but using client cursor + $stmt = $conn->prepare($tsql, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL)); + $stmt->execute(); + + $result = $stmt->fetch(); + if ($result !== false) { + echo 'Expected bool(false) when fetching an empty table but got: '; + var_dump($result); + } + + dropTable($conn, $tableName); + + unset($stmt); + unset($conn); +} catch (PDOException $e) { + echo $e->getMessage(); +} + +echo "Done\n"; + +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_929_language_option.phpt b/test/functional/pdo_sqlsrv/pdo_929_language_option.phpt new file mode 100644 index 00000000..870a8457 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_929_language_option.phpt @@ -0,0 +1,54 @@ +--TEST-- +GitHub issue 929 - able to change the language when connecting +--DESCRIPTION-- +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +getCode(); + if ($code !== '42S22') { + echo "Expected SQLSTATE 42S22\n"; + var_dump($code); + } + + // The error message is different when testing against Azure DB / Data Warehouse + // Use wildcard patterns for matching + if (isSQLAzure()) { + $expected = "*Invalid column name [\"']BadColumn[\"']\."; + } else { + $expected = "*Ungültiger Spaltenname [\"']BadColumn[\"']\."; + } + + $message = $e->getMessage(); + if (!fnmatch($expected, $message)) { + echo "Expected to find $expected in the error message\n"; + var_dump($message); + } + +} + +require_once("MsSetup.inc"); + +try { + $conn = new PDO("sqlsrv:server=$server;Language = German", $uid, $pwd); + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $tsql = "SELECT *, BadColumn FROM sys.syslanguages"; + $conn->query($tsql); + echo 'This should have failed!\n'; +} catch (PDOException $e) { + verifyErrorContents($e); +} + +unset($conn); + +echo "Done\n"; +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_azure_ad_access_token.phpt b/test/functional/pdo_sqlsrv/pdo_azure_ad_access_token.phpt index f468ffd6..ce36df03 100644 --- a/test/functional/pdo_sqlsrv/pdo_azure_ad_access_token.phpt +++ b/test/functional/pdo_sqlsrv/pdo_azure_ad_access_token.phpt @@ -129,6 +129,25 @@ function simpleTest($conn) dropTable($conn, $tableName); } +function connectAzureDB($accToken, $showException) +{ + global $adServer, $adDatabase, $maxAttempts; + + $conn = false; + try { + $connectionInfo = "Database = $adDatabase; AccessToken = $accToken;"; + $conn = new PDO("sqlsrv:server = $adServer; $connectionInfo"); + } catch (PDOException $e) { + if ($showException) { + echo "Could not connect with Azure AD AccessToken after $maxAttempts retries.\n"; + print_r($e->getMessage()); + echo PHP_EOL; + } + } + + return $conn; +} + // First test some error conditions require_once('MsSetup.inc'); connectWithInvalidOptions($server); @@ -138,13 +157,26 @@ connectWithEmptyAccessToken($server); // Next, test with a valid access token and perform some simple tasks require_once('access_token.inc'); +$maxAttempts = 3; + try { if ($adServer != 'TARGET_AD_SERVER' && $accToken != 'TARGET_ACCESS_TOKEN') { - $connectionInfo = "Database = $adDatabase; AccessToken = $accToken;"; - $conn = new PDO("sqlsrv:server = $adServer; $connectionInfo"); - $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, true); - simpleTest($conn); - unset($conn); + $conn = false; + $numAttempts = 0; + do { + $conn = connectAzureDB($accToken, ($numAttempts == ($maxAttempts - 1))); + if ($conn === false) { + $numAttempts++; + sleep(10); + } + } while ($conn === false && $numAttempts < $maxAttempts); + + // Proceed when successfully connected + if ($conn) { + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, true); + simpleTest($conn); + unset($conn); + } } } catch(PDOException $e) { print_r( $e->getMessage() ); diff --git a/test/functional/pdo_sqlsrv/pdo_azure_ad_authentication.phpt b/test/functional/pdo_sqlsrv/pdo_azure_ad_authentication.phpt index 105f6395..d5cf54b0 100644 --- a/test/functional/pdo_sqlsrv/pdo_azure_ad_authentication.phpt +++ b/test/functional/pdo_sqlsrv/pdo_azure_ad_authentication.phpt @@ -55,21 +55,39 @@ try { // your credentials to test, or this part is skipped. // $azureServer = $adServer; -$azureDatabase = $adDatabase; -$azureUsername = $adUser; -$azurePassword = $adPassword; +$maxAttempts = 3; -if ($azureServer != 'TARGET_AD_SERVER') { +function connectAzureDB($showException) +{ + global $adServer, $adUser, $adPassword, $maxAttempts; + $connectionInfo = "Authentication = ActiveDirectoryPassword; TrustServerCertificate = false"; - + + $conn = false; try { - $conn = new PDO("sqlsrv:server = $azureServer ; $connectionInfo", $azureUsername, $azurePassword); + $conn = new PDO("sqlsrv:server = $adServer; $connectionInfo", $adUser, $adPassword); echo "Connected successfully with Authentication=ActiveDirectoryPassword.\n"; } catch (PDOException $e) { - echo "Could not connect with ActiveDirectoryPassword.\n"; - print_r($e->getMessage()); - echo "\n"; + if ($showException) { + echo "Could not connect with ActiveDirectoryPassword after $maxAttempts retries.\n"; + print_r($e->getMessage()); + echo "\n"; + } } + + return $conn; +} + +if ($azureServer != 'TARGET_AD_SERVER') { + $conn = false; + $numAttempts = 0; + do { + $conn = connectAzureDB($numAttempts == ($maxAttempts - 1)); + if ($conn === false) { + $numAttempts++; + sleep(10); + } + } while ($conn === false && $numAttempts < $maxAttempts); } else { echo "Not testing with Authentication=ActiveDirectoryPassword.\n"; } diff --git a/test/functional/pdo_sqlsrv/pdo_batch_query.phpt b/test/functional/pdo_sqlsrv/pdo_batch_query.phpt new file mode 100644 index 00000000..9453e26d --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_batch_query.phpt @@ -0,0 +1,203 @@ +--TEST-- +Test a batch query with different cursor types +--DESCRIPTION-- +Verifies row and column counts from batch queries. This is the +equivalent of sqlsrv_batch_query.phpt on the sqlsrv side. +TODO: Fix this test once error reporting in PDO is fixed, because batch +queries are not supposed to work with server side cursors. For now, no errors +or warnings are returned. For information on the expected behaviour of cursors +with batch queries, see +https://docs.microsoft.com/en-us/previous-versions/visualstudio/aa266531(v=vs.60) +--SKIPIF-- + +--FILE-- + PDO::CURSOR_FWDONLY), + array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_DYNAMIC), + array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_STATIC), + array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_KEYSET), + array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED), + ); + +// Data for testing, all integer types +$data = array(array(86, -217483648, 0, -432987563, 7, 217483647), + array(0, 31, 127, 255, 1, 10), + array(4534, -212, 32767, 0, 7, -32768), + array(-1, 546098342985600, 9223372000000000000, 5115115115115, 7, -7), + array(0, 1, 0, 0, 1, 1), + ); + +// Column names +$colName = array('c1_int', 'c2_tinyint', 'c3_smallint', 'c4_bigint', 'c5_bit'); + +// Fetch one column at a time +$expectedCols = 1; + +// Number of table rows +$expectedRows = sizeof($data[0]); + +// Expected result sets = number of columns, since the batch fetches each column sequentially +$expectedResultSets = sizeof($colName); + +function checkErrors($expectedError) +{ + // TODO: Fill this in once PDO error reporting is fixed +} + +function checkColumnsAndRows($stmt, $cursor, $before) +{ + global $expectedCols, $expectedRows; + + $cols = $stmt->columnCount(); + + if ($cols != $expectedCols) { + fatalError("Incorrect number of columns returned with $cursor cursor. Expected $expectedCols columns, got $cols columns\n"); + } + + $rows = $stmt->rowCount(); + + // Buffered cursors always return the correct number of rows. Other cursors + // return -1 rows before fetching. Static and keyset cursors return -1 even + // after fetching, while forward and dynamic cursors return the correct + // number of rows after fetching. + if ($cursor == 'buffered') { + if ($rows != $expectedRows) { + fatalError("Incorrect number of columns returned with buffered cursor. Expected $expectedRows rows, got $rows rows\n"); + } + } else { + if ($before) { + if ($rows !== -1) { + fatalError("Incorrect number of rows returned before fetching with a $cursor cursor. Expected -1 rows, got $rows rows\n"); + } + } else { + if ($cursor == 'static' or $cursor == 'keyset') { + if ($rows !== -1) { + fatalError("Incorrect number of rows returned before fetching with a $cursor cursor. Expected -1 rows, got $rows rows\n"); + } + } else { + if ($rows != $expectedRows) { + fatalError("Incorrect number of columns returned with buffered cursor. Expected $expectedRows rows, got $rows rows\n"); + } + } + } + } +} + +function printCursor($element) +{ + $cursor = 'forward'; + switch($element) { + case 0: + echo "Testing with forward cursor...\n"; + break; + case 1: + echo "Testing with dynamic cursor...\n"; + $cursor = 'dynamic'; + break; + case 2: + echo "Testing with static cursor...\n"; + $cursor = 'static'; + break; + case 3: + echo "Testing with keyset cursor...\n"; + $cursor = 'keyset'; + break; + case 4: + echo "Testing with buffered cursor...\n"; + $cursor = 'buffered'; + break; + default: + fatalError("Unknown cursor type! Exiting\n"); + } + return $cursor; +} + +$conn = connect(); +$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +// Create and populate a table of integer types +$tableName = 'batch_query_test'; +$columns = array(new ColumnMeta('int', $colName[0]), + new ColumnMeta('tinyint', $colName[1]), + new ColumnMeta('smallint',$colName[2]), + new ColumnMeta('bigint', $colName[3]), + new ColumnMeta('bit', $colName[4])); + +createTable($conn, $tableName, $columns); + +// Insert each row. Need an associative array to use insertRow() +for ($i = 0; $i < $expectedRows; ++$i) { + $inputs = array(); + for ($j = 0; $j < $expectedResultSets; ++$j) { + $inputs[$colName[$j]] = $data[$j][$i]; + } + + $stmt = insertRow($conn, $tableName, $inputs); + unset($inputs); + unset($stmt); +} + +$query = "SELECT c1_int FROM $tableName; + SELECT c2_tinyint FROM $tableName; + SELECT c3_smallint FROM $tableName; + SELECT c4_bigint FROM $tableName; + SELECT c5_bit FROM $tableName;"; + +// Test the batch query with different cursor types +for ($i = 0; $i < sizeof($cursors); ++$i) { + try { + $cursorType = $cursors[$i]; + $cursor = printCursor($i); + + $stmt = $conn->prepare($query, $cursorType); + $stmt->execute(); + + $numResultSets = 0; + + // Check the column and row count before and after running through + // each result set, because some cursor types may return the number + // of rows only after fetching all rows in the result set + do { + checkColumnsAndRows($stmt, $cursor, true); + + $row = 0; + while ($res = $stmt->fetch(PDO::FETCH_NUM)) { + if ($res[0] != $data[$numResultSets][$row]) { + fatalError("Wrong result, expected ".$data[$numResultSets][$row].", got $res[0]\n"); + } + ++$row; + } + + checkColumnsAndRows($stmt, $cursor, false); + ++$numResultSets; + } while ($next = $stmt->nextRowset()); + + if ($numResultSets != $expectedResultSets) { + fatalError("Unexpected number of result sets, expected $expectedResultedSets, got $numResultSets\n"); + } + } catch(PDOException $e) { + echo "Exception caught\n"; + print_r($e); + } + +unset($stmt); +} + +dropTable($conn, $tableName); +unset($conn); + +echo "Done.\n"; +?> +--EXPECT-- +Testing with forward cursor... +Testing with dynamic cursor... +Testing with static cursor... +Testing with keyset cursor... +Testing with buffered cursor... +Done. diff --git a/test/functional/pdo_sqlsrv/pdo_construct_attr_errors.phpt b/test/functional/pdo_sqlsrv/pdo_construct_attr_errors.phpt new file mode 100644 index 00000000..7b0f101a --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_construct_attr_errors.phpt @@ -0,0 +1,140 @@ +--TEST-- +Test various connection errors with invalid attributes +--DESCRIPTION-- +This is similar to sqlsrv sqlsrv_connStr.phpt such that invalid connection attributes or values used when connecting. +--SKIPIF-- + +--FILE-- + PDO::ERRMODE_EXCEPTION); + $conn = connect("", $options); + $attr = ($binary) ? PDO::SQLSRV_ENCODING_BINARY : 'gibberish'; + + $conn->setAttribute(PDO::SQLSRV_ATTR_ENCODING, $attr); + echo "Should have failed about an invalid encoding.\n"; + } catch (PDOException $e) { + $error = '*An invalid encoding was specified for SQLSRV_ATTR_ENCODING.'; + if (!fnmatch($error, $e->getMessage())) { + echo "invalidEncoding($binary)\n"; + var_dump($e->getMessage()); + } + } +} + +function invalidServer() +{ + global $uid, $pwd; + + // Test an invalid server name in UTF-8 + try { + $options = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION); + $invalid = pack("H*", "ffc0"); + $conn = new PDO("sqlsrv:server = $invalid;", $uid, $pwd, $options); + echo "Should have failed to connect to invalid server.\n"; + } catch (PDOException $e) { + $error1 = '*Login timeout expired'; + $error2 = '*An error occurred translating the connection string to UTF-16: *'; + if (fnmatch($error1, $e->getMessage()) || fnmatch($error2, $e->getMessage())) { + ; // matched at least one of the expected error messages + } else { + echo "invalidServer\n"; + var_dump($e->getMessage()); + } + } +} + +function utf8APP() +{ + global $server, $uid, $pwd; + try { + // Use a UTF-8 name + $app = pack('H*', 'c59ec6a1d0bcc49720c59bc3a4e1839dd180c580e1bb8120ce86c59ac488c4a8c4b02dc5a5e284aec397c5a7'); + $dsn = "APP = $app;"; + $conn = connect($dsn); + } catch (PDOException $e) { + echo "With APP in UTF8 it should not have failed!\n"; + var_dump($e->getMessage()); + } +} + +function invalidCredentials() +{ + global $server, $database; + + // Use valid UTF-8 + $user = pack('H*', 'c59ec6a1d0bcc49720c59bc3a4e1839dd180c580e1bb8120ce86c59ac488c4a8c4b02dc5a5e284aec397c5a7'); + $passwd = pack('H*', 'c59ec6a1d0bcc49720c59bc3a4e1839dd180c580e1bb8120ce86c59ac488c4a8c4b02dc5a5e284aec397c5a7'); + + $options = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION); + $error1 = "*Login failed for user \'*\'."; + $error2 = "*Login timeout expired*"; + $error3 = "*Could not open a connection to SQL Server*"; + + try { + $conn = new PDO("sqlsrv:server = $server; database = $database;", $user, $passwd, $options); + echo "Should have failed to connect\n"; + } catch (PDOException $e) { + if (fnmatch($error1, $e->getMessage()) || + fnmatch($error2, $e->getMessage()) || + fnmatch($error3, $e->getMessage())) { + ; // matched at least one of the expected error messages + } else { + echo "invalidCredentials()\n"; + var_dump($e->getMessage()); + } + } +} + +function invalidPassword() +{ + global $server, $database; + + // Use valid UTF-8 + $user = pack('H*', 'c59ec6a1d0bcc49720c59bc3a4e1839dd180c580e1bb8120ce86c59ac488c4a8c4b02dc5a5e284aec397c5a7'); + // Use invalid UTF-8 + $passwd = pack('H*', 'c59ec6c0d0bcc49720c59bc3a4e1839dd180c580e1bb8120ce86c59ac488c4a8c4b02dc5a5e284aec397c5a7'); + + $options = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION); + + // Possible errors + $error = "*An error occurred translating the connection string to UTF-16: *"; + $error1 = "*Login failed for user \'*\'."; + $error2 = "*Login timeout expired*"; + + try { + $conn = new PDO("sqlsrv:server = $server; database = $database;", $user, $passwd, $options); + echo "Should have failed to connect\n"; + } catch (PDOException $e) { + if (!fnmatch($error, $e->getMessage())) { + // Sometimes it might fail with two other possible error messages + if (fnmatch($error1, $e->getMessage()) || fnmatch($error2, $e->getMessage())) { + ; // matched at least one of the expected error messages + } else { + echo "invalidPassword()\n"; + var_dump($e->getMessage()); + } + } + } +} + +try { + invalidEncoding(false); + invalidEncoding(true); + invalidServer(); + utf8APP(); + invalidCredentials(); + invalidPassword(); + + echo "Done\n"; +} catch (PDOException $e) { + var_dump($e); +} +?> +--EXPECT-- +Done + diff --git a/test/functional/pdo_sqlsrv/pdo_data_classification.phpt b/test/functional/pdo_sqlsrv/pdo_data_classification.phpt new file mode 100644 index 00000000..13add5bb --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_data_classification.phpt @@ -0,0 +1,322 @@ +--TEST-- +Test data classification feature - retrieving sensitivity metadata if supported +--DESCRIPTION-- +If both ODBC and server support this feature, this test verifies that sensitivity metadata can be added and correctly retrieved. If not, it will at least test the new statement attribute and some error cases. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + PDO::ERRMODE_EXCEPTION, PDO::SQLSRV_ATTR_DATA_CLASSIFICATION => true); + $conn = new PDO($dsn, $uid, $pwd, $attr); + } catch (PDOException $e) { + if (!fnmatch($stmtErr, $e->getMessage())) { + echo "Connection attribute test (1) unexpected\n"; + var_dump($e->getMessage()); + } + } + + try { + $dsn = getDSN($server, $databaseName, $driver); + $attr = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION); + $conn = new PDO($dsn, $uid, $pwd, $attr); + $conn->setAttribute(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION, true); + } catch (PDOException $e) { + if (!fnmatch($stmtErr, $e->getMessage())) { + echo "Connection attribute test (2) unexpected\n"; + var_dump($e->getMessage()); + } + } + + try { + $dsn = getDSN($server, $databaseName, $driver); + $attr = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION); + $conn = new PDO($dsn, $uid, $pwd, $attr); + $conn->getAttribute(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION); + } catch (PDOException $e) { + if (!fnmatch($noSupportErr, $e->getMessage())) { + echo "Connection attribute test (3) unexpected\n"; + var_dump($e->getMessage()); + } + } +} + +function testNotAvailable($conn, $tableName, $isSupported, $driverCapable) +{ + // If supported, the query should return a column with no classification + $options = array(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION => true); + $tsql = ($isSupported)? "SELECT PatientId FROM $tableName" : "SELECT * FROM $tableName"; + $stmt = $conn->prepare($tsql, $options); + $stmt->execute(); + + $notAvailableErr = '*Failed to retrieve Data Classification Sensitivity Metadata. If the driver and the server both support the Data Classification feature, check whether the query returns columns with classification information.'; + + $unexpectedErrorState = '*Failed to retrieve Data Classification Sensitivity Metadata: Check if ODBC driver or the server supports the Data Classification feature.'; + + $error = ($driverCapable) ? $notAvailableErr : $unexpectedErrorState; + try { + $metadata = $stmt->getColumnMeta(0); + echo "testNotAvailable: expected getColumnMeta to fail\n"; + } catch (PDOException $e) { + if (!fnmatch($error, $e->getMessage())) { + echo "testNotAvailable: exception unexpected\n"; + var_dump($e->getMessage()); + } + } +} + +function isDataClassSupported($conn, &$driverCapable) +{ + // Check both SQL Server version and ODBC driver version + $msodbcsqlVer = $conn->getAttribute(PDO::ATTR_CLIENT_VERSION)["DriverVer"]; + $version = explode(".", $msodbcsqlVer); + + // ODBC Driver must be 17.2 or above + $driverCapable = true; + if ($version[0] < 17 || $version[1] < 2) { + $driverCapable = false; + return false; + } + + // SQL Server must be SQL Server 2019 or above + $serverVer = $conn->getAttribute(PDO::ATTR_SERVER_VERSION); + if (explode('.', $serverVer)[0] < 15) + return false; + + return true; +} + +function getRegularMetadata($conn, $tsql) +{ + // Run the query without data classification metadata + $stmt1 = $conn->query($tsql); + + // Run the query with the attribute set to false + $options = array(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION => false); + $stmt2 = $conn->prepare($tsql, $options); + $stmt2->execute(); + + // The metadata for each column should be identical + $numCol = $stmt1->columnCount(); + for ($i = 0; $i < $numCol; $i++) { + $metadata1 = $stmt1->getColumnMeta($i); + $metadata2 = $stmt2->getColumnMeta($i); + + $diff = array_diff($metadata1, $metadata2); + if (!empty($diff)) { + print_r($diff); + } + } + + return $stmt1; +} + +function verifyClassInfo($input, $actual) +{ + // For simplicity of this test, only one set of sensitivity data (Label, Information Type) + if (count($actual) != 1) { + echo "Expected an array with only one element\n"; + return false; + } + + if (count($actual[0]) != 2) { + echo "Expected a Label pair and Information Type pair\n"; + return false; + } + + // Label should be name and id pair (id should be empty) + if (count($actual[0]['Label']) != 2) { + echo "Expected only two elements for the label\n"; + return false; + } + $label = $input[0]; + if ($actual[0]['Label']['name'] !== $label || !empty($actual[0]['Label']['id'])){ + return false; + } + + // Like Label, Information Type should also be name and id pair (id should be empty) + if (count($actual[0]['Information Type']) != 2) { + echo "Expected only two elements for the information type\n"; + return false; + } + $info = $input[1]; + if ($actual[0]['Information Type']['name'] !== $info || !empty($actual[0]['Information Type']['id'])){ + return false; + } + + return true; +} + +function compareDataClassification($stmt1, $stmt2, $classData) +{ + global $dataClassKey; + + $numCol = $stmt1->columnCount(); + $noClassInfo = array($dataClassKey => array()); + + for ($i = 0; $i < $numCol; $i++) { + $metadata1 = $stmt1->getColumnMeta($i); + $metadata2 = $stmt2->getColumnMeta($i); + + // If classification sensitivity data exists, only the + // 'flags' field should be different + foreach ($metadata2 as $key => $value) { + if ($key == 'flags') { + // Is classification input data empty? + if (empty($classData[$i])) { + // Then it should be equivalent to $noClassInfo + if ($value !== $noClassInfo) { + var_dump($value); + } + } else { + // Verify the classification metadata + if (!verifyClassInfo($classData[$i], $value[$dataClassKey])) { + var_dump($value); + } + } + } else { + // The other fields should be identical + if ($metadata1[$key] !== $value) { + var_dump($value); + } + } + } + } +} + +function runBatchQuery($conn, $tableName) +{ + global $dataClassKey; + + $options = array(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION => true); + $tsql = "SELECT SSN, BirthDate FROM $tableName"; + + // Run a batch query + $batchQuery = $tsql . ';' . $tsql; + $stmt = $conn->prepare($batchQuery, $options); + $stmt->execute(); + + $numCol = $stmt->columnCount(); + + // The metadata returned should be the same + $c = rand(0, $numCol - 1); + $metadata1 = $stmt->getColumnMeta($c); + $stmt->nextRowset(); + $metadata2 = $stmt->getColumnMeta($c); + + // Check the returned flags + $data1 = $metadata1['flags']; + $data2 = $metadata2['flags']; + + if (!array_key_exists($dataClassKey, $data1) || !array_key_exists($dataClassKey, $data2)) { + echo "Metadata returned with no classification data\n"; + var_dump($data1); + var_dump($data2); + } else { + $jstr1 = json_encode($data1[$dataClassKey]); + $jstr2 = json_encode($data2[$dataClassKey]); + if ($jstr1 !== $jstr2) { + echo "The JSON encoded strings should be identical\n"; + var_dump($jstr1); + var_dump($jstr2); + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////// +try { + testConnAttrCases(); + + $conn = connect(); + $driverCapable = true; + $isSupported = isDataClassSupported($conn, $driverCapable); + + // Create a test table + $tableName = 'pdoPatients'; + $colMeta = array(new ColumnMeta('INT', 'PatientId', 'IDENTITY NOT NULL'), + new ColumnMeta('CHAR(11)', 'SSN'), + new ColumnMeta('NVARCHAR(50)', 'FirstName'), + new ColumnMeta('NVARCHAR(50)', 'LastName'), + new ColumnMeta('DATE', 'BirthDate')); + createTable($conn, $tableName, $colMeta); + + // If data classification is supported, then add sensitivity classification metadata + // to columns SSN and Birthdate + $classData = [ + array(), + array('Highly Confidential - GDPR', 'Credentials'), + array(), + array(), + array('Confidential Personal Data', 'Birthdays') + ]; + + if ($isSupported) { + // column SSN + $label = $classData[1][0]; + $infoType = $classData[1][1]; + $sql = "ADD SENSITIVITY CLASSIFICATION TO [$tableName].SSN WITH (LABEL = '$label', INFORMATION_TYPE = '$infoType')"; + $conn->query($sql); + + // column BirthDate + $label = $classData[4][0]; + $infoType = $classData[4][1]; + $sql = "ADD SENSITIVITY CLASSIFICATION TO [$tableName].BirthDate WITH (LABEL = '$label', INFORMATION_TYPE = '$infoType')"; + $conn->query($sql); + } + + // Test another error condition + testNotAvailable($conn, $tableName, $isSupported, $driverCapable); + + // Run the query without data classification metadata + $tsql = "SELECT * FROM $tableName"; + $stmt = getRegularMetadata($conn, $tsql); + + // Proceeed to retrieve sensitivity metadata, if supported + if ($isSupported) { + $options = array(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION => true); + $stmt1 = $conn->prepare($tsql, $options); + $stmt1->execute(); + + compareDataClassification($stmt, $stmt1, $classData); + + // $stmt2 should produce the same result as the previous $stmt1 + $stmt2 = $conn->prepare($tsql); + $stmt2->execute(); + $stmt2->setAttribute(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION, true); + + compareDataClassification($stmt, $stmt2, $classData); + + unset($stmt1); + unset($stmt2); + + runBatchQuery($conn, $tableName); + } + + dropTable($conn, $tableName); + + unset($stmt); + unset($conn); + + echo "Done\n"; +} catch (PDOException $e) { + var_dump($e->getMessage()); +} +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_empty_result_error.phpt b/test/functional/pdo_sqlsrv/pdo_empty_result_error.phpt index 0d2b730a..32bacc69 100644 --- a/test/functional/pdo_sqlsrv/pdo_empty_result_error.phpt +++ b/test/functional/pdo_sqlsrv/pdo_empty_result_error.phpt @@ -12,6 +12,7 @@ require_once("MsCommon.inc"); // These are the error messages we expect at various points below $errorNoMoreResults = "There are no more results returned by the query."; $errorNoFields = "The active result for the query contains no fields."; +$errorNoMoreRows = "There are no more rows in the active result set. Since this result set is not scrollable, no more data may be retrieved."; // This function compares the expected error message and the error returned by errorInfo(). function CheckError($stmt, $expectedError=NULL) @@ -94,6 +95,7 @@ echo "Empty result set, call fetch first: ##################################\n"; $stmt = $conn->query("TestEmptySetProc @a='a', @b='w'"); Fetch($stmt); +Fetch($stmt, $errorNoMoreRows); NextResult($stmt); Fetch($stmt); NextResult($stmt, $errorNoMoreResults); @@ -158,6 +160,7 @@ Next result... Next result... Empty result set, call fetch first: ################################## Fetch... +Fetch... Next result... Fetch... Next result... diff --git a/test/functional/pdo_sqlsrv/pdo_escape_braces.phpt b/test/functional/pdo_sqlsrv/pdo_escape_braces.phpt new file mode 100644 index 00000000..c8d4f191 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_escape_braces.phpt @@ -0,0 +1,71 @@ +--TEST-- +Test that right braces are escaped correctly and that error messages are correct when they're not +--SKIPIF-- + +--FILE-- +getMessage(), $test[1]) === false) { + print_r("Wrong error message returned for test string ".$test[0].". Expected ".$test[1].", actual output:\n"); + print_r($e->getMessage); + echo "\n"; + } + } +} + +echo "Done.\n"; +?> +--EXPECT-- +Done. diff --git a/test/functional/pdo_sqlsrv/pdo_fetch_column_twice.phpt b/test/functional/pdo_sqlsrv/pdo_fetch_column_twice.phpt new file mode 100644 index 00000000..7e2ec194 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_fetch_column_twice.phpt @@ -0,0 +1,151 @@ +--TEST-- +Test fetchColumn twice in a row. Intentionally trigger various error messages. +--DESCRIPTION-- +This is similar to sqlsrv_fetch_field_twice_data_types.phpt. +--SKIPIF-- + +--FILE-- +prepare($tsql); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if ($row !== false) { + echo "fetchBeforeExecute: fetch should have failed before execute!\n"; + } + $stmt->execute(); + $row = $stmt->fetch(PDO::FETCH_NUM); + + for ($i = 0; $i < count($inputs); $i++) { + if ($row[$i] !== $inputs[$i]) { + echo "fetchBeforeExecute: expected $inputs[$i] but got $row[$i]\n"; + } + } + + unset($stmt); + } catch (PDOException $e) { + var_dump($e->getMessage()); + } +} + +function fetchColumnTwice($conn, $tableName, $col, $input) +{ + try { + $tsql = "SELECT * FROM $tableName"; + $stmt = $conn->query($tsql); + $result = $stmt->fetchColumn($col); + if ($result !== $input) { + echo "fetchColumnTwice (1): expected $input but got $result\n"; + } + $result = $stmt->fetchColumn($col); + if ($result !== false) { + echo "fetchColumnTwice (2): expected the second fetchColumn to fail\n"; + } + + // Re-run the query with fetch style + $stmt = $conn->query($tsql, PDO::FETCH_COLUMN, $col); + $result = $stmt->fetch(); + if ($result !== $input) { + echo "fetchColumnTwice (3): expected $input but got $result\n"; + } + $result = $stmt->fetch(); + if ($result !== false) { + echo "fetchColumnTwice (4): expected the second fetch to fail\n"; + } + $result = $stmt->fetchColumn($col); + echo "fetchColumnTwice (5): expected fetchColumn to throw an exception\n"; + unset($stmt); + } catch (PDOException $e) { + $error = '*There are no more rows in the active result set. Since this result set is not scrollable, no more data may be retrieved.'; + + if (!fnmatch($error, $e->getMessage())) { + echo "Error message unexpected in fetchColumnTwice\n"; + var_dump($e->getMessage()); + } + } +} + +function fetchColumnOutOfBound1($conn, $tableName, $col) +{ + try { + $tsql = "SELECT * FROM $tableName"; + $stmt = $conn->query($tsql); + $result = $stmt->fetchColumn($col); + echo "fetchColumnOutOfBound1: expected fetchColumn to throw an exception\n"; + unset($stmt); + } catch (PDOException $e) { + $error1 = '*General error: Invalid column index'; + $error2 = '*An invalid column number was specified.'; + + // Different errors may be returned depending on running with run-tests.php or not + if (fnmatch($error1, $e->getMessage()) || fnmatch($error2, $e->getMessage())) { + ; + } else { + echo "Error message unexpected in fetchColumnOutOfBound1\n"; + var_dump($e->getMessage()); + } + } +} + +function fetchColumnOutOfBound2($conn, $tableName, $col) +{ + try { + $tsql = "SELECT * FROM $tableName"; + $stmt = $conn->query($tsql, PDO::FETCH_COLUMN, $col); + $result = $stmt->fetch(); + unset($stmt); + } catch (PDOException $e) { + var_dump($e->getMessage()); + } +} + +try { + $conn = connect(); + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $tableName = 'pdoFetchColumnTwice'; + $colMeta = array(new ColumnMeta('int', 'c1_int'), + new ColumnMeta('varchar(20)', 'c2_varchar'), + new ColumnMeta('decimal(5, 3)', 'c3_decimal'), + new ColumnMeta('datetime', 'c4_datetime')); + createTable($conn, $tableName, $colMeta); + + $inputs = array('968580013', 'dummy value', '3.438', ('1756-04-16 23:27:09.130')); + $numCols = count($inputs); + + $tsql = "INSERT INTO $tableName(c1_int, c2_varchar, c3_decimal, c4_datetime) VALUES (?,?,?,?)"; + $stmt = $conn->prepare($tsql); + + for ($i = 0; $i < $numCols; $i++) { + $stmt->bindParam($i + 1, $inputs[$i]); + } + $stmt->execute(); + unset($stmt); + + fetchBeforeExecute($conn, $tableName, $inputs); + for ($i = 0; $i < $numCols; $i++) { + fetchColumnTwice($conn, $tableName, $i, $inputs[$i]); + } + + fetchColumnOutOfBound1($conn, $tableName, -1); + + // Change to warning mode + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); + fetchColumnOutOfBound2($conn, $tableName, $numCols + 1); + + dropTable($conn, $tableName); + unset($conn); + echo "Done\n"; +} catch (PDOException $e) { + var_dump($e); +} +?> +--EXPECTREGEX-- +Warning: PDOStatement::fetch\(\): SQLSTATE\[HY000\]: General error: Invalid column index in .+(\/|\\)pdo_fetch_column_twice.php on line [0-9]+ + +Warning: PDOStatement::fetch\(\): SQLSTATE\[HY000\]: General error in .+(\/|\\)pdo_fetch_column_twice.php on line [0-9]+ +Done diff --git a/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_as_objects.phpt b/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_as_objects.phpt index ab8ba5ec..8463875b 100644 --- a/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_as_objects.phpt +++ b/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_as_objects.phpt @@ -15,11 +15,24 @@ require_once("MsCommon_mid-refactor.inc"); function checkStringValues($obj, $columns, $values) { $size = count($values); - $objArray = (array)$obj; // turn the object into an associated array - for ($i = 0; $i < $size; $i++) { $col = $columns[$i]; - $val = $objArray[$col]; + switch ($i) { + case 0: + $val = $obj->c1; break; + case 1: + $val = $obj->c2; break; + case 2: + $val = $obj->c3; break; + case 3: + $val = $obj->c4; break; + case 4: + $val = $obj->c5; break; + case 5: + $val = $obj->c6; break; + default: + echo "Something went wrong!\n"; + } if ($val != $values[$i]) { echo "Expected $values[$i] for column $col but got: "; @@ -57,6 +70,33 @@ function checkColumnDTValue($index, $column, $values, $dtObj) } } +function randomColumns($conn, $query, $columns, $values) +{ + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + $stmt = $conn->prepare($query); + + // Fetch a random column to trigger caching + $lastCol = count($columns) - 1; + $col = rand(0, $lastCol); + $stmt->execute(); + $dtObj = $stmt->fetchColumn($col); + checkColumnDTValue($col, $columns[$col], $values, $dtObj); + + // Similarly, fetch another column + $col = (++$col) % count($columns); + $stmt->execute(); + $dtObj = $stmt->fetchColumn($col); + checkColumnDTValue($col, $columns[$col], $values, $dtObj); + + // Now fetch all columns in a backward order + $i = $lastCol; + do { + $stmt->execute(); + $dtObj = $stmt->fetchColumn($i); + checkColumnDTValue($i, $columns[$i], $values, $dtObj); + } while (--$i >= 0); +} + function runTest($conn, $query, $columns, $values, $useBuffer = false) { // fetch the date time values as strings or date time objects @@ -90,7 +130,7 @@ function runTest($conn, $query, $columns, $values, $useBuffer = false) $stmt->execute(); $row = $stmt->fetch(PDO::FETCH_BOTH); checkDTObjectValues($row, $columns, $values, PDO::FETCH_BOTH); - + // ATTR_STRINGIFY_FETCHES should have no effect when fetching date time objects // Setting it to true only converts numeric values to strings when fetching // See http://www.php.net/manual/en/pdo.setattribute.php for details @@ -151,7 +191,6 @@ function runTest($conn, $query, $columns, $values, $useBuffer = false) // last test: set statement attribute fetch_datetime on with no change to // prepared statement -- expected datetime objects to be returned $stmt->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); - $stmt->execute(); $i = 0; do { $stmt->execute(); @@ -221,8 +260,9 @@ try { $query = "SELECT * FROM $tableName"; - runTest($conn, $query, $columns, $values); - runTest($conn, $query, $columns, $values, true); + runtest($conn, $query, $columns, $values); + runtest($conn, $query, $columns, $values, true); + randomColumns($conn, $query, $columns, $values); dropTable($conn, $tableName); diff --git a/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_nulls.phpt b/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_nulls.phpt index 9bffdf75..98c7c957 100644 --- a/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_nulls.phpt +++ b/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_nulls.phpt @@ -110,7 +110,6 @@ function runTest($conn, $query, $columns, $useBuffer = false) // last test: set statement attribute fetch_datetime on with no change to // prepared statement -- expected datetime objects to be returned $stmt->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); - $stmt->execute(); $i = 0; do { $stmt->execute(); diff --git a/test/functional/pdo_sqlsrv/pdo_fetch_variants_diff_styles.phpt b/test/functional/pdo_sqlsrv/pdo_fetch_variants_diff_styles.phpt index 311538ab..e99aae85 100644 --- a/test/functional/pdo_sqlsrv/pdo_fetch_variants_diff_styles.phpt +++ b/test/functional/pdo_sqlsrv/pdo_fetch_variants_diff_styles.phpt @@ -200,7 +200,7 @@ function fetchColumns($conn, $tableName, $numRows, $numCols) $res = $stmtTmp->execute(); if (! $res) { - echo "Failed to insert data from column ". $j +1 ."\n"; + echo "Failed to insert data from column ". ($j + 1) ."\n"; } } diff --git a/test/functional/pdo_sqlsrv/pdo_insert_fetch_invalid_utf16.phpt b/test/functional/pdo_sqlsrv/pdo_insert_fetch_invalid_utf16.phpt new file mode 100644 index 00000000..c90e3135 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_insert_fetch_invalid_utf16.phpt @@ -0,0 +1,86 @@ +--TEST-- +Test fetching invalid UTF-16 from the server +--DESCRIPTION-- +This is similar to sqlsrv 0079.phpt with checking for error conditions concerning encoding issues. +--SKIPIF-- + +--FILE-- +setAttribute(PDO::SQLSRV_ATTR_ENCODING, PDO::SQLSRV_ENCODING_SYSTEM); + + // Create test table + $tableName = 'pdoUTF16invalid'; + $columns = array(new ColumnMeta('int', 'id', 'identity'), + new ColumnMeta('nvarchar(100)', 'c1')); + $stmt = createTable($conn, $tableName, $columns); + + // 0xdc00,0xdbff is an invalid surrogate pair + $invalidUTF16 = pack("H*", '410042004300440000DCFFDB45004600'); + + $insertSql = "INSERT INTO $tableName (c1) VALUES (?)"; + $stmt = $conn->prepare($insertSql); + $stmt->bindParam(1, $invalidUTF16, PDO::PARAM_STR, null, PDO::SQLSRV_ENCODING_BINARY); + $stmt->execute(); + + try { + // Now fetch data with UTF-8 encoding + $tsql = "SELECT * FROM $tableName"; + $stmt = $conn->prepare($tsql); + $stmt->setAttribute(PDO::SQLSRV_ATTR_ENCODING, PDO::SQLSRV_ENCODING_UTF8); + $stmt->execute(); + $utf8 = $stmt->fetchColumn(1); // Ignore the id column + echo "fetchColumn should have failed with an error.\n"; + } catch (PDOException $e) { + $error = '*An error occurred translating string for a field to UTF-8:*'; + if ($e->getCode() !== "IMSSP" || !fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } + + dropProc($conn, 'Utf16InvalidOut'); + $createProc = <<query($createProc); + + try { + $invalidUTF16Out = ''; + $tsql = '{call Utf16InvalidOut(?)}'; + $stmt = $conn->prepare($tsql); + $stmt->setAttribute(PDO::SQLSRV_ATTR_ENCODING, PDO::SQLSRV_ENCODING_UTF8); + $stmt->bindParam(1, $invalidUTF16Out, PDO::PARAM_STR, 25); + $stmt->execute(); + } catch (PDOException $e) { + $error = '*An error occurred translating string for an output param to UTF-8:*'; + if ($e->getCode() !== "IMSSP" || !fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } + + echo "Done\n"; + + // Done testing with the stored procedure and test table + dropProc($conn, 'Utf16InvalidOut'); + dropTable($conn, $tableName); + + unset($stmt); + unset($conn); +} catch (PDOException $e) { + var_dump($e->errorInfo); +} +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_insert_fetch_utf8stream.phpt b/test/functional/pdo_sqlsrv/pdo_insert_fetch_utf8stream.phpt new file mode 100644 index 00000000..b5d2c9b2 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_insert_fetch_utf8stream.phpt @@ -0,0 +1,106 @@ +--TEST-- +Test inserting UTF-8 stream via PHP including some checking of error conditions +--DESCRIPTION-- +This is similar to sqlsrv 0067.phpt with checking for error conditions concerning encoding issues. +--SKIPIF-- + +--FILE-- +prepare($insertSql); + $stmt->bindParam(1, $f1); + $stmt->bindParam(2, $f2); + $stmt->bindParam(3, $f3); + $stmt->bindParam(4, $f4, PDO::PARAM_LOB); + + $stmt->execute(); + + // Next test UTF-8 cutoff in the middle of a valid 3 byte UTF-8 char + $utf8 = str_repeat("41", 8188); + $utf8 = $utf8 . "e38395"; + $utf8 = pack("H*", $utf8); + $f4 = fopen("data://text/plain," . $utf8, "r"); + $stmt->bindParam(4, $f4, PDO::PARAM_LOB); + $stmt->execute(); + + // Now test a 2 byte incomplete character + $utf8 = str_repeat("41", 8188); + $utf8 = $utf8 . "dfa0"; + $utf8 = pack("H*", $utf8); + $f4 = fopen("data://text/plain," . $utf8, "r"); + $stmt->bindParam(4, $f4, PDO::PARAM_LOB); + $stmt->execute(); + + // Then test a 4 byte incomplete character + $utf8 = str_repeat("41", 8186); + $utf8 = $utf8 . "f1a680bf"; + $utf8 = pack("H*", $utf8); + $f4 = fopen("data://text/plain," . $utf8, "r"); + $stmt->bindParam(4, $f4, PDO::PARAM_LOB); + $stmt->execute(); + + // Finally, verify error conditions with invalid inputs + $error = '*An error occurred translating a PHP stream from UTF-8 to UTF-16:*'; + + // First test UTF-8 cutoff (really cutoff) + $utf8 = str_repeat("41", 8188); + $utf8 = $utf8 . "e383"; + $utf8 = pack("H*", $utf8); + $f4 = fopen("data://text/plain," . $utf8, "r"); + try { + $stmt->bindParam(4, $f4, PDO::PARAM_LOB); + $stmt->execute(); + echo "Should have failed with a cutoff UTF-8 string\n"; + } catch (PDOException $e) { + if ($e->getCode() !== "IMSSP" || !fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } + + // Then test UTF-8 invalid/corrupt stream + $utf8 = str_repeat("41", 8188); + $utf8 = $utf8 . "e38395e38395"; + $utf8 = substr_replace($utf8, "fe", 1000, 2); + $utf8 = pack("H*", $utf8); + $f4 = fopen("data://text/plain," . $utf8, "r"); + try { + $stmt->bindParam(4, $f4, PDO::PARAM_LOB); + $stmt->execute(); + echo "Should have failed with an invalid UTF-8 string\n"; + } catch (PDOException $e) { + if ($e->getCode() !== "IMSSP" || !fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } + + echo "Done\n"; + + // Done testing with stored procedures and table + dropTable($conn, $tableName); + + unset($stmt); + unset($conn); +} catch (PDOException $e) { + var_dump($e->errorInfo); +} +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_insert_fetch_utf8text.phpt b/test/functional/pdo_sqlsrv/pdo_insert_fetch_utf8text.phpt new file mode 100644 index 00000000..acbeaf23 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_insert_fetch_utf8text.phpt @@ -0,0 +1,233 @@ +--TEST-- +Test inserting and retrieving UTF-8 text +--DESCRIPTION-- +This is similar to sqlsrv 0065.phpt with checking for error conditions concerning encoding issues. +--SKIPIF-- + +--FILE-- + 0) { + if ($results[$i] !== $utf8) { + echo $columns[$i]->colName . ' does not match the inserted UTF-8 text'; + var_dump($results[$i]); + } + } else { + // The first column, a varchar(100) column, should have question marks, + // like this one: + $expected = "So?e sä???? ?SCII-te×t"; + // With AE, the fetched result may be different in Windows and other + // platforms -- the point is to check if there are some '?' + if (!isAEConnected() && $results[$i] !== $expected) { + echo $columns[$i]->colName . " does not match $expected"; + var_dump($results[$i]); + } else { + $arr = explode('?', $results[$i]); + if (count($arr) == 1) { + // this means there is no question mark in $t + echo $columns[$i]->colName . " value is unexpected"; + var_dump($results[$i]); + } + } + } + } +} + +function dropProcedures($conn) +{ + // Drop all procedures + dropProc($conn, "pdoIntDoubleProc"); + dropProc($conn, "pdoUTF8OutProc"); + dropProc($conn, "pdoUTF8OutWithResultsetProc"); + dropProc($conn, "pdoUTF8InOutProc"); +} + +function createProcedures($conn, $tableName) +{ + // Drop all procedures first + dropProcedures($conn); + + $createProc = <<query($createProc); + + $createProc = "CREATE PROCEDURE pdoUTF8OutWithResultsetProc @param NVARCHAR(25) OUTPUT AS BEGIN SELECT c1, c2, c3 FROM $tableName SET @param = CONVERT(NVARCHAR(25), 0x5E01A1013C04170120005B01E400DD1040044001C11E200086035A010801280130012D0065012E21D7006701); END"; + $stmt = $conn->query($createProc); + + $createProc = "CREATE PROCEDURE pdoUTF8InOutProc @param NVARCHAR(25) OUTPUT AS BEGIN SET @param = CONVERT(NVARCHAR(25), 0x6001E11EDD10130120006101E200DD1040043A01BB1E2000C5005A01C700CF0007042D006501BF1E45046301); END"; + $stmt = $conn->query($createProc); + + $createProc = "CREATE PROCEDURE pdoIntDoubleProc @param INT OUTPUT AS BEGIN SET @param = @param + @param; END;"; + $stmt = $conn->query($createProc); +} + +function runBaselineProc($conn) +{ + $sql = "{call pdoIntDoubleProc(?)}"; + $val = 1; + $stmt = $conn->prepare($sql); + $stmt->bindParam(1, $val, PDO::PARAM_INT | PDO::PARAM_INPUT_OUTPUT, 100); + $stmt->execute(); + + if ($val !== 2) { + echo "Incorrect value $val for pdoIntDoubleProc\n"; + } +} + +function runImmediateConversion($conn, $utf8) +{ + $sql = "{call pdoUTF8OutProc(?)}"; + $val = ''; + $stmt = $conn->prepare($sql); + $stmt->bindParam(1, $val, PDO::PARAM_STR, 50); + $stmt->execute(); + + if ($val !== $utf8) { + echo "Incorrect value $val for pdoUTF8OutProc\n"; + } +} + +function runProcWithResultset($conn, $utf8) +{ + $sql = "{call pdoUTF8OutWithResultsetProc(?)}"; + $val = ''; + $stmt = $conn->prepare($sql); + $stmt->bindParam(1, $val, PDO::PARAM_STR, 50); + $stmt->execute(); + + // Moves the cursor to the next result set + $stmt->nextRowset(); + + if ($val !== $utf8) { + echo "Incorrect value $val for pdoUTF8OutWithResultsetProc\n"; + } +} + +function runInOutProcWithErrors($conn, $utf8_2) +{ + // The input string is smaller than the output size for testing + $val = 'This is a test.'; + + // The following should work + $sql = "{call pdoUTF8InOutProc(?)}"; + $stmt = $conn->prepare($sql); + $stmt->bindParam(1, $val, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT, 25); + $stmt->execute(); + + if ($val !== $utf8_2) { + echo "Incorrect value $val for pdoUTF8InOutProc Part 1\n"; + } + + // Use a much longer input string + $val = 'This is a longer test that exceeds the returned values buffer size so that we can test an input buffer size larger than the output buffer size.'; + try { + $stmt->bindParam(1, $val, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT, 25); + $stmt->execute(); + echo "Should have failed since the string is too long!\n"; + } catch (PDOException $e) { + $error = '*String data, right truncation'; + if ($e->getCode() !== "22001" || !fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } +} + +function runIntDoubleProcWithErrors($conn) +{ + $sql = "{call pdoIntDoubleProc(?)}"; + $val = pack('H*', 'ffffffff'); + + try { + $stmt = $conn->prepare($sql); + $stmt->bindParam(1, $val, PDO::PARAM_STR); + $stmt->execute(); + echo "Should have failed because of an invalid utf-8 string!\n"; + } catch (PDOException $e) { + $error = '*An error occurred translating string for input param 1 to UCS-2:*'; + if ($e->getCode() !== "IMSSP" || !fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } +} + +try { + $conn = connect(); + + // Create test table + $tableName = 'pdoUTF8test'; + $columns = array(new ColumnMeta('varchar(100)', 'c1'), + new ColumnMeta('nvarchar(100)', 'c2'), + new ColumnMeta('nvarchar(max)', 'c3')); + $stmt = createTable($conn, $tableName, $columns); + + $utf8 = "Şơмė śäოрŀề ΆŚĈĨİ-ť℮×ŧ"; + + $insertSql = "INSERT INTO $tableName (c1, c2, c3) VALUES (?, ?, ?)"; + $stmt1 = $conn->prepare($insertSql); + $stmt1->bindParam(1, $utf8); + $stmt1->bindParam(2, $utf8); + $stmt1->bindParam(3, $utf8); + + $stmt1->execute(); + + $stmt2 = $conn->prepare("SELECT c1, c2, c3 FROM $tableName"); + $stmt2->execute(); + $results = $stmt2->fetch(PDO::FETCH_NUM); + verifyColumnData($columns, $results, $utf8); + + // Start creating stored procedures for testing + createProcedures($conn, $tableName); + + runBaselineProc($conn); + runImmediateConversion($conn, $utf8); + runProcWithResultset($conn, $utf8); + + // Use another set of UTF-8 text to test + $utf8_2 = "Šỡოē šâოрĺẻ ÅŚÇÏЇ-ťếхţ"; + runInOutProcWithErrors($conn, $utf8_2); + + // Now insert second row + $utf8_3 = pack('H*', '7a61cc86c7bdceb2f18fb3bf'); + $stmt1->bindParam(1, $utf8_3); + $stmt1->bindParam(2, $utf8_3); + $stmt1->bindParam(3, $utf8_3); + $stmt1->execute(); + + // Fetch data, ignoring first row + $stmt2->execute(); + $stmt2->fetch(PDO::FETCH_NUM); + + // Move to the second row and check second field + $results2 = $stmt2->fetch(PDO::FETCH_NUM); + if ($results2[1] !== $utf8_3) { + echo "Unexpected $results2[1] from field 2 in second row.\n"; + } + + // Last test with an invalid input + runIntDoubleProcWithErrors($conn); + + echo "Done\n"; + + // Done testing with stored procedures and table + dropProcedures($conn); + dropTable($conn, $tableName); + + unset($stmt1); + unset($stmt2); + unset($conn); +} catch (PDOException $e) { + var_dump($e->errorInfo); +} +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_output_decimal.phpt b/test/functional/pdo_sqlsrv/pdo_output_decimal.phpt index b4512c31..3cdee20c 100644 --- a/test/functional/pdo_sqlsrv/pdo_output_decimal.phpt +++ b/test/functional/pdo_sqlsrv/pdo_output_decimal.phpt @@ -42,7 +42,7 @@ try { $expected1 = "7.4"; $expected2 = "7"; if ($outValue1 == $expected1 && $outValue2 == $expected2) { - echo "Test Successfully\n"; + echo "Test Successful\n"; } dropProc($conn, $proc_scale); @@ -56,4 +56,4 @@ try { ?> --EXPECT-- -Test Successfully +Test Successful diff --git a/test/functional/pdo_sqlsrv/pdo_output_decimal_errors.phpt b/test/functional/pdo_sqlsrv/pdo_output_decimal_errors.phpt new file mode 100644 index 00000000..5ec6c6fb --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_output_decimal_errors.phpt @@ -0,0 +1,128 @@ +--TEST-- +Call stored procedures with inputs of different datatypes to get outputs of various types +--DESCRIPTION-- +Similar to pdo_output_decimal.phpt but this time intentionally test some error cases +--SKIPIF-- + +--FILE-- +prepare("{CALL $proc (?, ?, ?)}"); + $stmt->bindValue(1, $inValue1); + $stmt->bindValue(2, $inValue2); + $stmt->bindParam(3, $outValue1, PDO::PARAM_STR, -1); + $stmt->execute(); + } catch (PDOException $e) { + $error = '*Invalid size for output string parameter 3. Input/output string parameters must have an explicit length.'; + + if (!fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } +} + +function testInvalidDirection($conn, $proc) +{ + global $inValue1, $inValue2, $outValue1; + + // Request input output parameter but do not provide a size + try { + $stmt = $conn->prepare("{CALL $proc (?, ?, ?)}"); + $stmt->bindValue(1, $inValue1); + $stmt->bindValue(2, $inValue2); + $stmt->bindParam(3, $outValue1, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT); + $stmt->execute(); + } catch (PDOException $e) { + $error = '*Invalid direction specified for parameter 3. Input/output parameters must have a length.'; + + if (!fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } +} + +function testInvalidType($conn, $proc) +{ + global $inValue1, $inValue2; + + $outValue = 0.3; + + // Pass an invalid type that is incompatible for the output parameter + try { + $stmt = $conn->prepare("{CALL $proc (?, ?, ?)}"); + $stmt->bindValue(1, $inValue1); + $stmt->bindValue(2, $inValue2); + $stmt->bindParam(3, $outValue, PDO::PARAM_BOOL | PDO::PARAM_INPUT_OUTPUT, 1024); + $stmt->execute(); + } catch (PDOException $e) { + $error = '*Types for parameter value and PDO::PARAM_* constant must be compatible for input/output parameter 3.'; + + if (!fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } +} + +try { + $conn = connect(); + + $proc_scale = getProcName('scale_proc'); + $proc_no_scale = getProcName('noScale_proc'); + + $stmt = $conn->exec("CREATE PROC $proc_scale (@p1 DECIMAL(18, 1), @p2 DECIMAL(18, 1), @p3 CHAR(128) OUTPUT) + AS BEGIN SELECT @p3 = CONVERT(CHAR(128), @p1 + @p2) END"); + + $inValue1 = '2.1'; + $inValue2 = '5.3'; + $outValue1 = '0'; + $outValue2 = '0'; + + // First error case: pass an invalid size for the output parameter + testInvalidSize($conn, $proc_scale); + testInvalidDirection($conn, $proc_scale); + testInvalidType($conn, $proc_scale); + + $stmt = $conn->prepare("{CALL $proc_scale (?, ?, ?)}"); + $stmt->bindValue(1, $inValue1); + $stmt->bindValue(2, $inValue2); + $stmt->bindParam(3, $outValue1, PDO::PARAM_STR, 300); + $stmt->execute(); + + $outValue1 = trim($outValue1); + + $stmt = $conn->exec("CREATE PROC $proc_no_scale (@p1 DECIMAL, @p2 DECIMAL, @p3 CHAR(128) OUTPUT) + AS BEGIN SELECT @p3 = CONVERT(CHAR(128), @p1 + @p2) END"); + + $stmt = $conn->prepare("{CALL $proc_no_scale (?, ?, ?)}"); + $stmt->bindValue(1, $inValue1); + $stmt->bindValue(2, $inValue2); + $stmt->bindParam(3, $outValue2, PDO::PARAM_STR, 300); + $stmt->execute(); + + $outValue2 = trim($outValue2); + + $expected1 = "7.4"; + $expected2 = "7"; + if ($outValue1 == $expected1 && $outValue2 == $expected2) { + echo "Test Successfully done\n"; + } + + dropProc($conn, $proc_scale); + dropProc($conn, $proc_no_scale); + + unset($stmt); + unset($conn); +} catch (Exception $e) { + echo $e->getMessage(); +} +?> + +--EXPECT-- +Test Successfully done diff --git a/test/functional/pdo_sqlsrv/pdo_test_non_LOB_types.phpt b/test/functional/pdo_sqlsrv/pdo_test_non_LOB_types.phpt new file mode 100644 index 00000000..cd709eb1 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_test_non_LOB_types.phpt @@ -0,0 +1,78 @@ +--TEST-- +Test reading non LOB types +--DESCRIPTION-- +A simpler version of sqlsrv test "test_sqlsrv_phptype_stream.phpt" for reading from +pre-populated tables [test_streamable_types] and [test_types] +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // test the allowed non LOB column types + $tsql = "SELECT [char_short_type], [varchar_short_type], [nchar_short_type], [nvarchar_short_type], [binary_short_type], [varbinary_short_type] FROM [test_streamable_types]"; + $stmt = $conn->query($tsql); + + $result = $stmt->fetch(PDO::FETCH_NUM); + verifyResult($result); + + // test not streamable types + $tsql = "SELECT * FROM [test_types]"; + $stmt = $conn->query($tsql); + $result = $stmt->fetch(PDO::FETCH_NUM); + print_r($result); + +} catch (PDOException $e) { + var_dump($e->errorInfo); +} + +unset($stmt); +unset($conn); + +?> +--EXPECT-- +Array +( + [0] => 9223372036854775807 + [1] => 2147483647 + [2] => 32767 + [3] => 255 + [4] => 1 + [5] => 9999999999999999999999999999999999999 + [6] => 922337203685477.5807 + [7] => 214748.3647 + [8] => 1.79E+308 + [9] => 1.1799999E-38 + [10] => 1968-12-12 16:20:00.000 + [11] => +) \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_utf8_conn.phpt b/test/functional/pdo_sqlsrv/pdo_utf8_conn.phpt index a5c31e09..0c4f88f8 100644 --- a/test/functional/pdo_sqlsrv/pdo_utf8_conn.phpt +++ b/test/functional/pdo_sqlsrv/pdo_utf8_conn.phpt @@ -24,6 +24,6 @@ if ($c !== false) { Fatal error: Uncaught PDOException: SQLSTATE\[(28000|08001|HYT00)\]: .*\[Microsoft\]\[ODBC Driver 1[0-9] for SQL Server\](\[SQL Server\])?(Named Pipes Provider: Could not open a connection to SQL Server \[2\]\. |TCP Provider: Error code (0x2726|0x2AF9)|Login timeout expired|Login failed for user 'sa'\.) in .+(\/|\\)pdo_utf8_conn\.php:[0-9]+ Stack trace: -#0 .+(\/|\\)pdo_utf8_conn\.php\([0-9]+\): PDO->__construct\('sqlsrv:Server=l\.\.\.', 'sa', 'Sunshine4u'\) +#0 .+(\/|\\)pdo_utf8_conn\.php\([0-9]+\): PDO->__construct(\(\)|\('sqlsrv:Server=l\.\.\.', 'sa', 'Sunshine4u'\)) #1 {main} thrown in .+(\/|\\)pdo_utf8_conn\.php on line [0-9]+ diff --git a/test/functional/pdo_sqlsrv/pdo_warning_errors.phpt b/test/functional/pdo_sqlsrv/pdo_warning_errors.phpt new file mode 100644 index 00000000..d92da15d --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_warning_errors.phpt @@ -0,0 +1,107 @@ +--TEST-- +Test various scenarios which all return the same error about statement not executed +--DESCRIPTION-- +This is similar to sqlsrv test_warning_errors2.phpt with checking for error conditions concerning fetching and metadata. +--SKIPIF-- + +--FILE-- +nextRowset(); + if (!is_null($error)) { + echo "getNextResult: expect this to fail with an error from the driver\n"; + } elseif ($result !== false) { + echo "getNextResult: expect this to simply return false\n"; + } + } catch (PDOException $e) { + if ($e->getCode() !== "IMSSP" || !fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } +} + +try { + $conn = connect(); + + $tsql = 'SELECT name FROM sys.objects'; + $stmt = $conn->prepare($tsql); + + $colCount = $stmt->columnCount(); + if ($colCount != 0) { + echo "Before execute(), result set should only have 0 columns\n"; + } + $metadata = $stmt->getColumnMeta(0); + if ($metadata !== false) { + echo "Before execute(), result set is empty so getColumnMeta should have failed\n"; + } + + // When fetching, PDO checks if statement is executed before passing the + // control to the driver, so it simply fails without error message + $result = $stmt->fetch(PDO::FETCH_ASSOC); + if ($result !== false) { + echo "Before execute(), fetch should have failed\n"; + } + + $result = $stmt->fetchAll(PDO::FETCH_ASSOC); + var_dump($result); + + $result = $stmt->fetchAll(PDO::FETCH_COLUMN, 0); + var_dump($result); + + getNextResult($stmt, $execError); + + // Now, call execute() + $stmt->execute(); + + $colCount = $stmt->columnCount(); + if ($colCount != 1) { + echo "Expected only one column\n"; + } + + $metadata = $stmt->getColumnMeta(0); + if ($metadata['native_type'] !== 'string') { + echo "The metadata returned is unexpected: \n"; + var_dump($metadata); + } + + $result = $stmt->fetch(PDO::FETCH_ASSOC); + if (!is_array($result) && count($result) == 0) { + echo "After execute(), fetch should have returned an array with results\n"; + } + + $result = $stmt->fetchAll(PDO::FETCH_COLUMN, 0); + if (!is_array($result) && count($result) == 0) { + echo "After execute(), fetchAll should have returned an array with results\n"; + } + + getNextResult($stmt); + + getNextResult($stmt, $noMoreResult); + + $result = $stmt->fetch(PDO::FETCH_ASSOC); + if ($result !== false) { + // When nextRowset() fails, it resets the execute flag to false + echo "After nextRowset failed, fetch should have failed\n"; + } + + echo "Done\n"; + + unset($stmt); + unset($conn); +} catch (PDOException $e) { + var_dump($e->errorInfo); +} +?> +--EXPECT-- +array(0) { +} +array(0) { +} +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt b/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt index 62bc542d..5f1a4f43 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt @@ -431,6 +431,6 @@ Test_9 : FETCH_INVALID : Fatal error: Uncaught Error: Undefined class constant 'FETCH_UNKNOWN' in %s:%x Stack trace: -#0 %s: fetchAllInvalid(Object(PDO), 'PDO_MainTypes') +#0 %s: fetchAllInvalid(%S) #1 {main} thrown in %s on line %x diff --git a/test/functional/pdo_sqlsrv/pdostatement_fetch_orientation.phpt b/test/functional/pdo_sqlsrv/pdostatement_fetch_orientation.phpt index b8fd577a..a14d6dde 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_fetch_orientation.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_fetch_orientation.phpt @@ -35,7 +35,7 @@ try { throw new Exception( "Not A" ); } $row = $stmt1->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_PRIOR ); - if( $row[ 'val' ] != false ) { + if ($row !== false) { throw new Exception( "Not false" ); } @@ -53,7 +53,7 @@ try { throw new Exception( "Not A" ); } $row = $stmt1->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_REL, -1 ); - if( $row[ 'val' ] != false ) { + if ($row !== false) { throw new Exception( "Not false" ); } @@ -63,7 +63,7 @@ try { throw new Exception( "Not C" ); } $row = $stmt1->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_NEXT ); - if( $row[ 'val' ] != false ) { + if ($row !== false) { throw new Exception( "Not false" ); } @@ -73,7 +73,7 @@ try { throw new Exception( "Not C" ); } $row = $stmt1->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_REL, 1 ); - if( $row[ 'val' ] != false ) { + if ($row !== false) { throw new Exception( "Not false" ); } @@ -99,7 +99,7 @@ try { throw new Exception( "Not A" ); } $row = $stmt1->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_ABS, -1 ); - if( $row[ 'val' ] != false ) { + if ($row !== false) { throw new Exception( "Not false" ); } @@ -125,7 +125,7 @@ try { throw new Exception( "Not C" ); } $row = $stmt1->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_NEXT); - if( $row[ 'val' ] != false ) { + if ($row !== false) { throw new Exception( "Not false" ); } @@ -135,7 +135,7 @@ try { throw new Exception( "Not A" ); } $row = $stmt1->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_PRIOR); - if( $row[ 'val' ] != false ) { + if ($row !== false) { throw new Exception( "Not false" ); } @@ -145,7 +145,7 @@ try { throw new Exception( "Not A" ); } $row = $stmt1->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_REL, -1); - if( $row[ 'val' ] != false ) { + if ($row !== false) { throw new Exception( "Not false" ); } diff --git a/test/functional/pdo_sqlsrv/pdostatement_fetch_style.phpt b/test/functional/pdo_sqlsrv/pdostatement_fetch_style.phpt index 000931b6..5170bd97 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_fetch_style.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_fetch_style.phpt @@ -257,6 +257,6 @@ Test_9 : FETCH_INVALID : Fatal error: Uncaught Error: Undefined class constant 'FETCH_UNKNOWN' in %s:%x Stack trace: -#0 %s: fetchWithStyle(Object(PDO), 'PDO_MainTypes', 'PDO::FETCH_INVA...') +#0 %s: fetchWithStyle(%S) #1 {main} thrown in %s on line %x diff --git a/test/functional/pdo_sqlsrv/pdostatement_format_money_types.phpt b/test/functional/pdo_sqlsrv/pdostatement_format_money_types.phpt index c02d634a..1f1d341a 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_format_money_types.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_format_money_types.phpt @@ -64,17 +64,9 @@ function testFloatTypes($conn, $numDigits) trace("testFloatTypes: $floatVal1, $floatVal\n"); - // Check if the numbers of decimal digits are the same - // It is highly unlikely but not impossible - $numbers = explode('.', $floatStr); - $len = strlen($numbers[1]); - if ($len == $numDigits && $floatVal1 != $floatVal) { - echo "Expected $floatVal but $floatVal1 returned. \n"; - } else { - $diff = abs($floatVal1 - $floatVal) / $floatVal; - if ($diff > $epsilon) { - echo "$diff: Expected $floatVal but $floatVal1 returned. \n"; - } + $diff = abs($floatVal1 - $floatVal) / $floatVal; + if ($diff > $epsilon) { + echo "$diff: Expected $floatVal but $floatVal1 returned. \n"; } } } diff --git a/test/functional/pdo_sqlsrv/pdostatement_get_set_attr.phpt b/test/functional/pdo_sqlsrv/pdostatement_get_set_attr.phpt index af341baa..087ce296 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_get_set_attr.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_get_set_attr.phpt @@ -1,5 +1,5 @@ --TEST-- -Test setting and getting various statement attributes. +Test setting and getting various statement attributes with error verifications. --SKIPIF-- --FILE-- @@ -8,40 +8,30 @@ Test setting and getting various statement attributes. function set_stmt_option($conn, $arr) { try { - - $stmt = $conn->prepare( "Select * from temptb", $arr ); + $stmt = $conn->prepare("Select * from temptb", $arr); return $stmt; - } - - catch( PDOException $e) - { + } catch (PDOException $e) { echo $e->getMessage() . "\n\n"; - return NULL; - } + return null; + } } function set_stmt_attr($conn, $attr, $val) { - $stmt = NULL; - try - { + $stmt = null; + try { echo "Set Attribute: " . $attr . "\n"; - $stmt = $conn->prepare( "Select * from temptb"); - } - catch( PDOException $e) - { + $stmt = $conn->prepare("Select * from temptb"); + } catch (PDOException $e) { echo $e->getMessage() . "\n\n"; - return NULL; + return null; } try { $res = $stmt->setAttribute(constant($attr), $val); var_dump($res); echo "\n\n"; - } - - catch( PDOException $e) - { + } catch (PDOException $e) { echo $e->getMessage() . "\n\n"; } return $stmt; @@ -49,27 +39,23 @@ function set_stmt_attr($conn, $attr, $val) function get_stmt_attr($stmt, $attr) { - try - { + try { echo "Get Attribute: " . $attr. "\n"; $res = $stmt->getAttribute(constant($attr)); var_dump($res); echo "\n"; - } - - catch( PDOException $e) - { + } catch (PDOException $e) { echo $e->getMessage() . "\n\n"; - } + } } // valid function Test1($conn) -{ - echo "Test1 - Set stmt option: SQLSRV_ATTR_ENCODING, ATTR_CURSOR, SQLSRV_ATTR_QUERY_TIMEOUT \n"; - set_stmt_option($conn, array(PDO::SQLSRV_ATTR_ENCODING => 3, PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY, PDO::SQLSRV_ATTR_QUERY_TIMEOUT => 44)); - echo "Test Successful\n\n"; -} + { + echo "Test1 - Set stmt option: SQLSRV_ATTR_ENCODING, ATTR_CURSOR, SQLSRV_ATTR_QUERY_TIMEOUT \n"; + set_stmt_option($conn, array(PDO::SQLSRV_ATTR_ENCODING => 3, PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY, PDO::SQLSRV_ATTR_QUERY_TIMEOUT => 44)); + echo "Test Successful\n\n"; + } // invalid function Test2($conn) @@ -84,10 +70,11 @@ function Test3($conn) echo "Test3 \n"; $attr = "PDO::ATTR_CURSOR"; $stmt = set_stmt_attr($conn, $attr, 1); - if($stmt) + if ($stmt) { get_stmt_attr($stmt, $attr); - else + } else { echo "Test3: stmt was null"; + } } // not supported attribute @@ -117,16 +104,33 @@ function Test6($conn) $attr = "PDO::SQLSRV_ATTR_QUERY_TIMEOUT"; $stmt = set_stmt_attr($conn, $attr, 45); get_stmt_attr($stmt, $attr); - } - -try -{ +// PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED or invalid option +// Expect errors +function Test7($conn) +{ + echo "Test7 - Set stmt option: SQLSRV_ATTR_CURSOR_SCROLL_TYPE \n"; + set_stmt_option($conn, array(PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED)); + + // pass an invalid option + set_stmt_option($conn, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => 10)); +} + +// PDO::SQLSRV_ATTR_DIRECT_QUERY as statement attribute +// Expect error +function Test8($conn) +{ + echo "Test8 - Set stmt attr: SQLSRV_ATTR_DIRECT_QUERY \n"; + $attr = "PDO::SQLSRV_ATTR_DIRECT_QUERY"; + $stmt = set_stmt_attr($conn, $attr, true); +} + +try { include("MsSetup.inc"); - $conn = new PDO( "sqlsrv:Server=$server; Database = $databaseName ", $uid, $pwd); - $conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); + $conn = new PDO("sqlsrv:Server=$server; Database = $databaseName ", $uid, $pwd); + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $conn->exec("IF OBJECT_ID('temptb', 'U') IS NOT NULL DROP TABLE temptb"); $conn->exec("CREATE TABLE temptb(id INT NOT NULL PRIMARY KEY, val VARCHAR(10)) "); @@ -136,12 +140,12 @@ try test4($conn); test5($conn); test6($conn); - -} - -catch( PDOException $e ) { - - var_dump( $e ); + test7($conn); + test8($conn); + + $conn->exec("DROP TABLE temptb"); +} catch (PDOException $e) { + var_dump($e); exit; } @@ -181,4 +185,12 @@ bool\(true\) Get Attribute: PDO::SQLSRV_ATTR_QUERY_TIMEOUT -int\(45\) \ No newline at end of file +int\(45\) + +Test7 - Set stmt option: SQLSRV_ATTR_CURSOR_SCROLL_TYPE +SQLSTATE\[IMSSP\]: The PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE attribute may only be set when PDO::ATTR_CURSOR is set to PDO::CURSOR_SCROLL in the $driver_options array of PDO::prepare. + +SQLSTATE\[IMSSP\]: The value passed for the 'Scrollable' statement option is invalid. + +Test8 - Set stmt attr: SQLSRV_ATTR_DIRECT_QUERY +SQLSTATE\[IMSSP\]: The PDO::SQLSRV_ATTR_DIRECT_QUERY attribute may only be set in the $driver_options array of PDO::prepare. \ No newline at end of file diff --git a/test/functional/setup/build_ksp.py b/test/functional/setup/build_ksp.py deleted file mode 100644 index fb524719..00000000 --- a/test/functional/setup/build_ksp.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/python3 -######################################################################################### -# -# Description: This script builds a custom keystore provider and compiles the app that -# uses this KSP. Their names can be passed as arguments, but the outputs -# are always -# - myKSP.dll (myKSPx64.dll) / myKSP.so -# - ksp_app.exe / ksp_app -# -# Requirement: -# python 3.x -# myKSP.c (or any equivalent) -# ksp_app.c (or any equivalent) -# msodbcsql.h (odbc header file) -# -# Execution: Run with command line with optional options -# py build_ksp.py --KSP myKSP --APP ksp_app -# -############################################################################################# - -import sys -import os -import platform -import argparse - -# This creates a batch *filename*, which compiles a C program according to -# *command* and *arch* (either x86 or x64) -def create_batch_file(arch, filename, command): - root_dir = 'C:' + os.sep - vcvarsall = os.path.join(root_dir, "Program Files (x86)", "Microsoft Visual Studio 14.0", "VC", "vcvarsall.bat") - - try: - file = open(filename, 'w') - file.write('@ECHO OFF' + os.linesep) - if arch == 'x64': - file.write('@CALL "' + vcvarsall + '" amd64' + os.linesep) - else: - file.write('@CALL "' + vcvarsall + '" x86' + os.linesep) - - # compile the code - file.write('@CALL ' + command + os.linesep) - file.close() - except: - print('Cannot create ', filename) - -# This invokes the newly created batch file to compile the code, -# according to *arch* (either x86 or x64). The batch file will be -# removed afterwards -def compile_KSP_windows(arch, ksp_src): - output = 'myKSP' - if arch == 'x64': - output = output + arch + '.dll' - else: - output = output + '.dll' - - command = 'cl {0} /LD /MD /link /out:'.format(ksp_src) + output - batchfile = 'build_KSP.bat' - create_batch_file(arch, batchfile, command) - os.system(batchfile) - os.remove(batchfile) - -# This compiles myKSP.c -# -# In Windows, this will create batch files to compile two dll(s). -# Otherwise, this will compile the code and generate a .so file. -# -# Output: A custom keystore provider created -def compile_KSP(ksp_src): - print('Compiling ', ksp_src) - if platform.system() == 'Windows': - compile_KSP_windows('x64', ksp_src) - compile_KSP_windows('x86', ksp_src) - else: - os.system('gcc -fshort-wchar -fPIC -o myKSP.so -shared {0}'.format(ksp_src)) - -# This compiles ksp app, which assumes the existence of the .dll or the .so file. -# -# In Windows, a batch file is created in order to compile the code. -def configure_KSP(app_src): - print('Compiling ', app_src) - if platform.system() == 'Windows': - command = 'cl /MD {0} /link odbc32.lib /out:ksp_app.exe'.format(app_src) - batchfile = 'build_app.bat' - create_batch_file('x86', batchfile, command) - os.system(batchfile) - os.remove(batchfile) - else: - os.system('gcc -o ksp_app -fshort-wchar {0} -lodbc -ldl'.format(app_src)) - -################################### Main Function ################################### -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('-ksp', '--KSPSRC', default='myKSP.c', help='The source file of KSP (keystore provider)') - parser.add_argument('-app', '--APPSRC', default='ksp_app.c', help='The source file for the app that uses the KSP') - args = parser.parse_args() - - ksp_src = args.KSPSRC - app_src = args.APPSRC - header = 'msodbcsql.h' - - cwd = os.getcwd() - - # make sure all required source and header files are present - work_dir = os.path.dirname(os.path.realpath(__file__)) - os.chdir(work_dir) - - if not os.path.exists(os.path.join(work_dir, header)): - print('Error: {0} not found!'.format(header)) - exit(1) - if not os.path.exists(os.path.join(work_dir, ksp_src)): - print('Error: {0}.c not found!'.format(ksp_src)) - exit(1) - if not os.path.exists(os.path.join(work_dir, app_src)): - print('Error: {0}.c not found!'.format(app_src)) - exit(1) - - compile_KSP(ksp_src) - configure_KSP(app_src) - - os.chdir(cwd) - - \ No newline at end of file diff --git a/test/functional/setup/ksp_app.c b/test/functional/setup/ksp_app.c deleted file mode 100644 index 86107802..00000000 --- a/test/functional/setup/ksp_app.c +++ /dev/null @@ -1,305 +0,0 @@ -/****************************************************************************** - Example application for demonstration of custom keystore provider usage - - Windows: compile with cl /MD ksp_app.c /link odbc32.lib /out:ksp_app.exe - Linux/mac: compile with gcc -o ksp_app -fshort-wchar ksp_app.c -lodbc -ldl - - usage: kspapp connstr - - ******************************************************************************/ - -#define KSPNAME L"MyCustomKSPName" -#define PROV_ENCRYPT_KEY "LPKCWVD07N3RG98J0MBLG4H2" /* this can be any character string */ - -#include -#include -#ifdef _WIN32 -#include -#else -#define __stdcall -#include -#endif -#include -#include -#include "msodbcsql.h" - -enum job { - set_up = 0, - clean_up = 1 -}; - -/* Convenience functions */ - -int checkRC(SQLRETURN rc, char *msg, int ret, SQLHANDLE h, SQLSMALLINT ht) { - if (rc == SQL_ERROR) { - fprintf(stderr, "Error occurred upon %s\n", msg); - if (h) { - SQLSMALLINT i = 0; - SQLSMALLINT outlen = 0; - char errmsg[1024]; - while ((rc = SQLGetDiagField( - ht, h, ++i, SQL_DIAG_MESSAGE_TEXT, errmsg, sizeof(errmsg), &outlen)) == SQL_SUCCESS - || rc == SQL_SUCCESS_WITH_INFO) { - fprintf(stderr, "Err#%d: %s\n", i, errmsg); - } - } - if (ret) - exit(ret); - return 0; - } - else if (rc == SQL_SUCCESS_WITH_INFO && h) { - SQLSMALLINT i = 0; - SQLSMALLINT outlen = 0; - char errmsg[1024]; - printf("Success with info for %s:\n", msg); - while ((rc = SQLGetDiagField( - ht, h, ++i, SQL_DIAG_MESSAGE_TEXT, errmsg, sizeof(errmsg), &outlen)) == SQL_SUCCESS - || rc == SQL_SUCCESS_WITH_INFO) { - fprintf(stderr, "Msg#%d: %s\n", i, errmsg); - } - } - return 1; -} - -void postKspError(CEKEYSTORECONTEXT *ctx, const wchar_t *msg, ...) { - if (msg > (wchar_t*)65535) - wprintf(L"Provider emitted message: %s\n", msg); - else - wprintf(L"Provider emitted message ID %d\n", msg); -} - -int setKSPLibrary(SQLHSTMT stmt) { - unsigned char CEK[32]; - unsigned char *ECEK; - unsigned short ECEKlen; - unsigned char foundProv = 0; - int i; -#ifdef _WIN32 - HMODULE hProvLib; -#else - void *hProvLib; -#endif - CEKEYSTORECONTEXT ctx = {0}; - CEKEYSTOREPROVIDER **ppKsp, *pKsp; - int(__stdcall *pEncryptCEK)(CEKEYSTORECONTEXT *, errFunc *, unsigned char *, unsigned short, unsigned char **, unsigned short *); - - /* Load the provider library */ -#ifdef _WIN32 - if (!(hProvLib = LoadLibrary("myKSP.dll"))) { -#else - if (!(hProvLib = dlopen("./myKSP.so", RTLD_NOW))) { -#endif - fprintf(stderr, "Error loading KSP library\n"); - return 2; - } -#ifdef _WIN32 - if (!(ppKsp = (CEKEYSTOREPROVIDER**)GetProcAddress(hProvLib, "CEKeystoreProvider"))) { -#else - if (!(ppKsp = (CEKEYSTOREPROVIDER**)dlsym(hProvLib, "CEKeystoreProvider"))) { -#endif - fprintf(stderr, "The export CEKeystoreProvider was not found in the KSP library\n"); - return 3; - } - while (pKsp = *ppKsp++) { - if (!memcmp(KSPNAME, pKsp->Name, sizeof(KSPNAME))) { - foundProv = 1; - break; - } - } - if (! foundProv) { - fprintf(stderr, "Could not find provider in the library\n"); - return 4; - } - - if (pKsp->Init && !pKsp->Init(&ctx, postKspError)) { - fprintf(stderr, "Could not initialize provider\n"); - return 5; - } -#ifdef _WIN32 - if (!(pEncryptCEK = (LPVOID)GetProcAddress(hProvLib, "KeystoreEncrypt"))) { -#else - if (!(pEncryptCEK = dlsym(hProvLib, "KeystoreEncrypt"))) { -#endif - fprintf(stderr, "The export KeystoreEncrypt was not found in the KSP library\n"); - return 6; - } - if (!pKsp->Write) { - fprintf(stderr, "Provider does not support configuration\n"); - return 7; - } - - /* Configure the provider with the key */ - if (!pKsp->Write(&ctx, postKspError, PROV_ENCRYPT_KEY, strlen(PROV_ENCRYPT_KEY))) { - fprintf(stderr, "Error writing to KSP\n"); - return 8; - } - - /* Generate a CEK and encrypt it with the provider */ - srand(time(0) ^ getpid()); - for (i = 0; i < sizeof(CEK); i++) - CEK[i] = rand(); - - if (!pEncryptCEK(&ctx, postKspError, CEK, sizeof(CEK), &ECEK, &ECEKlen)) { - fprintf(stderr, "Error encrypting CEK\n"); - return 9; - } - - /* Create a CMK definition on the server */ - { - static char cmkSql[] = "CREATE COLUMN MASTER KEY CustomCMK WITH (" - "KEY_STORE_PROVIDER_NAME = 'MyCustomKSPName'," - "KEY_PATH = 'TheOneAndOnlyKey')"; - printf("Create CMK: %s\n", cmkSql); - SQLExecDirect(stmt, cmkSql, SQL_NTS); - } - - /* Create a CEK definition on the server */ - { - const char cekSqlBefore[] = "CREATE COLUMN ENCRYPTION KEY CustomCEK WITH VALUES (" - "COLUMN_MASTER_KEY = CustomCMK," - "ALGORITHM = 'none'," - "ENCRYPTED_VALUE = 0x"; - char *cekSql = malloc(sizeof(cekSqlBefore) + 2 * ECEKlen + 2); /* 1 for ')', 1 for null terminator */ - strcpy(cekSql, cekSqlBefore); - for (i = 0; i < ECEKlen; i++) - sprintf(cekSql + sizeof(cekSqlBefore) - 1 + 2 * i, "%02x", ECEK[i]); - strcat(cekSql, ")"); - printf("Create CEK: %s\n", cekSql); - SQLExecDirect(stmt, cekSql, SQL_NTS); - free(cekSql); -#ifdef _WIN32 - LocalFree(ECEK); -#else - free(ECEK); -#endif - } - -#ifdef _WIN32 - FreeLibrary(hProvLib); -#else - dlclose(hProvLib); -#endif - - return 0; -} - -void populateTestTable(SQLHDBC dbc, SQLHSTMT stmt) -{ - SQLRETURN rc; - int i, j; - - /* Create a table with encrypted columns */ - { - static char *tableSql = "CREATE TABLE CustomKSPTestTable (" - "c1 int," - "c2 varchar(255) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = CustomCEK, ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256')," - "c3 char(5) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = CustomCEK, ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256')," - "c4 date ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = CustomCEK, ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256'))"; - printf("Create table: %s\n", tableSql); - SQLExecDirect(stmt, tableSql, SQL_NTS); - } - - /* Load provider into the ODBC Driver and configure it */ - { - unsigned char ksd[sizeof(CEKEYSTOREDATA) + sizeof(PROV_ENCRYPT_KEY) - 1]; - CEKEYSTOREDATA *pKsd = (CEKEYSTOREDATA*)ksd; - pKsd->name = L"MyCustomKSPName"; - pKsd->dataSize = sizeof(PROV_ENCRYPT_KEY) - 1; - memcpy(pKsd->data, PROV_ENCRYPT_KEY, sizeof(PROV_ENCRYPT_KEY) - 1); -#ifdef _WIN32 - rc = SQLSetConnectAttr(dbc, SQL_COPT_SS_CEKEYSTOREPROVIDER, "myKSP.dll", SQL_NTS); -#else - rc = SQLSetConnectAttr(dbc, SQL_COPT_SS_CEKEYSTOREPROVIDER, "./myKSP.so", SQL_NTS); -#endif - checkRC(rc, "Loading KSP into ODBC Driver", 7, dbc, SQL_HANDLE_DBC); - rc = SQLSetConnectAttr(dbc, SQL_COPT_SS_CEKEYSTOREDATA, (SQLPOINTER)pKsd, SQL_IS_POINTER); - checkRC(rc, "Configuring the KSP", 7, dbc, SQL_HANDLE_DBC); - } - - /* Insert some data */ - { - int c1; - char c2[256]; - char c3[6]; - SQL_DATE_STRUCT date; - SQLLEN cbdate; - rc = SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &c1, 0, 0); - checkRC(rc, "Binding parameters for insert", 9, stmt, SQL_HANDLE_STMT); - rc = SQLBindParameter(stmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 255, 0, c2, 255, 0); - checkRC(rc, "Binding parameters for insert", 9, stmt, SQL_HANDLE_STMT); - rc = SQLBindParameter(stmt, 3, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, 5, 0, c3, 5, 0); - checkRC(rc, "Binding parameters for insert", 9, stmt, SQL_HANDLE_STMT); - checkRC(rc, "Binding parameters for insert", 9, stmt, SQL_HANDLE_STMT); - cbdate = sizeof(SQL_DATE_STRUCT); - rc = SQLBindParameter(stmt, 4, SQL_PARAM_INPUT, SQL_C_TYPE_DATE, SQL_TYPE_DATE, 10, 0, &date, 0, &cbdate); - checkRC(rc, "Binding parameters for insert", 9, stmt, SQL_HANDLE_STMT); - - date.year = 2017; - date.month = 8; - for (i = 0; i < 10; i++) { - date.day = i + 10; - - c1 = i * 10 + i + 1; - sprintf(c2, "Sample data %d for column 2", i); - for (j = 0; j < 3; j++) { - c3[j] = 'a' + i + j; - } - c3[3] = '\0'; - rc = SQLExecDirect(stmt, "INSERT INTO CustomKSPTestTable (c1, c2, c3, c4) values (?, ?, ?, ?)", SQL_NTS); - checkRC(rc, "Inserting rows query", 10, stmt, SQL_HANDLE_STMT); - } - printf("(Encrypted) data has been inserted into CustomKSPTestTable. You may inspect the data now.\n"); - } - -} - -int main(int argc, char **argv) { - char sqlbuf[1024]; - SQLHENV env; - SQLHDBC dbc; - SQLHSTMT stmt; - SQLRETURN rc; - int i; - char connStr[1024]; - enum job task; - - if (argc < 6) { - fprintf(stderr, "usage: kspapp job server database uid pwd\n"); - return 1; - } - - task = atoi(argv[1]); - - sprintf(connStr, "DRIVER={ODBC Driver 17 for SQL Server};SERVER=%s;ColumnEncryption=Enabled;DATABASE=%s;UID=%s;PWD=%s", argv[2], argv[3], argv[4], argv[5]); - - /* Connect to Server */ - rc = SQLAllocHandle(SQL_HANDLE_ENV, NULL, &env); - checkRC(rc, "allocating environment handle", 2, 0, 0); - rc = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0); - checkRC(rc, "setting ODBC version to 3.0", 3, env, SQL_HANDLE_ENV); - rc = SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc); - checkRC(rc, "allocating connection handle", 4, env, SQL_HANDLE_ENV); - rc = SQLDriverConnect(dbc, 0, connStr, strlen(connStr), NULL, 0, NULL, SQL_DRIVER_NOPROMPT); - checkRC(rc, "connecting to data source", 5, dbc, SQL_HANDLE_DBC); - rc = SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt); - checkRC(rc, "allocating statement handle", 6, dbc, SQL_HANDLE_DBC); - - if (task == set_up) { - printf("Setting up KSP...\n"); - setKSPLibrary(stmt); - populateTestTable(dbc, stmt); - } - else if (task == clean_up) { - printf("Cleaning up KSP...\n"); - - SQLExecDirect(stmt, "DROP TABLE CustomKSPTestTable", SQL_NTS); - SQLExecDirect(stmt, "DROP COLUMN ENCRYPTION KEY CustomCEK", SQL_NTS); - SQLExecDirect(stmt, "DROP COLUMN MASTER KEY CustomCMK", SQL_NTS); - printf("Removed table, CEK, and CMK\n"); - } - - SQLDisconnect(dbc); - SQLFreeHandle(SQL_HANDLE_DBC, dbc); - SQLFreeHandle(SQL_HANDLE_ENV, env); - return 0; -} diff --git a/test/functional/setup/myKSP.c b/test/functional/setup/myKSP.c deleted file mode 100644 index d6188842..00000000 --- a/test/functional/setup/myKSP.c +++ /dev/null @@ -1,132 +0,0 @@ -/****************************************************************************** -Custom Keystore Provider Example - -Windows: compile with cl myKSP.c /LD /MD /link /out:myKSP.dll -Linux/mac: compile with gcc -fshort-wchar -fPIC -o myKSP.so -shared myKSP.c - -******************************************************************************/ - -#ifdef _WIN32 -#include -#else -#define __stdcall -#endif - -#define DEBUG 0 - -#include -#include -#include -#include -#include -#include "msodbcsql.h" - -int wcscmp_short(wchar_t *s1, wchar_t *s2) { - while(*s1 && *s2 && *s1 == *s2) - s1++, s2++; - return *s1 - *s2; -} - -int __stdcall KeystoreInit(CEKEYSTORECONTEXT *ctx, errFunc *onError) { - if (DEBUG) - printf("KSP Init() function called\n"); - return 1; -} - -static unsigned char *g_encryptKey; -static unsigned int g_encryptKeyLen; - -int __stdcall KeystoreWrite(CEKEYSTORECONTEXT *ctx, errFunc *onError, void *data, unsigned int len) { - if (DEBUG) - printf("KSP Write() function called (%d bytes)\n", len); - if (len) { - if (g_encryptKey) - free(g_encryptKey); - g_encryptKey = malloc(len); - if (!g_encryptKey) { - onError(ctx, L"Memory Allocation Error"); - return 0; - } - memcpy(g_encryptKey, data, len); - g_encryptKeyLen = len; - } - return 1; -} - -// Very simple "encryption" scheme - rotating XOR with the key -int __stdcall KeystoreDecrypt(CEKEYSTORECONTEXT *ctx, errFunc *onError, const wchar_t *keyPath, const wchar_t *alg, unsigned char *ecek, unsigned short ecekLen, unsigned char **cekOut, unsigned short *cekLen) { - unsigned int i; - if (DEBUG) - printf("KSP Decrypt() function called (keypath=%S alg=%S ecekLen=%u)\n", keyPath, alg, ecekLen); - if (wcscmp_short(keyPath, L"TheOneAndOnlyKey")) { - onError(ctx, L"Invalid key path"); - return 0; - } - if (wcscmp_short(alg, L"none")) { - onError(ctx, L"Invalid algorithm"); - return 0; - } - if (!g_encryptKey) { - onError(ctx, L"Keystore provider not initialized with key"); - return 0; - } -#ifndef _WIN32 - *cekOut = malloc(ecekLen); -#else - *cekOut = LocalAlloc(LMEM_FIXED, ecekLen); -#endif - if (!*cekOut) { - onError(ctx, L"Memory Allocation Error"); - return 0; - } - *cekLen = ecekLen; - for (i = 0; i < ecekLen; i++) - (*cekOut)[i] = ecek[i] ^ g_encryptKey[i % g_encryptKeyLen]; - return 1; -} - -// Note that in the provider interface, this function would be referenced via the CEKEYSTOREPROVIDER -// structure. However, that does not preclude keystore providers from exporting their own functions, -// as illustrated by this example where the encryption is performed via a separate function (with a -// different prototype than the one in the KSP interface.) -#ifdef _WIN32 -__declspec(dllexport) -#endif -int KeystoreEncrypt(CEKEYSTORECONTEXT *ctx, errFunc *onError, - unsigned char *cek, unsigned short cekLen, - unsigned char **ecekOut, unsigned short *ecekLen) { - unsigned int i; - - if (DEBUG) - printf("KSP Encrypt() function called (cekLen=%u)\n", cekLen); - if (!g_encryptKey) { - onError(ctx, L"Keystore provider not initialized with key"); - return 0; - } - *ecekOut = malloc(cekLen); - if (!*ecekOut) { - onError(ctx, L"Memory Allocation Error"); - return 0; - } - *ecekLen = cekLen; - for (i = 0; i < cekLen; i++) - (*ecekOut)[i] = cek[i] ^ g_encryptKey[i % g_encryptKeyLen]; - return 1; -} - -CEKEYSTOREPROVIDER MyCustomKSPName_desc = { - L"MyCustomKSPName", - KeystoreInit, - 0, - KeystoreWrite, - KeystoreDecrypt, - 0 -}; - -#ifdef _WIN32 -__declspec(dllexport) -#endif -CEKEYSTOREPROVIDER *CEKeystoreProvider[] = { - &MyCustomKSPName_desc, - 0 -}; \ No newline at end of file diff --git a/test/functional/setup/run_ksp.py b/test/functional/setup/run_ksp.py deleted file mode 100644 index 0d0ff897..00000000 --- a/test/functional/setup/run_ksp.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/python3 -######################################################################################### -# -# Description: This script assumes the existence of the ksp_app executable and will -# invoke it to create / remove the Column Master Key, the Column Encryption key, -# and the table [CustomKSPTestTable] in the test database. -# -# Requirement: -# python 3.x -# ksp_app executable -# -# Execution: Run with command line with required options -# py run_ksp.py --SERVER=server --DBNAME=database --UID=uid --PWD=pwd -# py run_ksp.py --SERVER=server --DBNAME=database --UID=uid --PWD=pwd --REMOVE -# -############################################################################################# - -import sys -import os -import platform -import argparse - -################################### Main Function ################################### -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('-server', '--SERVER', required=True, help='SQL Server') - parser.add_argument('-dbname', '--DBNAME', required=True, help='Name of an existing database') - parser.add_argument('-uid', '--UID', required=True, help='User name') - parser.add_argument('-pwd', '--PWD', required=True, help='User password') - parser.add_argument('-remove', '--REMOVE', action='store_true', help='Clean up KSP related data, false by default') - - args = parser.parse_args() - - app_name = 'ksp_app' - cwd = os.getcwd() - - # first check if the ksp app is present - work_dir = os.path.dirname(os.path.realpath(__file__)) - os.chdir(work_dir) - - if platform.system() == 'Windows': - path = os.path.join(work_dir, app_name + '.exe') - executable = app_name - else: - path = os.path.join(work_dir, app_name) - executable = './' + app_name - - if not os.path.exists(path): - print('Error: {0} not found!'.format(path)) - exit(1) - - if args.REMOVE: - os.system('{0} 1 {1} {2} {3} {4}'.format(executable, args.SERVER, args.DBNAME, args.UID, args.PWD)) - else: - os.system('{0} 0 {1} {2} {3} {4}'.format(executable, args.SERVER, args.DBNAME, args.UID, args.PWD)) - - os.chdir(cwd) diff --git a/test/functional/sqlsrv/0075.phpt b/test/functional/sqlsrv/0075.phpt index 327c2260..1657af0f 100644 --- a/test/functional/sqlsrv/0075.phpt +++ b/test/functional/sqlsrv/0075.phpt @@ -1,5 +1,7 @@ --TEST-- Fix for output string parameter truncation error +--DESCRIPTION-- +This test includes calling sqlsrv_query with an array of parameters with a named key, which should result in an error. --SKIPIF-- --FILE-- @@ -23,10 +25,25 @@ if ($s === false) { $inValue1 = "Some data"; $outValue1 = ""; +$tsql = '{CALL [test_output] (?, ?)}'; + $s = sqlsrv_query( $conn, - "{CALL [test_output] (?, ?)}", - array(array($inValue1, SQLSRV_PARAM_IN, null, SQLSRV_SQLTYPE_VARCHAR(512)), + $tsql, + array("k1" => array($inValue1, SQLSRV_PARAM_IN, null, SQLSRV_SQLTYPE_VARCHAR(512)), + array(&$outValue1, SQLSRV_PARAM_OUT, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR), SQLSRV_SQLTYPE_VARCHAR(512))) +); + +if ($s !== false) { + echo "Expect this to fail!\n"; +} else { + print_r(sqlsrv_errors()); +} + +$s = sqlsrv_query( + $conn, + $tsql, + array(array($inValue1, SQLSRV_PARAM_IN, null, SQLSRV_SQLTYPE_VARCHAR(512)), array(&$outValue1, SQLSRV_PARAM_OUT, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR), SQLSRV_SQLTYPE_VARCHAR(512))) ); @@ -45,5 +62,18 @@ sqlsrv_close($conn); ?> --EXPECT-- +Array +( + [0] => Array + ( + [0] => IMSSP + [SQLSTATE] => IMSSP + [1] => -57 + [code] => -57 + [2] => String keys are not allowed in parameters arrays. + [message] => String keys are not allowed in parameters arrays. + ) + +) 512 Some data diff --git a/test/functional/sqlsrv/MsCommon.inc b/test/functional/sqlsrv/MsCommon.inc index eb4933b7..dec27c55 100644 --- a/test/functional/sqlsrv/MsCommon.inc +++ b/test/functional/sqlsrv/MsCommon.inc @@ -84,6 +84,26 @@ function isDaasMode() return ($daasMode ? true : false); } +function isSQLAzure() +{ + // 'SQL Azure' indicates SQL Database or SQL Data Warehouse + // For details, https://docs.microsoft.com/sql/t-sql/functions/serverproperty-transact-sql + $conn = connect(); + $tsql = "SELECT SERVERPROPERTY ('edition')"; + $stmt = sqlsrv_query($conn, $tsql); + + if (sqlsrv_fetch($stmt)) { + $edition = sqlsrv_get_field($stmt, 0); + if ($edition === "SQL Azure") { + return true; + } else { + return false; + } + } else { + die("Could not fetch server property."); + } +} + function isAzureDW() { // Check if running Azure Data Warehouse diff --git a/test/functional/sqlsrv/TC81_MemoryCheck.phpt b/test/functional/sqlsrv/TC81_MemoryCheck.phpt index 5845da21..40f84a6a 100644 --- a/test/functional/sqlsrv/TC81_MemoryCheck.phpt +++ b/test/functional/sqlsrv/TC81_MemoryCheck.phpt @@ -7,11 +7,15 @@ emalloc (which only allocate memory in the memory space allocated for the PHP pr PHPT_EXEC=true --SKIPIF-- - + --FILE-- getMessage(); } diff --git a/test/functional/sqlsrv/sqlsrv_378_out_param_error.phpt b/test/functional/sqlsrv/sqlsrv_378_out_param_error.phpt index ebb7334f..a0169225 100644 --- a/test/functional/sqlsrv/sqlsrv_378_out_param_error.phpt +++ b/test/functional/sqlsrv/sqlsrv_378_out_param_error.phpt @@ -2,11 +2,14 @@ This test verifies that GitHub issue #378 is fixed. --DESCRIPTION-- GitHub issue #378 - output parameters appends garbage info when variable is initialized with different data type -steps to reproduce the issue: +Steps to reproduce the issue: 1- create a store procedure with print and output parameter 2- initialize output parameters to a different data type other than the type declared in sp. 3- set the WarningsReturnAsErrors to true 4- call sp. +Also check error conditions by passing output parameters NOT by reference. +--ENV-- +PHPT_EXEC=true --SKIPIF-- --FILE-- @@ -19,11 +22,8 @@ $conn = AE\connect(); $procName = 'test_378'; createSP($conn, $procName); -sqlsrv_configure('WarningsReturnAsErrors', true); -executeSP($conn, $procName); - -sqlsrv_configure('WarningsReturnAsErrors', false); -executeSP($conn, $procName); +runTests($conn, $procName, true); +runTests($conn, $procName, false); dropProc($conn, $procName); echo "Done\n"; @@ -46,7 +46,34 @@ function createSP($conn, $procName) } } -function executeSP($conn, $procName) +//-------------------functions------------------- +function runTests($conn, $procName, $warningAsErrors) +{ + sqlsrv_configure('WarningsReturnAsErrors', $warningAsErrors); + + trace("\nWarningsReturnAsErrors: $warningAsErrors\n"); + + executeSP($conn, $procName, true, false); + executeSP($conn, $procName, true, true); + executeSP($conn, $procName, false, false); + executeSP($conn, $procName, false, true); +} + +function compareErrors() +{ + $message = 'Variable parameter 3 not passed by reference (prefaced with an &). Output or bidirectional variable parameters (SQLSRV_PARAM_OUT and SQLSRV_PARAM_INOUT) passed to sqlsrv_prepare or sqlsrv_query should be passed by reference, not by value.'; + + $error = sqlsrv_errors()[0]['message']; + + if ($error !== $message) { + print_r(sqlsrv_errors(), true); + return; + } + + trace("Comparing errors: matched!\n"); +} + +function executeSP($conn, $procName, $noRef, $prepare) { $expected = 3; $v1 = 1; @@ -54,14 +81,44 @@ function executeSP($conn, $procName) $v3 = 'str'; $res = true; - if (AE\isColEncrypted()) { - $stmt = sqlsrv_prepare($conn, "{call $procName( ?, ?, ?)}", array($v1, $v2, array(&$v3, SQLSRV_PARAM_OUT))); + $tsql = "{call $procName( ?, ?, ?)}"; + + if ($noRef) { + $params = array($v1, $v2, array($v3, SQLSRV_PARAM_OUT)); + } else { + $params = array($v1, $v2, array(&$v3, SQLSRV_PARAM_OUT)); + } + + trace("No reference: $noRef\n"); + trace("Use prepared stmt: $prepare\n"); + + if (AE\isColEncrypted() || $prepare) { + $stmt = sqlsrv_prepare($conn, $tsql, $params); if ($stmt) { $res = sqlsrv_execute($stmt); + } else { + fatalError("executeSP: failed in preparing statement with reference($noRef)"); } + if ($noRef) { + if ($res !== false) { + echo "Expect this to fail!\n"; + } + compareErrors(); + return; + } } else { - $stmt = sqlsrv_query($conn, "{call $procName( ?, ?, ?)}", array($v1, $v2, array(&$v3, SQLSRV_PARAM_OUT))); + $stmt = sqlsrv_query($conn, $tsql, $params); + if ($noRef) { + if ($stmt !== false) { + echo "Expect this to fail!\n"; + } + compareErrors(); + return; + } } + + trace("No errors: $v3 and $expected\n"); + // No errors expected if ($stmt === false || !$res) { print_r(sqlsrv_errors(), true); } diff --git a/test/functional/sqlsrv/sqlsrv_ae_insert_datetime.phpt b/test/functional/sqlsrv/sqlsrv_ae_insert_datetime.phpt index 6b85cf8f..0c1c2828 100644 --- a/test/functional/sqlsrv/sqlsrv_ae_insert_datetime.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_insert_datetime.phpt @@ -29,18 +29,20 @@ foreach ($dataTypes as $dataType) { is_incompatible_types_error($dataType, "default type"); } else { echo "****Encrypted default type is compatible with encrypted $dataType****\n"; - if ($dataType != "time") { - AE\fetchAll($conn, $tbname); - } else { - $sql = "SELECT * FROM $tbname"; - $stmt = sqlsrv_query($conn, $sql); - $row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC); - foreach ($row as $key => $value) { - //var_dump( $row ); + $sql = "SELECT * FROM $tbname"; + $stmt = sqlsrv_query($conn, $sql); + $row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC); + foreach ($row as $key => $value) { + if ($dataType == "time") { $t = $value->format('H:i:s'); print "$key: $t\n"; + } else { + $t = date_format($value, "Y-m-d H:i:s.u"); + $tz = $value->getTimezone()->getName(); + print "$key: $t $tz\n"; } } + } dropTable($conn, $tbname); } @@ -51,47 +53,23 @@ sqlsrv_close($conn); Testing date: ****Encrypted default type is compatible with encrypted date**** -c_det: - date: 0001-01-01 00:00:00.000000 - timezone_type: 3 - timezone: Canada/Pacific -c_rand: - date: 9999-12-31 00:00:00.000000 - timezone_type: 3 - timezone: Canada/Pacific +c_det: 0001-01-01 00:00:00.000000 Canada/Pacific +c_rand: 9999-12-31 00:00:00.000000 Canada/Pacific Testing datetime: ****Encrypted default type is compatible with encrypted datetime**** -c_det: - date: 1753-01-01 00:00:00.000000 - timezone_type: 3 - timezone: Canada/Pacific -c_rand: - date: 9999-12-31 23:59:59.997000 - timezone_type: 3 - timezone: Canada/Pacific +c_det: 1753-01-01 00:00:00.000000 Canada/Pacific +c_rand: 9999-12-31 23:59:59.997000 Canada/Pacific Testing datetime2: ****Encrypted default type is compatible with encrypted datetime2**** -c_det: - date: 0001-01-01 00:00:00.000000 - timezone_type: 3 - timezone: Canada/Pacific -c_rand: - date: 9999-12-31 23:59:59.123456 - timezone_type: 3 - timezone: Canada/Pacific +c_det: 0001-01-01 00:00:00.000000 Canada/Pacific +c_rand: 9999-12-31 23:59:59.123456 Canada/Pacific Testing smalldatetime: ****Encrypted default type is compatible with encrypted smalldatetime**** -c_det: - date: 1900-01-01 00:00:00.000000 - timezone_type: 3 - timezone: Canada/Pacific -c_rand: - date: 2079-06-05 23:59:00.000000 - timezone_type: 3 - timezone: Canada/Pacific +c_det: 1900-01-01 00:00:00.000000 Canada/Pacific +c_rand: 2079-06-05 23:59:00.000000 Canada/Pacific Testing time: ****Encrypted default type is compatible with encrypted time**** @@ -100,11 +78,5 @@ c_rand: 23:59:59 Testing datetimeoffset: ****Encrypted default type is compatible with encrypted datetimeoffset**** -c_det: - date: 0001-01-01 00:00:00.000000 - timezone_type: 1 - timezone: -14:00 -c_rand: - date: 9999-12-31 23:59:59.123456 - timezone_type: 1 - timezone: +14:00 +c_det: 0001-01-01 00:00:00.000000 -14:00 +c_rand: 9999-12-31 23:59:59.123456 +14:00 diff --git a/test/functional/sqlsrv/sqlsrv_ae_insert_retrieve.phpt b/test/functional/sqlsrv/sqlsrv_ae_insert_retrieve.phpt index e88ef0df..36df5670 100644 --- a/test/functional/sqlsrv/sqlsrv_ae_insert_retrieve.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_insert_retrieve.phpt @@ -34,10 +34,13 @@ foreach ($decrypted_row as $key => $value) { if (!is_object($value)) { print "$key: $value\n"; } else { - print "$key:\n"; - foreach ($value as $dateKey => $dateValue) { - print " $dateKey: $dateValue\n"; - } + // print "$key:\n"; + // foreach ($value as $dateKey => $dateValue) { + // print " $dateKey: $dateValue\n"; + // } + $t = date_format($value, "Y-m-d H:i:s.u"); + $tz = $value->getTimezone()->getName(); + print "$key: $t $tz\n"; } } sqlsrv_free_stmt($stmt); @@ -70,10 +73,7 @@ Retrieving plaintext data: SSN: 795-73-9838 FirstName: Catherine LastName: Abel -BirthDate: - date: 1996-10-19 00:00:00.000000 - timezone_type: 3 - timezone: Canada/Pacific +BirthDate: 1996-10-19 00:00:00.000000 Canada/Pacific Checking ciphertext data: Done diff --git a/test/functional/sqlsrv/sqlsrv_ae_insert_retrieve_fixed_size.phpt b/test/functional/sqlsrv/sqlsrv_ae_insert_retrieve_fixed_size.phpt index d8c272ff..dac610fb 100644 --- a/test/functional/sqlsrv/sqlsrv_ae_insert_retrieve_fixed_size.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_insert_retrieve_fixed_size.phpt @@ -38,7 +38,20 @@ if ($r === false) { } print "Decrypted values:\n"; -AE\fetchAll($conn, $tbname); + +$stmt = selectFromTable($conn, $tbname); +while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) { + foreach ($row as $key => $value) { + if (is_object($value)) { + // datetime objects + $t = date_format($value,"Y-m-d H:i:s.u"); + $tz = $value->getTimezone()->getName(); + print("$key: $t $tz\n"); + } else { + print("$key: $value\n"); + } + } +} sqlsrv_free_stmt($stmt); @@ -75,12 +88,6 @@ IntData: 2147483647 BigIntData: 92233720368547 DecimalData: 79228162514264 BitData: 1 -DateTimeData: - date: 9999-12-31 23:59:59.997000 - timezone_type: 3 - timezone: Canada/Pacific -DateTime2Data: - date: 9999-12-31 23:59:59.123456 - timezone_type: 3 - timezone: Canada/Pacific +DateTimeData: 9999-12-31 23:59:59.997000 Canada/Pacific +DateTime2Data: 9999-12-31 23:59:59.123456 Canada/Pacific Done diff --git a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_datetime.phpt b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_datetime.phpt index 7eebb400..67c601b8 100755 --- a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_datetime.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_datetime.phpt @@ -59,7 +59,8 @@ foreach ($dataTypes as $dataType) { } } // 22018 is the SQLSTATE for any incompatible conversion errors - if ($isCompatible && sqlsrv_errors()[0]['SQLSTATE'] == 22018) { + $errors = sqlsrv_errors(); + if (!empty($errors) && $isCompatible && $errors[0]['SQLSTATE'] == 22018) { echo "$sqlType should be compatible with $dataType\n"; $success = false; } diff --git a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_numeric.phpt b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_numeric.phpt index 25d7d15f..ea5285d8 100755 --- a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_numeric.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_numeric.phpt @@ -69,7 +69,8 @@ foreach ($dataTypes as $dataType) { } } // 22018 is the SQLSTATE for any incompatible conversion errors - if ($isCompatible && sqlsrv_errors()[0]['SQLSTATE'] == 22018) { + $errors = sqlsrv_errors(); + if (!empty($errors) && $isCompatible && $errors[0]['SQLSTATE'] == 22018) { echo "$sqlType should be compatible with $dataType\n"; $success = false; } diff --git a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_string.phpt b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_string.phpt index 77d53147..8bbe9606 100755 --- a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_string.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_string.phpt @@ -57,7 +57,8 @@ foreach ($dataTypes as $dataType) { } } // 22018 is the SQLSTATE for any incompatible conversion errors - if ($isCompatible && sqlsrv_errors()[0]['SQLSTATE'] == 22018) { + $errors = sqlsrv_errors(); + if (!empty($errors) && $isCompatible && $errors[0]['SQLSTATE'] == 22018) { echo "$sqlType should be compatible with $dataType\n"; $success = false; } diff --git a/test/functional/sqlsrv/sqlsrv_azure_ad_access_token.phpt b/test/functional/sqlsrv/sqlsrv_azure_ad_access_token.phpt index 96c82d63..fcbc4080 100644 --- a/test/functional/sqlsrv/sqlsrv_azure_ad_access_token.phpt +++ b/test/functional/sqlsrv/sqlsrv_azure_ad_access_token.phpt @@ -104,6 +104,27 @@ function simpleTest($conn) dropTable($conn, $tableName); } +function connectAzureDB($accToken, $showException) +{ + global $adServer, $adDatabase, $maxAttempts; + + $conn = false; + $connectionInfo = array("Database"=>$adDatabase, "AccessToken"=>$accToken); + + $conn = sqlsrv_connect($adServer, $connectionInfo); + if ($conn === false) { + if ($showException) { + fatalError("Could not connect with Azure AD AccessToken after $maxAttempts retries.\n"); + } + } else { + simpleTest($conn); + + sqlsrv_close($conn); + } + + return $conn; +} + // First test some error conditions connectWithInvalidOptions($server); @@ -112,17 +133,18 @@ connectWithEmptyAccessToken($server); // Next, test with a valid access token and perform some simple tasks require_once('access_token.inc'); -if ($adServer != 'TARGET_AD_SERVER' && $accToken != 'TARGET_ACCESS_TOKEN') { - $connectionInfo = array("Database"=>$adDatabase, "AccessToken"=>$accToken); +$maxAttempts = 3; - $conn = sqlsrv_connect($adServer, $connectionInfo); - if ($conn === false) { - fatalError("Could not connect with Azure AD AccessToken.\n"); - } else { - simpleTest($conn); - - sqlsrv_close($conn); - } +if ($adServer != 'TARGET_AD_SERVER' && $accToken != 'TARGET_ACCESS_TOKEN') { + $conn = false; + $numAttempts = 0; + do { + $conn = connectAzureDB($accToken, ($numAttempts == ($maxAttempts - 1))); + if ($conn === false) { + $numAttempts++; + sleep(10); + } + } while ($conn === false && $numAttempts < $maxAttempts); } echo "Done\n"; diff --git a/test/functional/sqlsrv/sqlsrv_azure_ad_authentication.phpt b/test/functional/sqlsrv/sqlsrv_azure_ad_authentication.phpt index 51f61b27..f02a6f62 100644 --- a/test/functional/sqlsrv/sqlsrv_azure_ad_authentication.phpt +++ b/test/functional/sqlsrv/sqlsrv_azure_ad_authentication.phpt @@ -54,23 +54,44 @@ if ($conn === false) { // Test Azure AD on an Azure database instance. Replace $azureServer, etc with // your credentials to test, or this part is skipped. // -$azureServer = $adServer; -$azureDatabase = $adDatabase; -$azureUsername = $adUser; -$azurePassword = $adPassword; - -if ($azureServer != 'TARGET_AD_SERVER') { - $connectionInfo = array( "UID"=>$azureUsername, "PWD"=>$azurePassword, - "Authentication"=>'ActiveDirectoryPassword', "TrustServerCertificate"=>false ); - - $conn = sqlsrv_connect($azureServer, $connectionInfo); +function connectAzureDB($showException) +{ + global $adServer, $adUser, $adPassword, $maxAttempts; + + $connectionInfo = array("UID"=>$adUser, + "PWD"=>$adPassword, + "Authentication"=>'ActiveDirectoryPassword', + "TrustServerCertificate"=>false ); + + $conn = false; + $conn = sqlsrv_connect($adServer, $connectionInfo); if ($conn === false) { - echo "Could not connect with ActiveDirectoryPassword.\n"; - print_r(sqlsrv_errors()); + if ($showException) { + echo "Could not connect with ActiveDirectoryPassword after $maxAttempts retries.\n"; + print_r(sqlsrv_errors()); + } } else { echo "Connected successfully with Authentication=ActiveDirectoryPassword.\n"; sqlsrv_close($conn); } + + return $conn; +} + +$azureServer = $adServer; +$maxAttempts = 3; + +if ($azureServer != 'TARGET_AD_SERVER') { + $conn = false; + $numAttempts = 0; + do { + $conn = connectAzureDB($numAttempts == ($maxAttempts - 1)); + if ($conn === false) { + $numAttempts++; + sleep(10); + } + } while ($conn === false && $numAttempts < $maxAttempts); + } else { echo "Not testing with Authentication=ActiveDirectoryPassword.\n"; } diff --git a/test/functional/sqlsrv/sqlsrv_batch_query.phpt b/test/functional/sqlsrv/sqlsrv_batch_query.phpt new file mode 100644 index 00000000..4cfa1445 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_batch_query.phpt @@ -0,0 +1,199 @@ +--TEST-- +Test a batch query with different cursor types +--DESCRIPTION-- +Verifies that batch queries don't work with dynamic, static, and keyset +server-side cursors, and checks that correct column and row counts are +returned otherwise. For information on the expected behaviour of cursors +with batch queries, see +https://docs.microsoft.com/en-us/previous-versions/visualstudio/aa266531(v=vs.60) +--SKIPIF-- + +--FILE-- +$cursor)); + if (!$stmt) { + fatalError("Error preparing statement with $cursor cursor\n"); + } + + if (!sqlsrv_execute($stmt)) { + if ($cursor == 'forward' or $cursor == 'buffered') { + fatalError("Statement execution failed unexpectedly with a $cursor cursor\n"); + } else { + checkErrors($noCursor); + continue; + } + } + + $numResultSets = 0; + + // Check the column and row count before and after running through + // each result set, because some cursor types may return the number + // of rows only after fetching all rows in the result set + do { + checkColumnsAndRows($stmt, $cursor, $wrongCursor); + + $row = 0; + while ($res = sqlsrv_fetch_array($stmt)) { + if ($res[0] != $data[$numResultSets][$row]) { + fatalError("Wrong result, expected ".$data[$numResultSets][$row].", got $res[0]\n"); + } + ++$row; + } + + checkColumnsAndRows($stmt, $cursor, $wrongCursor); + ++$numResultSets; + + } while ($next = sqlsrv_next_result($stmt)); + + if ($numResultSets != $expectedResultSets) { + fatalError("Unexpected number of result sets, expected $expectedResultedSets, got $numResultSets\n"); + } + + // We expect an error if sqlsrv_next_result returns false, + // but not if it returns null (i.e. if we are genuinely at + // the end of all the result sets with a buffered cursor) + if ($next === false) { + if ($cursor == 'forward') { + checkErrors($noNextResult); + } else { + fatalError("sqlsrv_next_result failed with a $cursor cursor\n"); + } + } + + sqlsrv_free_stmt($stmt); +} + +dropTable($conn, $tableName); +sqlsrv_close($conn); + +echo "Done.\n"; +?> +--EXPECT-- +Testing with forward cursor... +Testing with dynamic cursor... +Testing with static cursor... +Testing with keyset cursor... +Testing with buffered cursor... +Done. diff --git a/test/functional/sqlsrv/sqlsrv_connStr.phpt b/test/functional/sqlsrv/sqlsrv_connStr.phpt index 144ceb94..7c92bd99 100644 --- a/test/functional/sqlsrv/sqlsrv_connStr.phpt +++ b/test/functional/sqlsrv/sqlsrv_connStr.phpt @@ -1,11 +1,18 @@ --TEST-- UTF-8 connection strings --SKIPIF-- - + --FILE-- 'gibberish' )); if ($c !== false) { fatalError("Should have errored on an invalid encoding."); } -print_r(sqlsrv_errors()); +checkErrors($gibberishEncoding); $c = connect(array( 'CharacterSet' => SQLSRV_ENC_BINARY )); if ($c !== false) { fatalError("Should have errored on an invalid encoding."); } -print_r(sqlsrv_errors()); +checkErrors($binaryEncoding); $c = connect(array( 'CharacterSet' => SQLSRV_ENC_CHAR )); if ($c === false) { @@ -50,7 +68,7 @@ $c = sqlsrv_connect($server_invalid, array( 'Database' => 'test', 'CharacterSet' if ($c !== false) { fatalError("sqlsrv_connect(1) should have failed"); } -print_r(sqlsrv_errors()); +checkErrors($utf16Error); // APP has a UTF-8 name $c = connect(array( @@ -67,7 +85,7 @@ $c = connect(array( if ($c !== false) { fatalError("sqlsrv_connect(3) should have failed"); } -print_r(sqlsrv_errors()); +checkErrors($userLoginFailed); // invalid UTF-8 in the pwd $c = connect(array( @@ -77,89 +95,10 @@ $c = connect(array( if ($c !== false) { fatalError("sqlsrv_connect(4) should have failed"); } -print_r(sqlsrv_errors()); +checkErrors($utf16Error); echo "Test succeeded.\n"; ?> ---EXPECTF-- -Array -( - [0] => Array - ( - [0] => IMSSP - [SQLSTATE] => IMSSP - [1] => -48 - [code] => -48 - [2] => The encoding 'gibberish' is not a supported encoding for the CharacterSet connection option. - [message] => The encoding 'gibberish' is not a supported encoding for the CharacterSet connection option. - ) - -) -Array -( - [0] => Array - ( - [0] => IMSSP - [SQLSTATE] => IMSSP - [1] => -48 - [code] => -48 - [2] => The encoding 'binary' is not a supported encoding for the CharacterSet connection option. - [message] => The encoding 'binary' is not a supported encoding for the CharacterSet connection option. - ) - -) -Array -( - [0] => Array - ( - [0] => IMSSP - [SQLSTATE] => IMSSP - [1] => -47 - [code] => -47 - [2] => An error occurred translating the connection string to UTF-16: No mapping for the Unicode character exists in the target multi-byte code page. - - [message] => An error occurred translating the connection string to UTF-16: No mapping for the Unicode character exists in the target multi-byte code page. - - ) - -) -Array -( - [0] => Array - ( - [0] => 28000 - [SQLSTATE] => 28000 - [1] => 18456 - [code] => 18456 - [2] => %SLogin failed for user '%s'. - [message] => %SLogin failed for user '%s'. - ) - - [1] => Array - ( - [0] => 28000 - [SQLSTATE] => 28000 - [1] => 18456 - [code] => 18456 - [2] => %SLogin failed for user '%s'. - [message] => %SLogin failed for user '%s'. - ) - -) -Array -( - [0] => Array - ( - [0] => IMSSP - [SQLSTATE] => IMSSP - [1] => -47 - [code] => -47 - [2] => An error occurred translating the connection string to UTF-16: No mapping for the Unicode character exists in the target multi-byte code page. - - [message] => An error occurred translating the connection string to UTF-16: No mapping for the Unicode character exists in the target multi-byte code page. - - ) - -) +--EXPECT-- Test succeeded. diff --git a/test/functional/sqlsrv/sqlsrv_data_classification.phpt b/test/functional/sqlsrv/sqlsrv_data_classification.phpt new file mode 100644 index 00000000..ab06cce6 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_data_classification.phpt @@ -0,0 +1,312 @@ +--TEST-- +Test data classification feature - retrieving sensitivity metadata if supported +--DESCRIPTION-- +If both ODBC and server support this feature, this test verifies that sensitivity metadata can be added and correctly retrieved. If not, it will at least test the new statement attribute and some error cases. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + true); + $tsql = ($isSupported)? "SELECT PatientId FROM $tableName" : "SELECT * FROM $tableName"; + $stmt = sqlsrv_query($conn, $tsql, array(), $options); + if (!$stmt) { + fatalError("testErrorCases (1): failed with sqlsrv_query '$tsql'.\n"); + } + + $notAvailableErr = '*Failed to retrieve Data Classification Sensitivity Metadata. If the driver and the server both support the Data Classification feature, check whether the query returns columns with classification information.'; + + $unexpectedErrorState = '*Failed to retrieve Data Classification Sensitivity Metadata: Check if ODBC driver or the server supports the Data Classification feature.'; + + $error = ($driverCapable) ? $notAvailableErr : $unexpectedErrorState; + + $metadata = sqlsrv_field_metadata($stmt); + if ($metadata) { + echo "testErrorCases (1): expected sqlsrv_field_metadata to fail\n"; + } + + if (!fnmatch($error, sqlsrv_errors()[0]['message'])) { + var_dump(sqlsrv_errors()); + } + + // (2) call sqlsrv_prepare() with DataClassification but do not execute the stmt + $stmt = sqlsrv_prepare($conn, $tsql, array(), $options); + if (!$stmt) { + fatalError("testErrorCases (2): failed with sqlsrv_prepare '$tsql'.\n"); + } + + $executeFirstErr = '*The statement must be executed to retrieve Data Classification Sensitivity Metadata.'; + $metadata = sqlsrv_field_metadata($stmt); + if ($metadata) { + echo "testErrorCases (2): expected sqlsrv_field_metadata to fail\n"; + } + + if (!fnmatch($executeFirstErr, sqlsrv_errors()[0]['message'])) { + var_dump(sqlsrv_errors()); + } +} + +function isDataClassSupported($conn, &$driverCapable) +{ + // Check both SQL Server version and ODBC driver version + $msodbcsqlVer = sqlsrv_client_info($conn)['DriverVer']; + $version = explode(".", $msodbcsqlVer); + + // ODBC Driver must be 17.2 or above + $driverCapable = true; + if ($version[0] < 17 || $version[1] < 2) { + $driverCapable = false; + return false; + } + + // SQL Server must be SQL Server 2019 or above + $serverVer = sqlsrv_server_info($conn)['SQLServerVersion']; + if (explode('.', $serverVer)[0] < 15) { + return false; + } + + return true; +} + +function getRegularMetadata($conn, $tsql) +{ + // Run the query without data classification metadata + $stmt1 = sqlsrv_query($conn, $tsql); + if (!$stmt1) { + fatalError("getRegularMetadata (1): failed in sqlsrv_query.\n"); + } + + // Run the query with the attribute set to false + $options = array('DataClassification' => false); + $stmt2 = sqlsrv_query($conn, $tsql, array(), $options); + if (!$stmt2) { + fatalError("getRegularMetadata (2): failed in sqlsrv_query.\n"); + } + + // The metadata for each statement, column by column, should be identical + $numCol = sqlsrv_num_fields($stmt1); + $metadata1 = sqlsrv_field_metadata($stmt1); + $metadata2 = sqlsrv_field_metadata($stmt2); + + for ($i = 0; $i < $numCol; $i++) { + $diff = array_diff($metadata1[$i], $metadata2[$i]); + if (!empty($diff)) { + print_r($diff); + } + } + + return $stmt1; +} + +function verifyClassInfo($input, $actual) +{ + // For simplicity of this test, only one set of sensitivity data. Namely, + // an array with one set of Label (name, id) and Information Type (name, id) + if (count($actual) != 1) { + echo "Expected an array with only one element\n"; + return false; + } + + if (count($actual[0]) != 2) { + echo "Expected a Label pair and Information Type pair\n"; + return false; + } + + // Label should be name and id pair (id should be empty) + if (count($actual[0]['Label']) != 2) { + echo "Expected only two elements for the label\n"; + return false; + } + $label = $input[0]; + if ($actual[0]['Label']['name'] !== $label || !empty($actual[0]['Label']['id'])){ + return false; + } + + // Like Label, Information Type should also be name and id pair (id should be empty) + if (count($actual[0]['Information Type']) != 2) { + echo "Expected only two elements for the information type\n"; + return false; + } + $info = $input[1]; + if ($actual[0]['Information Type']['name'] !== $info || !empty($actual[0]['Information Type']['id'])){ + return false; + } + + return true; +} + +function compareDataClassification($stmt1, $stmt2, $classData) +{ + global $dataClassKey; + + $numCol = sqlsrv_num_fields($stmt1); + + $metadata1 = sqlsrv_field_metadata($stmt1); + $metadata2 = sqlsrv_field_metadata($stmt2); + + // The built-in array_diff_assoc() function compares the keys and values + // of two (or more) arrays, and returns an array that contains the entries + // from array1 that are not present in array2 or array3, etc. + // + // For this test, $metadata2 should have one extra key 'Data Classification', + // which should not be present in $metadata1 + // + // If the column does not have sensitivity metadata, the value should be an + // empty array. Otherwise, it should contain an array with one set of + // Label (name, id) and Information Type (name, id) + + $noClassInfo = array($dataClassKey => array()); + for ($i = 0; $i < $numCol; $i++) { + $diff = array_diff_assoc($metadata2[$i], $metadata1[$i]); + + // Is classification input data empty? + if (empty($classData[$i])) { + // Then it should be equivalent to $noClassInfo + if ($diff !== $noClassInfo) { + var_dump($diff); + } + } else { + // Verify the classification metadata + if (!verifyClassInfo($classData[$i], $diff[$dataClassKey])) { + var_dump($diff); + } + } + } +} + +function runBatchQuery($conn, $tableName) +{ + global $dataClassKey; + + $options = array('DataClassification' => true); + $tsql = "SELECT SSN, BirthDate FROM $tableName"; + $batchQuery = $tsql . ';' . $tsql; + + $stmt = sqlsrv_query($conn, $batchQuery, array(), $options); + if (!$stmt) { + fatalError("Error when calling sqlsrv_query '$tsql'.\n"); + } + + $numCol = sqlsrv_num_fields($stmt); + $c = rand(0, $numCol - 1); + + $metadata1 = sqlsrv_field_metadata($stmt); + if (!$metadata1 || !array_key_exists($dataClassKey, $metadata1[$c])) { + fatalError("runBatchQuery(1): failed to get metadata"); + } + $result = sqlsrv_next_result($stmt); + if (is_null($result) || !$result) { + fatalError("runBatchQuery: failed to get next result"); + } + $metadata2 = sqlsrv_field_metadata($stmt); + if (!$metadata2 || !array_key_exists($dataClassKey, $metadata2[$c])) { + fatalError("runBatchQuery(2): failed to get metadata"); + } + + $jstr1 = json_encode($metadata1[$c][$dataClassKey]); + $jstr2 = json_encode($metadata2[$c][$dataClassKey]); + if ($jstr1 !== $jstr2) { + echo "The JSON encoded strings should be identical\n"; + var_dump($jstr1); + var_dump($jstr2); + } +} + +/////////////////////////////////////////////////////////////////////////////////////// +require_once('MsCommon.inc'); + +$conn = AE\connect(); +if (!$conn) { + fatalError("Failed to connect.\n"); +} + +$driverCapable = true; +$isSupported = isDataClassSupported($conn, $driverCapable); + +// Create a test table +$tableName = 'srvPatients'; +$colMeta = array(new AE\ColumnMeta('INT', 'PatientId', 'IDENTITY NOT NULL'), + new AE\ColumnMeta('CHAR(11)', 'SSN'), + new AE\ColumnMeta('NVARCHAR(50)', 'FirstName'), + new AE\ColumnMeta('NVARCHAR(50)', 'LastName'), + new AE\ColumnMeta('DATE', 'BirthDate')); +AE\createTable($conn, $tableName, $colMeta); + +// If data classification is supported, then add sensitivity classification metadata +// to columns SSN and Birthdate +$classData = [ + array(), + array('Highly Confidential - GDPR', 'Credentials'), + array(), + array(), + array('Confidential Personal Data', 'Birthdays') + ]; + +if ($isSupported) { + // column SSN + $label = $classData[1][0]; + $infoType = $classData[1][1]; + $sql = "ADD SENSITIVITY CLASSIFICATION TO [$tableName].SSN WITH (LABEL = '$label', INFORMATION_TYPE = '$infoType')"; + $stmt = sqlsrv_query($conn, $sql); + if (!$stmt) { + fatalError("SSN: Add sensitivity $label and $infoType failed.\n"); + } + + // column BirthDate + $label = $classData[4][0]; + $infoType = $classData[4][1]; + $sql = "ADD SENSITIVITY CLASSIFICATION TO [$tableName].BirthDate WITH (LABEL = '$label', INFORMATION_TYPE = '$infoType')"; + $stmt = sqlsrv_query($conn, $sql); + if (!$stmt) { + fatalError("BirthDate: Add sensitivity $label and $infoType failed.\n"); + } +} + +testErrorCases($conn, $tableName, $isSupported, $driverCapable); + +// Run the query without data classification metadata +$tsql = "SELECT * FROM $tableName"; +$stmt = getRegularMetadata($conn, $tsql); + +// Proceeed to retrieve sensitivity metadata, if supported +if ($isSupported) { + $options = array('DataClassification' => true); + $stmt1 = sqlsrv_prepare($conn, $tsql, array(), $options); + if (!$stmt1) { + fatalError("Error when calling sqlsrv_prepare '$tsql'.\n"); + } + if (!sqlsrv_execute($stmt1)) { + fatalError("Error in executing statement.\n"); + } + + compareDataClassification($stmt, $stmt1, $classData); + sqlsrv_free_stmt($stmt1); + + // $stmt2 should produce the same result as the previous $stmt1 + $stmt2 = sqlsrv_query($conn, $tsql, array(), $options); + if (!$stmt2) { + fatalError("Error when calling sqlsrv_query '$tsql'.\n"); + } + + compareDataClassification($stmt, $stmt2, $classData); + sqlsrv_free_stmt($stmt2); + + runBatchQuery($conn, $tableName); +} + +sqlsrv_free_stmt($stmt); + +dropTable($conn, $tableName); +sqlsrv_close($conn); + +echo "Done\n"; +?> +--EXPECT-- +Done diff --git a/test/functional/sqlsrv/sqlsrv_data_to_str.phpt b/test/functional/sqlsrv/sqlsrv_data_to_str.phpt index 08f42b26..8087d1d0 100644 --- a/test/functional/sqlsrv/sqlsrv_data_to_str.phpt +++ b/test/functional/sqlsrv/sqlsrv_data_to_str.phpt @@ -1,5 +1,7 @@ --TEST-- large types to strings of 1MB size. +--DESCRIPTION-- +This includes a test by providing an invalid php type. --SKIPIF-- --FILE-- @@ -8,7 +10,7 @@ large types to strings of 1MB size. sqlsrv_configure( 'WarningsReturnAsErrors', 0 ); sqlsrv_configure( 'LogSeverity', SQLSRV_LOG_SEVERITY_ALL ); - require( 'MsCommon.inc' ); + require_once( 'MsCommon.inc' ); $conn = Connect(); if( !$conn ) { @@ -59,6 +61,16 @@ large types to strings of 1MB size. die( "sqlsrv_get_field(6) failed." ); } + $str = sqlsrv_get_field( $stmt, 0, SQLSRV_PHPTYPE_STRING("UTF") ); + if ($str === false) { + $error = sqlsrv_errors()[0]['message']; + if ($error !== 'Invalid type') { + fatalError('Unexpected error returned'); + } + } else { + echo "Expect sqlsrv_get_field(7) to fail!\n"; + } + sqlsrv_free_stmt( $stmt ); sqlsrv_close( $conn ); diff --git a/test/functional/sqlsrv/sqlsrv_empty_result_error.phpt b/test/functional/sqlsrv/sqlsrv_empty_result_error.phpt index 7f751324..92b7f0ae 100644 --- a/test/functional/sqlsrv/sqlsrv_empty_result_error.phpt +++ b/test/functional/sqlsrv/sqlsrv_empty_result_error.phpt @@ -153,7 +153,7 @@ echo "Null result set, call next result first: #############################\n"; $stmt = sqlsrv_query($conn, "TestEmptySetProc @a='a', @b='c'"); NextResult($stmt, []); -Fetch($stmt, [$errorFuncSeq()]); +Fetch($stmt, [$errorNoFields]); // Call next_result twice in succession on a null result set echo "Null result set, call next result twice: #############################\n"; diff --git a/test/functional/sqlsrv/sqlsrv_escape_braces.phpt b/test/functional/sqlsrv/sqlsrv_escape_braces.phpt new file mode 100644 index 00000000..3c3eeae0 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_escape_braces.phpt @@ -0,0 +1,70 @@ +--TEST-- +Test that right braces are escaped correctly and that error messages are correct when they're not +--SKIPIF-- + +--FILE-- +$test[0], 'pwd'=>$password, 'LoginTimeout'=>1)); + + if (strpos(sqlsrv_errors()[0][2], $test[1]) === false) { + print_r("Wrong error message returned for test string ".$test[0].". Expected ".$test[1].", actual output:\n"); + print_r(sqlsrv_errors()); + } + + unset($conn); +} + +echo "Done.\n"; +?> +--EXPECT-- +Done. diff --git a/test/functional/sqlsrv/sqlsrv_statement_format_money_types.phpt b/test/functional/sqlsrv/sqlsrv_statement_format_money_types.phpt index ac2c1cd0..abd4bb7d 100644 --- a/test/functional/sqlsrv/sqlsrv_statement_format_money_types.phpt +++ b/test/functional/sqlsrv/sqlsrv_statement_format_money_types.phpt @@ -63,20 +63,10 @@ function testFloatTypes($conn) for ($i = 0; $i < count($values); $i++) { $floatStr = sqlsrv_get_field($stmt, $i, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); $floatVal = floatval($floatStr); - - // Check if the numbers of decimal digits are the same - // It is highly unlikely but not impossible - $numbers = explode('.', $floatStr); - $len = strlen($numbers[1]); - if ($len == $numDigits && $floatVal != $floats[$i]) { - echo "Expected $floats[$i] but returned "; + $diff = abs($floatVal - $floats[$i]) / $floats[$i]; + if ($diff > $epsilon) { + echo "$diff: Expected $floats[$i] but returned "; var_dump($floatVal); - } else { - $diff = abs($floatVal - $floats[$i]) / $floats[$i]; - if ($diff > $epsilon) { - echo "$diff: Expected $floats[$i] but returned "; - var_dump($floatVal); - } } } } else { diff --git a/test/functional/sqlsrv/srv_007_login_timeout.phpt b/test/functional/sqlsrv/srv_007_login_timeout.phpt index c8fa5cf1..eda8cf3a 100644 --- a/test/functional/sqlsrv/srv_007_login_timeout.phpt +++ b/test/functional/sqlsrv/srv_007_login_timeout.phpt @@ -9,17 +9,42 @@ Intentionally provide an invalid server name and set LoginTimeout. Verify the ti $serverName = "WRONG_SERVER_NAME"; -$t0 = microtime(true); +// Based on the following reference, a login timeout of less than approximately 10 seconds +// is not reliable. The defaut is 15 seconds so we fix it at 20 seconds. +// https://docs.microsoft.com/sql/connect/odbc/windows/features-of-the-microsoft-odbc-driver-for-sql-server-on-windows -$conn = sqlsrv_connect($serverName , array("LoginTimeout" => 8)); +$timeout = 20; +$maxAttempts = 3; +$numAttempts = 0; +$leeway = 1.0; +$missed = false; -$t1 = microtime(true); +do { + $t0 = microtime(true); -echo "Connection attempt time: " . ($t1 - $t0) . " [sec]\n"; + $conn = sqlsrv_connect($serverName , array("LoginTimeout" => $timeout)); + $numAttempts++; -print "Done"; + $t1 = microtime(true); + + // Sometimes time elapsed might be less than expected timeout, such as 19.99* + // something, but 1.0 second leeway should be reasonable + $elapsed = $t1 - $t0; + $diff = abs($elapsed - $timeout); + + $missed = ($diff > $leeway); + if ($missed) { + if ($numAttempts == $maxAttempts) { + echo "Connection failed at $elapsed secs. Leeway is $leeway sec but the difference is $diff\n"; + } else { + // The test will fail but this helps us decide if this test should be redesigned + echo "$numAttempts\t"; + sleep(5); + } + } +} while ($missed && $numAttempts < $maxAttempts); + +print "Done\n"; ?> - ---EXPECTREGEX-- -Connection attempt time: [7-9]\.[0-9]+ \[sec\] +--EXPECT-- Done diff --git a/test/functional/sqlsrv/srv_228_sqlsrv_clientbuffermaxkbsize_option.phpt b/test/functional/sqlsrv/srv_228_sqlsrv_clientbuffermaxkbsize_option.phpt index 459169d9..c60e23de 100644 --- a/test/functional/sqlsrv/srv_228_sqlsrv_clientbuffermaxkbsize_option.phpt +++ b/test/functional/sqlsrv/srv_228_sqlsrv_clientbuffermaxkbsize_option.phpt @@ -44,7 +44,8 @@ function fetchData($conn, $table, $size) echo "Expect this to fail\n"; } else { $error = 'Memory limit of 1 KB exceeded for buffered query'; - if (strpos(sqlsrv_errors()[0]['message'], $error) === false) { + $errors = sqlsrv_errors(); + if (!empty($errors) && strpos($errors[0]['message'], $error) === false) { print_r(sqlsrv_errors()); } } diff --git a/test/functional/sqlsrv/srv_569_query_varcharmax.phpt b/test/functional/sqlsrv/srv_569_query_varcharmax.phpt new file mode 100644 index 00000000..6d4bab35 --- /dev/null +++ b/test/functional/sqlsrv/srv_569_query_varcharmax.phpt @@ -0,0 +1,123 @@ +--TEST-- +GitHub issue #569 - sqlsrv_query on varchar max fields results in function sequence error +--DESCRIPTION-- +Verifies that the problem is no longer reproducible. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + $database, "UID" => $userName, "PWD" => $userPassword); +$conn = sqlsrv_connect($server, $options); +if ($conn === false) { + fatalError("Failed to connect to $server."); +} + +$qualified = AE\isQualified($conn); +if ($qualified) { + sqlsrv_close($conn); + + // Now connect with ColumnEncryption enabled + $connectionOptions = array_merge($options, array('ColumnEncryption' => 'Enabled')); + $conn = sqlsrv_connect($server, $connectionOptions); + if ($conn === false) { + fatalError("Failed to connect to $server."); + } +} + +$tableName = 'srvTestTable_569'; + +dropTable($conn, $tableName); + +if ($qualified && strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + $tsql = "CREATE TABLE $tableName ([c1] varchar(max) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = deterministic, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = AEColumnKey))"; +} else { + $tsql = "CREATE TABLE $tableName ([c1] varchar(max))"; +} + +$stmt = sqlsrv_query($conn, $tsql); +if (!$stmt) { + fatalError("Failed to create $tableName"); +} + +$input = 'some very large string'; +$stmt = sqlsrv_prepare($conn, "INSERT INTO $tableName (c1) VALUES (?)", array($input)); +sqlsrv_execute($stmt); + +$tsql = "SELECT * FROM $tableName"; +$stmt = sqlsrv_query($conn, $tsql); +if (!$stmt) { + fatalError("Failed to read from $tableName"); +} + +sqlsrv_fetch($stmt); +$fieldVal = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); + +if ($fieldVal !== $input) { + echo "Expected $input but got: "; + var_dump($fieldVal); +} + +$tsql2 = "DELETE FROM $tableName"; +$stmt = sqlsrv_query($conn, $tsql2); +if (!$stmt) { + fatalError("Failed to delete rows from $tableName"); +} + +$stmt = sqlsrv_query($conn, $tsql); +if (!$stmt) { + fatalError("Failed to read $tableName, now empty"); +} + +$result = sqlsrv_fetch($stmt); +if (!is_null($result)) { + echo 'Expected null when fetching an empty table but got: '; + var_dump($result); +} + +$fieldVal = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); +verifyFetchError(); +if ($fieldVal !== false) { + echo 'Expected bool(false) but got: '; + var_dump($fieldVal); +} + +$stmt = sqlsrv_query($conn, $tsql, array(), array("Scrollable"=>"buffered")); +$result = sqlsrv_fetch($stmt); +if (!is_null($result)) { + echo 'Expected null when fetching an empty table but got: '; + var_dump($result); +} + +$fieldVal = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); +verifyFetchError(); +if ($fieldVal !== false) { + echo 'Expected bool(false) but got: '; + var_dump($fieldVal); +} + + +dropTable($conn, $tableName); + +echo "Done\n"; + +sqlsrv_free_stmt($stmt); +sqlsrv_close($conn); + +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/srv_570_fetch_varbinary.phpt b/test/functional/sqlsrv/srv_570_fetch_varbinary.phpt new file mode 100644 index 00000000..34f11240 --- /dev/null +++ b/test/functional/sqlsrv/srv_570_fetch_varbinary.phpt @@ -0,0 +1,143 @@ +--TEST-- +GitHub issue #570 - fetching a varbinary field as a stream using client buffer +--DESCRIPTION-- +Verifies that a varbinary field (with size or max) can be successfully fetched even when a client buffer is used. There is no more "Invalid cursor state" error. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + SQLSRV_CURSOR_CLIENT_BUFFERED, "ClientBufferMaxKBSize" => 51200)); + } else { + $stmt = sqlsrv_prepare($conn, $tsql); + } + if ($stmt === false) { + fatalError("Error in preparing the query ($buffered)."); + } + + $result = sqlsrv_execute($stmt); + if ($result === false) { + fatalError("Error in executing the query ($buffered)."); + } + } else { + if ($buffered) { + // Use the default buffer size in this case + $stmt = sqlsrv_query($conn, $tsql, array(), array("Scrollable"=>SQLSRV_CURSOR_CLIENT_BUFFERED)); + } else { + $stmt = sqlsrv_query($conn, $tsql); + } + + if ($stmt === false) { + fatalError("Error in sqlsrv_query ($buffered)."); + } + } + + fetchData($stmt, $data, $buffered); +} + +function runTest($conn, $columnType, $path) +{ + $tableName = 'srvTestTable_570' . rand(0, 10); + dropTable($conn, $tableName); + + // Create the test table with only one column + $tsql = "CREATE TABLE $tableName([picture] $columnType NOT NULL)"; + $stmt = sqlsrv_query($conn, $tsql); + if (!$stmt) { + fatalError("Failed to create table $tableName\n"); + } + + // Insert php.gif as stream data + $tsql = "INSERT INTO $tableName (picture) VALUES (?)"; + + $data = fopen($path, 'rb'); + if (!$data) { + fatalError('Could not open image for reading.'); + } + + $params = array($data, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY)); + $stmt = sqlsrv_query($conn, $tsql, array($params)); + if ($stmt === false) { + fatalError("Failed to insert image into $tableName"); + } + do { + $read = sqlsrv_send_stream_data($stmt); + } while ($read); + sqlsrv_free_stmt($stmt); + + // Start testing, with or without client buffer, using prepared statement or direct query + $tsql = "SELECT picture FROM $tableName"; + runQuery($conn, $data, $tsql, false, true); + runQuery($conn, $data, $tsql, true, true); + runQuery($conn, $data, $tsql, false, false); + runQuery($conn, $data, $tsql, true, false); + + // Clean up + fclose($data); + + dropTable($conn, $tableName); +} + +require_once('MsCommon.inc'); + +$conn = connect(); +if ($conn === false) { + die(print_r(sqlsrv_errors(), true)); +} + +if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + $pic = '\\php.gif'; +} else { // other than Windows + $pic = '/php.gif'; +} +$path = dirname($_SERVER['PHP_SELF']) . $pic; + +runTest($conn, 'VARBINARY(MAX)', $path); +runTest($conn, 'VARBINARY(4096)', $path); + +echo "Done\n"; + +sqlsrv_close($conn); +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/test_error_encoding.phpt b/test/functional/sqlsrv/test_error_encoding.phpt index 76302e20..c14b41ce 100644 --- a/test/functional/sqlsrv/test_error_encoding.phpt +++ b/test/functional/sqlsrv/test_error_encoding.phpt @@ -4,11 +4,35 @@ Encoding of sqlsrv errors --FILE-- 'UTF-8' )); +require_once('MsSetup.inc'); + +$connectionOptions = array('UID' => $userName, 'PWD' => $userPassword, 'CharacterSet' => 'UTF-8'); +$conn = sqlsrv_connect($server, $connectionOptions); if (!$conn) { die(print_r(sqlsrv_errors(), true)); } @@ -22,27 +46,15 @@ sqlsrv_free_stmt($stmt); $stmt = sqlsrv_query($conn, "select *, BadColumn from sys.syslanguages"); if ($stmt) { - echo 'OK!'; + echo 'This should have failed!\n'; sqlsrv_free_stmt($stmt); } else { - $errs = sqlsrv_errors(); - print_r($errs); + verifyErrorContents(); } sqlsrv_close($conn); +echo "Done\n"; ?> ---EXPECTF-- -Array -( - [0] => Array - ( - [0] => 42S22 - [SQLSTATE] => 42S22 - [1] => 207 - [code] => 207 - [2] => %SUngültiger Spaltenname %cBadColumn%c. - [message] => %SUngültiger Spaltenname %cBadColumn%c. - ) - -) +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/test_error_encoding_with_language_option.phpt b/test/functional/sqlsrv/test_error_encoding_with_language_option.phpt new file mode 100644 index 00000000..6309af7e --- /dev/null +++ b/test/functional/sqlsrv/test_error_encoding_with_language_option.phpt @@ -0,0 +1,55 @@ +--TEST-- +GitHub issue 929 - able to change the language when connecting +--DESCRIPTION-- +A test similar to test_error_encoding, created for GitHub issue 929 +--SKIPIF-- + +--FILE-- + $userName, 'PWD' => $userPassword, 'CharacterSet' => 'UTF-8', 'Language' => 'German'); +$conn = sqlsrv_connect($server, $connectionOptions); +if (!$conn) { + die(print_r(sqlsrv_errors(), true)); +} + +$stmt = sqlsrv_query($conn, "select *, BadColumn from sys.syslanguages"); +if ($stmt) { + echo 'This should have failed!\n'; + sqlsrv_free_stmt($stmt); +} else { + verifyErrorContents(); +} + +sqlsrv_close($conn); + +echo "Done\n"; +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/test_largeData.phpt b/test/functional/sqlsrv/test_largeData.phpt index 4fa2d830..72feabf8 100644 --- a/test/functional/sqlsrv/test_largeData.phpt +++ b/test/functional/sqlsrv/test_largeData.phpt @@ -1,119 +1,158 @@ --TEST-- -send a large amount (10MB) using encryption. +Send a large amount (10MB) using encryption. In a Linux CI environment use a smaller size. --SKIPIF-- --FILE-- total_read = 0; return true; } - function stream_read( $count ) + public function stream_read($count) { - if( $this->total_read > 20000000 ) { + global $limit; + if ($this->total_read > $limit) { return 0; } global $packets; ++$packets; - $str = str_repeat( "A", $count ); + + // 8192 is passed to stream_read as $count + $str = str_repeat("A", $count); $this->total_read += $count; return $str; } - function stream_write($data) + public function stream_write($data) { } - function stream_tell() + public function stream_tell() { return $this->total_read; } - function stream_eof() + public function stream_eof() { - return $this->total_read > 20000000; + global $limit; + return $this->total_read > $limit; } - function stream_seek($offset, $whence) + public function stream_seek($offset, $whence) { // For the purpose of this test only support SEEK_SET to $offset 0 if ($whence == SEEK_SET && $offset == 0) { $this->total_read = $offset; return true; - } + } return false; } } +function isServerInLinux($conn) +{ + // This checks if SQL Server is running in Linux (Docker) in a CI environment + // If so, the major version must be 14 or above (SQL Server 2017 or above) + $serverVer = sqlsrv_server_info($conn)['SQLServerVersion']; + if (explode('.', $serverVer)[0] < 14) { + return false; + } + + // The view sys.dm_os_host_info, available starting in SQL Server 2017, is somewhat similar to sys.dm_os_windows_info. + // It returns one row that displays operating system version information and has columns to differentiate + // Windows and Linux. + $stmt = sqlsrv_query($conn, 'SELECT host_platform FROM sys.dm_os_host_info'); + if ($stmt && sqlsrv_fetch($stmt)) { + $host = sqlsrv_get_field($stmt, 0); + return ($host === 'Linux'); + } + + return false; +} + set_time_limit(0); -sqlsrv_configure( 'WarningsReturnAsErrors', 0 ); -sqlsrv_configure( 'LogSubsystems', SQLSRV_LOG_SYSTEM_ALL ); +sqlsrv_configure('WarningsReturnAsErrors', 0); +sqlsrv_configure('LogSubsystems', SQLSRV_LOG_SYSTEM_ALL); $packets = 0; +$limit = 20000000; -$result = stream_wrapper_register( "mystr", "my_stream" ); -if( !$result ) { - die( "Couldn't register stream class." ); +$result = stream_wrapper_register("mystr", "my_stream"); +if (!$result) { + die("Couldn't register stream class."); } -require( 'MsCommon.inc' ); +require_once('MsCommon.inc'); $conn = Connect(array( 'Encrypt' => true, 'TrustServerCertificate' => true )); -if( $conn === false ) { - die( print_r( sqlsrv_errors(), true )); +if ($conn === false) { + die(print_r(sqlsrv_errors(), true)); } -$stmt = sqlsrv_query( $conn, "IF OBJECT_ID('test_lob', 'U') IS NOT NULL DROP TABLE test_lob" ); -if( $stmt !== false ) sqlsrv_free_stmt( $stmt ); - -$stmt = sqlsrv_query( $conn, "CREATE TABLE test_lob (id tinyint, stuff varbinary(max))" ); -if( $stmt === false ) { - die( print_r( sqlsrv_errors(), true )); -} -sqlsrv_free_stmt( $stmt ); - -$lob = fopen( "mystr://test_data", "rb" ); -if( !$lob ) { - die( "failed opening test stream.\n" ); -} -$stmt = sqlsrv_query( $conn, "INSERT INTO test_lob (id, stuff) VALUES (?,?)", array( 1, array( $lob, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY), SQLSRV_SQLTYPE_VARBINARY('max')))); -if( $stmt === false ) { - die( print_r( sqlsrv_errors(), true )); +// In a Linux CI environment use a smaller size +if (isServerInLinux($conn)) { + $limit /= 100; } -while( $result = sqlsrv_send_stream_data( $stmt )) { +$stmt = sqlsrv_query($conn, "IF OBJECT_ID('test_lob', 'U') IS NOT NULL DROP TABLE test_lob"); +if ($stmt !== false) { + sqlsrv_free_stmt($stmt); +} + +$stmt = sqlsrv_query($conn, "CREATE TABLE test_lob (id tinyint, stuff varbinary(max))"); +if ($stmt === false) { + die(print_r(sqlsrv_errors(), true)); +} +sqlsrv_free_stmt($stmt); + +$lob = fopen("mystr://test_data", "rb"); +if (!$lob) { + die("failed opening test stream.\n"); +} +$stmt = sqlsrv_query($conn, "INSERT INTO test_lob (id, stuff) VALUES (?,?)", array( 1, array( $lob, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY), SQLSRV_SQLTYPE_VARBINARY('max')))); +if ($stmt === false) { + die(print_r(sqlsrv_errors(), true)); +} + +while ($result = sqlsrv_send_stream_data($stmt)) { ++$packets; } -if( $result === false ) { - die( print_r( sqlsrv_errors(), true )); -} -echo "$packets sent.\n"; - -$stmt = sqlsrv_query( $conn, "SELECT LEN(stuff) FROM test_lob" ); -if( $stmt === false ) { - die( print_r( sqlsrv_errors(), true )); -} -while( $result = sqlsrv_fetch_array( $stmt )) { - print_r( $result ); +if ($result === false) { + die(print_r(sqlsrv_errors(), true)); } -sqlsrv_query( $conn, "DROP TABLE test_lob" ); +// Number of packets sent should be $limit / 8192 (rounded up) +// Length of the varbinary = $packetsSent * 8192 + 1 (HEX 30 appended at the end) +$packetsSent = ceil($limit / 8192); +$length = $packetsSent * 8192 + 1; +if ($packets != $packetsSent) { + echo "$packets sent.\n"; +} -sqlsrv_free_stmt( $stmt ); -sqlsrv_close( $conn ); +$stmt = sqlsrv_query($conn, "SELECT LEN(stuff) FROM test_lob"); +if ($stmt === false) { + die(print_r(sqlsrv_errors(), true)); +} +while ($result = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC)) { + if ($result[0] != $length) { + print_r($result); + } +} + +sqlsrv_query($conn, "DROP TABLE test_lob"); + +sqlsrv_free_stmt($stmt); +sqlsrv_close($conn); sleep(10); // since this is a long test, we give the database some time to finish + +echo "Done\n"; ?> --EXPECT-- -2442 sent. -Array -( - [0] => 20004865 - [] => 20004865 -) +Done diff --git a/test/functional/sqlsrv/test_scrollable.phpt b/test/functional/sqlsrv/test_scrollable.phpt index 7e67a597..524da151 100644 --- a/test/functional/sqlsrv/test_scrollable.phpt +++ b/test/functional/sqlsrv/test_scrollable.phpt @@ -1,5 +1,5 @@ --TEST-- -scrollable result sets. +Scrollable result sets with a simple test for an expected error. --SKIPIF-- --FILE-- @@ -69,6 +69,13 @@ for ($i = 1; $i <= $numRows; $i++) { } $query = "SELECT * FROM $tableName"; +$options = array('Scrollable' => 'dummy'); +$stmt = sqlsrv_query($conn, $query, array(), $options); +if ($stmt !== false) { + fatalError("Expect dummy scrollable to fail!\n"); +} +print_r(sqlsrv_errors()); + $options = array('Scrollable' => SQLSRV_CURSOR_FORWARD); $stmt = sqlsrv_query($conn, $query, array(), $options); @@ -205,4 +212,17 @@ echo "Test succeeded.\n"; ?> --EXPECT-- +Array +( + [0] => Array + ( + [0] => IMSSP + [SQLSTATE] => IMSSP + [1] => -54 + [code] => -54 + [2] => The value passed for the 'Scrollable' statement option is invalid. Please use 'static', 'dynamic', 'keyset', 'forward', or 'buffered'. + [message] => The value passed for the 'Scrollable' statement option is invalid. Please use 'static', 'dynamic', 'keyset', 'forward', or 'buffered'. + ) + +) Test succeeded. diff --git a/test/functional/sqlsrv/test_sqlsrv_phptype_stream.phpt b/test/functional/sqlsrv/test_sqlsrv_phptype_stream.phpt index cdf1453510012a233933bed64095435bb02bffb6..908e5d4d4766b54a2d0dc2aca64ad26053a4983d 100644 GIT binary patch delta 143 zcmeAV+!{FHbG?STZ*g#HNoj#zW?r(orZq2@0uZPq=jY`q*eVnk<`fr|#e;aM$t4;p z#i>PQsYN;vd7wyUex82;h%AQb%uLgO>9)01NK4EqPF2uUsD>B|GCD1>Br(Ues3^Zk RL&+_1vLB<&<_t!4bpT!QERg^J delta 80 zcmdlQ*dI9IvqOA-UUI62l5eqder|4lo?d2NvXZ7XFBdPD0uZPq=jY`q*eZai)Z`Kk YkPJvXGfhE55iB`5oY8#q1V&YL02_4|)c^nh