diff --git a/.gitignore b/.gitignore index 4197c631f..acb9b8d91 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,9 @@ build/ *~.nib local.properties .classpath +.vscode/ .settings/ +.gradle/ .loadpath # External tool builders diff --git a/.travis.yml b/.travis.yml index 8aeb99738..47c919414 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -sudo: required +sudo: required language: java jdk: @@ -8,15 +8,33 @@ services: - docker env: + global: - mssql_jdbc_test_connection_properties='jdbc:sqlserver://localhost:1433;databaseName=master;username=sa;password=;' - + - mssql_jdbc_logging='true' + # Enabling logging with console / file handler for JUnit Test Framework. + #- mssql_jdbc_logging_handler='console'|'file' + +#Cache the .m2 folder +cache: + directories: + - $HOME/.m2 + +before_install: + - mkdir AE_Certificates + install: + - cd AE_Certificates + - openssl req -newkey rsa:2048 -x509 -keyout cakey.pem -out cacert.pem -days 3650 -subj "/C=US/ST=WA/L=Redmond/O=Microsoft Corporation/OU=SQL Server/CN=JDBC Driver" -nodes + - openssl pkcs12 -export -in cacert.pem -inkey cakey.pem -out identity.p12 -password pass:password + - keytool -importkeystore -destkeystore clientcert.jks -deststorepass password -srckeystore identity.p12 -srcstoretype PKCS12 -srcstorepass password + - keytool -list -v -keystore clientcert.jks -storepass "password" > JavaKeyStore.txt + - cd .. - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V -Pbuild41 - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V -Pbuild42 before_script: - - docker pull microsoft/mssql-server-linux - - docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=' -p 1433:1433 -d microsoft/mssql-server-linux + - docker pull microsoft/mssql-server-linux:2017-latest + - docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=' -p 1433:1433 -d microsoft/mssql-server-linux:2017-latest script: - docker ps -a @@ -24,7 +42,3 @@ script: ##Test for JDBC Specification 41 & 42 and submit coverage report. - mvn test -B -Pbuild41 jacoco:report && bash <(curl -s https://codecov.io/bash) -cF JDBC41 - mvn test -B -Pbuild42 jacoco:report && bash <(curl -s https://codecov.io/bash) -cF JDBC42 - -#after_success: -# instead of after success we are using && operator for conditional submitting coverage report. -# - bash <(curl -s https://codecov.io/bash) diff --git a/AppVeyorJCE/LICENSE b/AppVeyorJCE/LICENSE new file mode 100644 index 000000000..c65825e32 --- /dev/null +++ b/AppVeyorJCE/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/AppVeyorJCE/README.md b/AppVeyorJCE/README.md new file mode 100644 index 000000000..994e01385 --- /dev/null +++ b/AppVeyorJCE/README.md @@ -0,0 +1,31 @@ +# JCE chocolatey package + +### Disclaimers: +1. All contents within this directory originate from [this GitHub project](https://github.com/TobseF/jce-chocolatey-package). This project was added to allow us to test the Always Encrypted feature on AppVeyor builds. + +2. This is not an official project of Oracle. It\`s only easy of the manual installation: It downloads the JCE from oracle.com and unpacks it to the installed JDK. + + +[Chocolatey](https://chocolatey.org/) package for the [JCE (Unlimited Strength Java Cryptography Extension Policy Files)](http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html) + +This chocolatey package adds the JCE to latest installed Java SDK. The The `JAVA_HOME` environment variable has to point to the JDK. If `JAVA_HOME` is not set, nothing will be changed. The original files are backuped (renamed to `*_old`) and can be reverted at any time. This package is a perfect addion to the [JDK8 package](https://chocolatey.org/packages/jdk8). + +#### Install with [Chocolatey](https://chocolatey.org/) +```PowerShell +choco install jce -y +``` + +#### Build from source: +1. Install [Chocolatey](https://chocolatey.org/). +2. Open cmd with admin rights in jce package directory. +3. Pack NuGet Package (.nupkg). +```PowerShell +cpack +``` +4. Install JCE NuGet Package. +```PowerShell +choco install jce -fdv -s . -y +``` + + + diff --git a/AppVeyorJCE/jce.nuspec b/AppVeyorJCE/jce.nuspec new file mode 100644 index 000000000..9a748b3c7 --- /dev/null +++ b/AppVeyorJCE/jce.nuspec @@ -0,0 +1,28 @@ + + + + + + jce + JCE (Java Cryptography Extension) + 7.0.0 + Sun Microsystems/Oracle Corporation + Tobse Fritz + Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 7 + Downloads and installs the Java Cryptography Extension (JCE) to the lastest JDK. The The JAVA_HOME environment variable has to point to the JDK. If JAVA_HOME is not set, nothing will be changed. The original files are backuped (renamed to *_old) and can be reverted at any time. + https://github.com/TobseF/jce-chocolatey-package + java jce admin + + http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html + false + http://cdn.rawgit.com/chocolatey/chocolatey-coreteampackages/50fd97744110dcbce1acde889c0870599c9d5584/icons/java.svg + + + + + + + diff --git a/AppVeyorJCE/tools/chocolateyInstall.ps1 b/AppVeyorJCE/tools/chocolateyInstall.ps1 new file mode 100644 index 000000000..16a1c30d8 --- /dev/null +++ b/AppVeyorJCE/tools/chocolateyInstall.ps1 @@ -0,0 +1,15 @@ +$script_path = $(Split-Path -parent $MyInvocation.MyCommand.Definition) +$common = $(Join-Path $script_path "common.ps1") +. $common + +#installs JCE +try { + chocolatey-install +} catch { + if ($_.Exception.InnerException) { + $msg = $_.Exception.InnerException.Message + } else { + $msg = $_.Exception.Message + } + throw +} diff --git a/AppVeyorJCE/tools/chocolateyUninstall.ps1 b/AppVeyorJCE/tools/chocolateyUninstall.ps1 new file mode 100644 index 000000000..e89c39932 --- /dev/null +++ b/AppVeyorJCE/tools/chocolateyUninstall.ps1 @@ -0,0 +1,14 @@ +$script_path = $(Split-Path -parent $MyInvocation.MyCommand.Definition) +$common = $(Join-Path $script_path "common.ps1") +. $common + +function Uninstall-ChocolateyPath { +param( + [string] $pathToUninstall, + [System.EnvironmentVariableTarget] $pathType = [System.EnvironmentVariableTarget]::User +) + Write-Debug "Running 'Uninstall-ChocolateyPath' with pathToUninstall:`'$pathToUninstall`'"; + + #get the PATH variable + $envPath = $env:PATH +} \ No newline at end of file diff --git a/AppVeyorJCE/tools/common.ps1 b/AppVeyorJCE/tools/common.ps1 new file mode 100644 index 000000000..128010459 --- /dev/null +++ b/AppVeyorJCE/tools/common.ps1 @@ -0,0 +1,85 @@ +$jce_version = '7' +$zipFolder = 'UnlimitedJCEPolicy' +$script_path = $(Split-Path -parent $MyInvocation.MyCommand.Definition) + +function has_file($filename) { + return Test-Path $filename +} + +function download-from-oracle($url, $output_filename) { + if (!(has_file($output_fileName))) { + Write-Host "Downloading JCE from $url" + + try { + [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } + $client = New-Object Net.WebClient + $dummy = $client.Headers.Add('Cookie', 'gpw_e24=http://www.oracle.com; oraclelicense=accept-securebackup-cookie') + $dummy = $client.DownloadFile($url, $output_filename) + } finally { + [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $null + } + } +} + +function download-jce-file($url, $output_filename) { + $dummy = download-from-oracle $url $output_filename +} + +function download-jce() { + $filename = "UnlimitedJCEPolicyJDK$jce_version.zip" + $url = "http://download.oracle.com/otn-pub/java/jce/$jce_version/$filename" + $output_filename = Join-Path $script_path $filename + If(!(Test-Path $output_filename)){ + $dummy = download-jce-file $url $output_filename + } + return $output_filename +} + +function get-java-home(){ + return Get-EnvironmentVariable 'JAVA_HOME' -Scope 'Machine' -PreserveVariables +} + +function get-jce-dir($java_home) { + return Join-Path $java_home 'jre\lib\security' +} + +function chocolatey-install() { + $java_home = get-java-home + if (!$java_home) { + Write-Host "Couldnt find JAVA_HOME environment variable" + Write-Host "Skipping installation" + }else{ + $jce_dir = get-jce-dir $java_home + $already_patched_file = Join-Path $jce_dir 'local_policy_old.jar' + + If(Test-Path $already_patched_file){ + Write-Host "JCE already installed: $jce_dir" + Write-Host "Skipping installation" + }else{ + Write-Host "JCE is not installed ($already_patched_file) is not present" + Write-Host "Starting installation" + install-jce $jce_dir + } + } +} + +function install-jce($jce_dir) { + $jce_zip_file = download-jce + $temp_dir = Get-EnvironmentVariable 'TEMP' -Scope User -PreserveVariables + $local_policy = Join-Path $jce_dir 'local_policy.jar' + $export_policy = Join-Path $jce_dir 'US_export_policy.jar' + + Write-Host "Downloading JCE ($jce_zip_file)" + Install-ChocolateyZipPackage -PackageName 'jce7' -Url $jce_zip_file -UnzipLocation $temp_dir + + If(Test-Path $local_policy){ + Rename-Item -Path $local_policy -NewName 'local_policy_old.jar' -Force + } + + If(Test-Path $export_policy){ + Rename-Item -Path $export_policy -NewName 'US_export_policy_old.jar' -Force + } + + $unzippedFolder = Join-Path $temp_dir $zipFolder + Copy-Item $unzippedFolder\*.jar $jce_dir -force +} diff --git a/CHANGELOG.md b/CHANGELOG.md index c3ce14f24..0fe6a5717 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,170 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) -## [6.1.5] +## [6.3.4] Preview Release +### Added +- Added new ThreadGroup creation to prevent IllegalThreadStateException if the underlying ThreadGroup has been destroyed. [#474](https://github.com/Microsoft/mssql-jdbc/pull/474) +- Added try-with-resources to JUnit tests [#520](https://github.com/Microsoft/mssql-jdbc/pull/520) + +### Fixed Issues +- Fixed the issue with passing parameters names that start with '@' to a CallableStatement [#495](https://github.com/Microsoft/mssql-jdbc/pull/495) +- Fixed SQLServerDataTable creation being O(n^2) issue [#514](https://github.com/Microsoft/mssql-jdbc/pull/514) + +### Changed +- Changed some manual array copying to System.arraycopy() [#500](https://github.com/Microsoft/mssql-jdbc/pull/500) +- Removed redundant toString() on String objects [#501](https://github.com/Microsoft/mssql-jdbc/pull/501) +- Replaced literals with constants [#502](https://github.com/Microsoft/mssql-jdbc/pull/502) + +## [6.3.3] Preview Release +### Added +- Added connection properties for specifying custom TrustManager [#74](https://github.com/Microsoft/mssql-jdbc/pull/74) + +### Fixed Issues +- Fixed exception thrown by getters on null columns [#488](https://github.com/Microsoft/mssql-jdbc/pull/488) +- Fixed issue with DatabaseMetaData#getImportedKeys() returns wrong value for DELETE_RULE [#490](https://github.com/Microsoft/mssql-jdbc/pull/490) +- Fixed issue with ActivityCorrelator causing a classloader leak [#465](https://github.com/Microsoft/mssql-jdbc/pull/465) + +### Changed +- Removed explicit extends Object [#469](https://github.com/Microsoft/mssql-jdbc/pull/469) +- Removed unnecessary return statements [#471](https://github.com/Microsoft/mssql-jdbc/pull/471) +- Simplified overly complex boolean expressions [#472](https://github.com/Microsoft/mssql-jdbc/pull/472) +- Replaced explicit types with <> (the diamond operator) [#420](https://github.com/Microsoft/mssql-jdbc/pull/420) + +## [6.3.2] Preview Release +### Added +- Added new connection property: sslProtocol [#422](https://github.com/Microsoft/mssql-jdbc/pull/422) +- Added "slow" tag to long running tests [#461](https://github.com/Microsoft/mssql-jdbc/pull/461) + +### Fixed Issues +- Fixed some error messages [#452](https://github.com/Microsoft/mssql-jdbc/pull/452) & [#459](https://github.com/Microsoft/mssql-jdbc/pull/459) +- Fixed statement leaks [#455](https://github.com/Microsoft/mssql-jdbc/pull/455) +- Fixed an issue regarding to loginTimeout with TLS [#456](https://github.com/Microsoft/mssql-jdbc/pull/456) +- Fixed sql_variant issue with String type [#442](https://github.com/Microsoft/mssql-jdbc/pull/442) +- Fixed issue with throwing error message for unsupported datatype [#450](https://github.com/Microsoft/mssql-jdbc/pull/450) +- Fixed issue that initial batchException was not thrown [#458](https://github.com/Microsoft/mssql-jdbc/pull/458) + +### Changed +- Changed sendStringParameterAsUnicode to impact set/update null [#445](https://github.com/Microsoft/mssql-jdbc/pull/445) +- Removed connection property: fipsProvider [#460](https://github.com/Microsoft/mssql-jdbc/pull/460) +- Replaced for and while loops with foeach loops [#421](https://github.com/Microsoft/mssql-jdbc/pull/421) +- Replaced explicit types with the diamond operator [#468](https://github.com/Microsoft/mssql-jdbc/pull/468) & [#420](https://github.com/Microsoft/mssql-jdbc/pull/420) + +## [6.3.1] Preview Release +### Added +- Added support for datetime/smallDatetime in TVP [#435](https://github.com/Microsoft/mssql-jdbc/pull/435) +- Added more Junit tests for Always Encrypted [#432](https://github.com/Microsoft/mssql-jdbc/pull/432) + +### Fixed Issues +- Fixed getString issue for uniqueIdentifier [#423](https://github.com/Microsoft/mssql-jdbc/pull/423) + +### Changed +- Skip long running tests based on Tag [#425](https://github.com/Microsoft/mssql-jdbc/pull/425) +- Removed volatile keyword [#409](https://github.com/Microsoft/mssql-jdbc/pull/409) + +## [6.3.0] Preview Release +### Added +- Added support for sql_variant datatype [#387](https://github.com/Microsoft/mssql-jdbc/pull/387) +- Added more Junit tests for Always Encrypted [#404](https://github.com/Microsoft/mssql-jdbc/pull/404) + +### Fixed Issues +- Fixed Turkey locale issue when lowercasing an "i" [#384](https://github.com/Microsoft/mssql-jdbc/pull/384) +- Fixed issue with incorrect parameter count for INSERT with subquery [#373](https://github.com/Microsoft/mssql-jdbc/pull/373) +- Fixed issue with running DDL in PreparedStatement [#372](https://github.com/Microsoft/mssql-jdbc/pull/372) +- Fixed issue with parameter metadata with whitespace characters [#371](https://github.com/Microsoft/mssql-jdbc/pull/371) +- Fixed handling of explicit boxing and unboxing [#84](https://github.com/Microsoft/mssql-jdbc/pull/84) +- Fixed metadata caching batch query issue [#393](https://github.com/Microsoft/mssql-jdbc/pull/393) +- Fixed javadoc issue for the newest maven version [#385](https://github.com/Microsoft/mssql-jdbc/pull/385) + +### Changed +- Updated ADAL4J dependency to version 1.2.0 [#392](https://github.com/Microsoft/mssql-jdbc/pull/392) +- Updated azure-keyvault dependency to version 1.0.0 [#397](https://github.com/Microsoft/mssql-jdbc/pull/397) + +## [6.2.2] Hotfix & Stable Release +### Changed +- Updated ADAL4J to version 1.2.0 and AKV to version 1.0.0 [#516](https://github.com/Microsoft/mssql-jdbc/pull/516) + +## [6.2.1] Hotfix & Stable Release +### Fixed Issues +- Fixed queries without parameters using preparedStatement [#372](https://github.com/Microsoft/mssql-jdbc/pull/372) +### Changed +- Removed metadata caching [#377](https://github.com/Microsoft/mssql-jdbc/pull/377) + +## [6.2.0] Release Candidate +### Added +- Added TVP and BulkCopy random data test for all data types with server cursor [#319](https://github.com/Microsoft/mssql-jdbc/pull/319) +- Added AE setup and test [#337](https://github.com/Microsoft/mssql-jdbc/pull/337),[328](https://github.com/Microsoft/mssql-jdbc/pull/328) +- Added validation for javadocs for every commit [#338](https://github.com/Microsoft/mssql-jdbc/pull/338) +- Added metdata caching [#345](https://github.com/Microsoft/mssql-jdbc/pull/345) +- Added caching mvn dependencies for Appveyor [#320](https://github.com/Microsoft/mssql-jdbc/pull/320) +- Added caching mvn dependencies for Travis-CI [#322](https://github.com/Microsoft/mssql-jdbc/pull/322) +- Added handle for bulkcopy exceptions [#286](https://github.com/Microsoft/mssql-jdbc/pull/286) +- Added handle for TVP exceptions [#285](https://github.com/Microsoft/mssql-jdbc/pull/285) + +### Fixed Issues +- Fixed metadata caching issue with AE on connection [#361](https://github.com/Microsoft/mssql-jdbc/pull/361) +- Fixed issue with String index out of range parameter metadata [#353](https://github.com/Microsoft/mssql-jdbc/pull/353) +- Fixed javaDocs [#354](https://github.com/Microsoft/mssql-jdbc/pull/354) +- Fixed javaDocs [#299](https://github.com/Microsoft/mssql-jdbc/pull/299) +- Performance fix from @brettwooldridge [#347](https://github.com/Microsoft/mssql-jdbc/pull/347) +- Get local host name before opening TDSChannel [#324](https://github.com/Microsoft/mssql-jdbc/pull/324) +- Fixed TVP Time issue [#317](https://github.com/Microsoft/mssql-jdbc/pull/317) +- Fixed SonarQube issues [#300](https://github.com/Microsoft/mssql-jdbc/pull/300) +- Fixed SonarQube issues [#301](https://github.com/Microsoft/mssql-jdbc/pull/301) +- Fixed random TDS invalid error [#310](https://github.com/Microsoft/mssql-jdbc/pull/310) +- Fixed password logging [#298](https://github.com/Microsoft/mssql-jdbc/pull/298) +- Fixed bulkcopy cursor issue [#270](https://github.com/Microsoft/mssql-jdbc/pull/270) + +### Changed +- Refresh Kerberos configuration [#279](https://github.com/Microsoft/mssql-jdbc/pull/279) + +## [6.1.7] Preview Release +### Added +- Added support for data type LONGVARCHAR, LONGNVARCHAR, LONGVARBINARY and SQLXML in TVP [#259](https://github.com/Microsoft/mssql-jdbc/pull/259) +- Added new connection property to accept custom JAAS configuration for Kerberos [#254](https://github.com/Microsoft/mssql-jdbc/pull/254) +- Added support for server cursor with TVP [#234](https://github.com/Microsoft/mssql-jdbc/pull/234) +- Experimental Feature: Added new connection property to support network timeout [#253](https://github.com/Microsoft/mssql-jdbc/pull/253) +- Added support to authenticate Kerberos with principal and password [#163](https://github.com/Microsoft/mssql-jdbc/pull/163) +- Added temporal types to BulkCopyCSVTestInput.csv [#262](https://github.com/Microsoft/mssql-jdbc/pull/262) +- Added automatic detection of REALM in SPN needed for Cross Domain authentication [#40](https://github.com/Microsoft/mssql-jdbc/pull/40) + +### Changed +- Updated minor semantics [#232](https://github.com/Microsoft/mssql-jdbc/pull/232) +- Cleaned up Azure Active Directory (AAD) Authentication methods [#256](https://github.com/Microsoft/mssql-jdbc/pull/256) +- Updated permission check before setting network timeout [#255](https://github.com/Microsoft/mssql-jdbc/pull/255) + +### Fixed Issues +- Turn TNIR (TransparentNetworkIPResolution) off for Azure Active Directory (AAD) Authentication and changed TNIR multipliers [#240](https://github.com/Microsoft/mssql-jdbc/pull/240) +- Wrapped ClassCastException in BulkCopy with SQLServerException [#260](https://github.com/Microsoft/mssql-jdbc/pull/260) +- Initialized the XA transaction manager for each XAResource [#257](https://github.com/Microsoft/mssql-jdbc/pull/257) +- Fixed BigDecimal scale rounding issue in BulkCopy [#230](https://github.com/Microsoft/mssql-jdbc/issues/230) +- Fixed the invalid exception thrown when stored procedure does not exist is used with TVP [#265](https://github.com/Microsoft/mssql-jdbc/pull/265) + +## [6.1.6] Preview Release +### Added +- Added constrained delegation to connection sample [#188](https://github.com/Microsoft/mssql-jdbc/pull/188) +- Added snapshot to identify nightly/dev builds [#221](https://github.com/Microsoft/mssql-jdbc/pull/221) +- Clarifying public deprecated constructors in LOBs [#226](https://github.com/Microsoft/mssql-jdbc/pull/226) +- Added OSGI Headers in MANIFEST.MF [#218](https://github.com/Microsoft/mssql-jdbc/pull/218) +- Added cause to SQLServerException [#202](https://github.com/Microsoft/mssql-jdbc/pull/202) + +### Changed +- Removed java.io.Serializable interface from SQLServerConnectionPoolProxy [#201](https://github.com/Microsoft/mssql-jdbc/pull/201) +- Refactored DROP TABLE and DROP PROCEDURE calls in test code [#222](https://github.com/Microsoft/mssql-jdbc/pull/222/files) +- Removed obsolete methods from DriverJDBCVersion [#187](https://github.com/Microsoft/mssql-jdbc/pull/187) + +### Fixed Issues +- Typos in SQLServerConnectionPoolProxy [#189](https://github.com/Microsoft/mssql-jdbc/pull/189) +- Fixed issue where exceptions are thrown if comments are in a SQL string [#157](https://github.com/Microsoft/mssql-jdbc/issues/157) +- Fixed test failures on pre-2016 servers [#215](https://github.com/Microsoft/mssql-jdbc/pull/215) +- Fixed SQLServerExceptions that are wrapped by another SQLServerException [#213](https://github.com/Microsoft/mssql-jdbc/pull/213) +- Fixed a stream isClosed error on LOBs test [#233](https://github.com/Microsoft/mssql-jdbc/pull/223) +- LOBs are fully materialised [#16](https://github.com/Microsoft/mssql-jdbc/issues/16) +- Fix precision issue in TVP [#217](https://github.com/Microsoft/mssql-jdbc/pull/217) +- Re-interrupt the current thread in order to restore the threads interrupt status [#196](https://github.com/Microsoft/mssql-jdbc/issues/196) +- Re-use parameter metadata when using Always Encrypted [#195](https://github.com/Microsoft/mssql-jdbc/issues/195) +- Improved performance for PreparedStatements through minimized server round-trips [#166](https://github.com/Microsoft/mssql-jdbc/issues/166) + +## [6.1.5] Preview Release ### Added - Added socket timeout exception as cause[#180](https://github.com/Microsoft/mssql-jdbc/pull/180) - Added Constrained delegation support[#178](https://github.com/Microsoft/mssql-jdbc/pull/178) @@ -21,7 +184,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) - Fixed local test failures [#179](https://github.com/Microsoft/mssql-jdbc/pull/179) - Fixed random failure in BulkCopyColumnMapping test[#165](https://github.com/Microsoft/mssql-jdbc/pull/165) -## [6.1.4] +## [6.1.4] Preview Release ### Added - Added isWrapperFor methods for MetaData classes[#94](https://github.com/Microsoft/mssql-jdbc/pull/94) - Added Code Coverage [#136](https://github.com/Microsoft/mssql-jdbc/pull/136) @@ -40,7 +203,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) - Fixed an issue of Bulk Copy when AlwaysEncrypted is enabled on connection and destination table is not encrypted [#151](https://github.com/Microsoft/mssql-jdbc/pull/151) -## [6.1.3] +## [6.1.3] Preview Release ### Added - Added Binary and Varbinary types to the jUnit test framework [#119](https://github.com/Microsoft/mssql-jdbc/pull/119) - Added BulkCopy test cases for csv [#123](https://github.com/Microsoft/mssql-jdbc/pull/123) @@ -57,7 +220,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) - Fixed NullPointerException in case when SocketTimeout occurs [#65](https://github.com/Microsoft/mssql-jdbc/issues/121) -## [6.1.2] +## [6.1.2] Preview Release ### Added - Socket timeout implementation for both connection string and data source [#85](https://github.com/Microsoft/mssql-jdbc/pull/85) - Query timeout API for datasource [#88](https://github.com/Microsoft/mssql-jdbc/pull/88) @@ -80,7 +243,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) - Fixed the connection close issue on using variant type [#91] (https://github.com/Microsoft/mssql-jdbc/issues/91) -## [6.1.1] +## [6.1.1] Preview Release ### Added - Java Docs [#46](https://github.com/Microsoft/mssql-jdbc/pull/46) - Driver version number in LOGIN7 packet [#43](https://github.com/Microsoft/mssql-jdbc/pull/43) @@ -103,6 +266,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) - Update Maven Plugin [#55](https://github.com/Microsoft/mssql-jdbc/pull/55) -## [6.1.0] +## [6.1.0] Stable Release ### Changed - Open Sourced. diff --git a/README.md b/README.md index bf205814e..1edeca9e1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/Microsoft/mssql-jdbc/master/LICENSE) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.microsoft.sqlserver/mssql-jdbc/badge.svg)](http://mvnrepository.com/artifact/com.microsoft.sqlserver/mssql-jdbc) +[![codecov.io](http://codecov.io/github/Microsoft/mssql-jdbc/coverage.svg?branch=master)](http://codecov.io/github/Microsoft/mssql-jdbc?branch=master) [![Javadocs](http://javadoc.io/badge/com.microsoft.sqlserver/mssql-jdbc.svg)](http://javadoc.io/doc/com.microsoft.sqlserver/mssql-jdbc) [![Gitter](https://img.shields.io/gitter/room/badges/shields.svg)](https://gitter.im/Microsoft/mssql-developers)
@@ -13,35 +14,36 @@ We hope you enjoy using the Microsoft JDBC Driver for SQL Server. SQL Server Team +## Take our survey + +Let us know how you think we're doing. + + + ## Status of Most Recent Builds | AppVeyor (Windows) | Travis CI (Linux) | |--------------------------|--------------------------| -| [![av-image][]][av-site] | [![tv-image][]][tv-site] | - -[av-image]: https://ci.appveyor.com/api/projects/status/o6fjg16678ol64d3?svg=true "Windows" -[av-site]: https://ci.appveyor.com/project/Microsoft-JDBC/mssql-jdbc -[tv-image]: https://travis-ci.org/Microsoft/mssql-jdbc.svg? "Linux" -[tv-site]: https://travis-ci.org/Microsoft/mssql-jdbc +| [![AppVeyor ](https://ci.appveyor.com/api/projects/status/o6fjg16678ol64d3?svg=true "Windows")](https://ci.appveyor.com/project/Microsoft-JDBC/mssql-jdbc) | [![Travis CI](https://travis-ci.org/Microsoft/mssql-jdbc.svg? "Linux")](https://travis-ci.org/Microsoft/mssql-jdbc ) |vg? "Linux" ## Announcements What's coming next? We will look into adding a more comprehensive set of tests, improving our javadocs, and start developing the next set of features. ## Get Started -* [**Ubuntu + SQL Server + Java**](https://www.microsoft.com/en-us/sql-server/developer-get-started/java-ubuntu) -* [**Red Hat + SQL Server + Java**](https://www.microsoft.com/en-us/sql-server/developer-get-started/java-rhel) -* [**Mac + SQL Server + Java**](https://www.microsoft.com/en-us/sql-server/developer-get-started/java-mac) -* [**Windows + SQL Server + Java**](https://www.microsoft.com/en-us/sql-server/developer-get-started/java-windows) +* [**Ubuntu + SQL Server + Java**](https://www.microsoft.com/en-us/sql-server/developer-get-started/java/ubuntu) +* [**Red Hat + SQL Server + Java**](https://www.microsoft.com/en-us/sql-server/developer-get-started/java/rhel) +* [**Mac + SQL Server + Java**](https://www.microsoft.com/en-us/sql-server/developer-get-started/java/mac) +* [**Windows + SQL Server + Java**](https://www.microsoft.com/en-us/sql-server/developer-get-started/java/windows) ## Build ### Prerequisites * Java 8 -* [Maven](http://maven.apache.org/download.cgi) or [Gradle](https://gradle.org/gradle-download/) +* [Maven](http://maven.apache.org/download.cgi) * An instance of SQL Server or Azure SQL Database that you can connect to. ### Build the JAR files -Maven and Gradle builds automatically trigger a set of verification tests to run. For these tests to pass, you will first need to add an environment variable in your system called `mssql_jdbc_test_connection_properties` to provide the [correct connection properties](https://msdn.microsoft.com/en-us/library/ms378428(v=sql.110).aspx) for your SQL Server or Azure SQL Database instance. +Maven builds automatically trigger a set of verification tests to run. For these tests to pass, you will first need to add an environment variable in your system called `mssql_jdbc_test_connection_properties` to provide the [correct connection properties](https://msdn.microsoft.com/en-us/library/ms378428(v=sql.110).aspx) for your SQL Server or Azure SQL Database instance. -To build the jar files, you must use Java 8 with either Maven or Gradle. You can choose to build a JDBC 4.1 compliant jar file (for use with JRE 7) and/or a JDBC 4.2 compliant jar file (for use with JRE 8). +To build the jar files, you must use Java 8 with Maven. You can choose to build a JDBC 4.1 compliant jar file (for use with JRE 7) and/or a JDBC 4.2 compliant jar file (for use with JRE 8). * Maven: 1. If you have not already done so, add the environment variable `mssql_jdbc_test_connection_properties` in your system with the connection properties for your SQL Server or SQL DB instance. @@ -49,6 +51,8 @@ To build the jar files, you must use Java 8 with either Maven or Gradle. You ca * Run `mvn install -Pbuild41`. This creates JDBC 4.1 compliant jar in \target directory * Run `mvn install -Pbuild42`. This creates JDBC 4.2 compliant jar in \target directory +**NOTE**: Beginning release v6.1.7, we will no longer be maintaining the existing [Gradle build script](build.gradle) and it will be left in the repository for reference. Please refer to issue [#62](https://github.com/Microsoft/mssql-jdbc/issues/62) for this decision. + * Gradle: 1. If you have not already done so, add the environment variable `mssql_jdbc_test_connection_properties` in your system with the connection properties for your SQL Server or SQL DB instance. 2. Run one of the commands below to build a JDBC 4.1 compliant jar or JDBC 4.2 compliant jar in the \build\libs directory. @@ -58,30 +62,41 @@ To build the jar files, you must use Java 8 with either Maven or Gradle. You ca ## Resources ### Documentation +API reference documentation is available in [Javadocs](https://aka.ms/jdbcjavadocs). + This driver is documented on [Microsoft's Documentation web site](https://msdn.microsoft.com/en-us/library/mt720657). ### Sample Code For samples, please see the src\sample directory. ### Download the DLLs -For some features (e.g. Integrated Authentication and Distributed Transactions), you may need to use the `sqljdbc_xa` and `sqljdbc_auth` DLLs. They can be downloaded from the [Microsoft Download Center](https://www.microsoft.com/en-us/download/details.aspx?displaylang=en&id=11774) +For some features (e.g. Integrated Authentication and Distributed Transactions), you may need to use the `sqljdbc_xa` and `sqljdbc_auth` DLLs. They can be downloaded from the [Microsoft Download Center](https://go.microsoft.com/fwlink/?linkid=852460) ### Download the driver Don't want to compile anything? -We're now on the Maven Central Repository. Add the following to your POM file: - +We're now on the Maven Central Repository. Add the following to your POM file to get the most stable release: +```xml + + com.microsoft.sqlserver + mssql-jdbc + 6.2.2.jre8 + ``` +The driver can be downloaded from the [Microsoft Download Center](https://go.microsoft.com/fwlink/?linkid=852460). + +To get the latest preview version of the driver, add the following to your POM file: +```xml com.microsoft.sqlserver mssql-jdbc - 6.1.0.jre8 + 6.3.4.jre8-preview ``` -The driver can be downloaded from the [Microsoft Download Center](https://www.microsoft.com/en-us/download/details.aspx?displaylang=en&id=11774) -##Dependencies + +## Dependencies This project has following dependencies: Compile Time: @@ -91,7 +106,7 @@ Compile Time: Test Time: - `junit:jar` : For Unit Test cases. -###Dependency Tree +### Dependency Tree One can see all dependencies including Transitive Dependency by executing following command. ``` mvn dependency:tree @@ -101,20 +116,21 @@ mvn dependency:tree Projects that require either of the two features need to explicitly declare the dependency in their pom file. ***For Example:*** If you are using *Azure Key Vault feature* then you need to redeclare *azure-keyvault* dependency in your project's pom file. Please see the following snippet: -``` +```xml com.microsoft.sqlserver mssql-jdbc - 6.1.0.jre8 + 6.3.4.jre8-preview compile com.microsoft.azure azure-keyvault - 0.9.7 + 1.0.0 ``` +***Please note*** as of the v6.2.2, the way to construct a `SQLServerColumnEncryptionAzureKeyVaultProvider` object has changed. Please refer to this [Wiki](https://github.com/Microsoft/mssql-jdbc/wiki/New-Constructor-Definition-for-SQLServerColumnEncryptionAzureKeyVaultProvider-after-6.2.2-Release) page for more information. ## Guidelines for Creating Pull Requests We love contributions from the community. To help improve the quality of our code, we encourage you to use the mssql-jdbc_formatter.xml formatter provided on all pull requests. @@ -127,7 +143,7 @@ We appreciate you taking the time to test the driver, provide feedback and repor - Report each issue as a new issue (but check first if it's already been reported) - Try to be detailed in your report. Useful information for good bug reports include: * What you are seeing and what the expected behaviour is - * Which jar file? + * Which jar file? * Environment details: e.g. Java version, client operating system? * Table schema (for some issues the data types make a big difference!) * Any other relevant information you want to share @@ -138,11 +154,25 @@ Thank you! ### Reporting security issues and security bugs Security issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC) [secure@microsoft.com](mailto:secure@microsoft.com). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Further information, including the MSRC PGP key, can be found in the [Security TechCenter](https://technet.microsoft.com/en-us/security/ff852094.aspx). +## Contributors +Special thanks to everyone who has contributed to the project. -## License -The Microsoft JDBC Driver for SQL Server is licensed under the MIT license. See the [LICENSE](https://github.com/Microsoft/mssql-jdbc/blob/master/LICENSE) file for more details. +Up-to-date list of contributors: https://github.com/Microsoft/mssql-jdbc/graphs/contributors +- marschall (Philippe Marschall) +- pierresouchay (Pierre Souchay) +- gordthompson (Gord Thompson) +- gstojsic +- cosmofrit +- JamieMagee (Jamie Magee) +- mfriesen (Mike Friesen) +- tonytamwk +- sehrope (Sehrope Sarkuni) +- jacobovazquez +- brettwooldridge (Brett Wooldridge) +## License +The Microsoft JDBC Driver for SQL Server is licensed under the MIT license. See the [LICENSE](https://github.com/Microsoft/mssql-jdbc/blob/master/LICENSE) file for more details. ## Code of conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/appveyor.yml b/appveyor.yml index 9c5bb3f98..5d0ba1f95 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,10 +7,31 @@ environment: services: - mssql2016 + +install: + - ps: Write-Host 'Installing JCE with powershell' + - ps: cd AppVeyorJCE + - ps: choco pack + - ps: choco install jce -fdv -s . -y -failonstderr + - ps: cd.. + - ps: mkdir AE_Certificates + - ps: cd AE_Certificates + - ps: $cert = New-SelfSignedCertificate -dns "AlwaysEncryptedCert" -CertStoreLocation Cert:CurrentUser\My + - ps: $pwd = ConvertTo-SecureString -String "password" -Force -AsPlainText + - ps: $path = 'cert:\CurrentUser\My\' + $cert.thumbprint + - ps: $certificate = Export-PfxCertificate -cert $path -FilePath cert.pfx -Password $pwd + - ps: Get-ChildItem -path cert:\CurrentUser\My > certificate.txt + +cache: + - C:\Users\appveyor\.m2 -> pom.xml build_script: + - keytool -importkeystore -srckeystore cert.pfx -srcstoretype pkcs12 -destkeystore clientcert.jks -deststoretype JKS -srcstorepass password -deststorepass password + - keytool -list -v -keystore clientcert.jks -storepass "password" > JavaKeyStore.txt + - cd.. - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V -Pbuild41 - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V -Pbuild42 + test_script: - mvn test -B -Pbuild41 - - mvn test -B -Pbuild42 \ No newline at end of file + - mvn test -B -Pbuild42 diff --git a/build.gradle b/build.gradle index 922eee4ee..5cc29b949 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'java' archivesBaseName = 'mssql-jdbc' -version = '6.1.5' +version = '6.1.6' allprojects { tasks.withType(JavaCompile) { @@ -66,9 +66,9 @@ repositories { } dependencies { - compile 'com.microsoft.azure:azure-keyvault:0.9.7', + compile 'com.microsoft.azure:azure-keyvault:1.0.0', 'com.microsoft.azure:adal4j:1.1.3' - + testCompile 'junit:junit:4.12', 'org.junit.platform:junit-platform-console:1.0.0-M3', 'org.junit.platform:junit-platform-commons:1.0.0-M3', @@ -77,5 +77,7 @@ dependencies { 'org.junit.platform:junit-platform-runner:1.0.0-M3', 'org.junit.platform:junit-platform-surefire-provider:1.0.0-M3', 'org.junit.jupiter:junit-jupiter-api:5.0.0-M3', - 'org.junit.jupiter:junit-jupiter-engine:5.0.0-M3' -} + 'org.junit.jupiter:junit-jupiter-engine:5.0.0-M3', + 'com.zaxxer:HikariCP:2.6.0', + 'org.apache.commons:commons-dbcp2:2.1.1' +} \ No newline at end of file diff --git a/issue_template.md b/issue_template.md new file mode 100644 index 000000000..4247b959e --- /dev/null +++ b/issue_template.md @@ -0,0 +1,23 @@ +## Driver version or jar name +Please tell us what the JDBC driver version or jar name is. + +## SQL Server version +Please tell us what the SQL Server version is. + +## Client operating system +Please tell us what oprating system the client program is running on. + +## Java/JVM version +Example: java version "1.8.0", IBM J9 VM + +## Table schema +Please tell us the table schema + +## Problem description +Please share more details with us. + +## Expected behavior and actual behavior +Please tell us what should happen and what happened instead + +## Repro code +Please share repro code with us, or tell us how to reproduce the issue. diff --git a/pom.xml b/pom.xml index ebb59ff85..a8ccbadda 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.microsoft.sqlserver mssql-jdbc - 6.1.5 + 6.3.5-SNAPSHOT.${jreVersion}-preview jar @@ -29,8 +29,6 @@ - Andrea Lam - andrela@microsoft.com Microsoft http://www.microsoft.com @@ -42,20 +40,22 @@ UTF-8 + 1.0.0-M3 + 5.0.0-M3 com.microsoft.azure azure-keyvault - 0.9.7 + 1.0.0 true com.microsoft.azure adal4j - 1.1.3 + 1.2.0 true @@ -70,55 +70,55 @@ org.junit.platform junit-platform-console - 1.0.0-M3 + ${junit.platform.version} test org.junit.platform junit-platform-commons - 1.0.0-M3 + ${junit.platform.version} test org.junit.platform junit-platform-engine - 1.0.0-M3 + ${junit.platform.version} test org.junit.platform junit-platform-launcher - 1.0.0-M3 + ${junit.platform.version} test org.junit.platform junit-platform-runner - 1.0.0-M3 + ${junit.platform.version} test org.junit.platform junit-platform-surefire-provider - 1.0.0-M3 + ${junit.platform.version} test org.junit.jupiter junit-jupiter-api - 5.0.0-M3 + ${junit.jupiter.version} test org.junit.jupiter junit-jupiter-engine - 5.0.0-M3 + ${junit.jupiter.version} test com.zaxxer HikariCP - 2.6.0 + 2.6.1 test @@ -140,6 +140,11 @@ build41 + + + jre7 + + @@ -158,11 +163,8 @@ maven-jar-plugin 3.0.2 - ${project.artifactId}-${project.version}.jre7 - - true - + ${project.build.outputDirectory}/META-INF/MANIFEST.MF @@ -173,9 +175,15 @@ build42 + true + + + jre8 + + @@ -194,11 +202,8 @@ maven-jar-plugin 3.0.2 - ${project.artifactId}-${project.version}.jre8 - - true - + ${project.build.outputDirectory}/META-INF/MANIFEST.MF @@ -226,6 +231,13 @@ **/*.csv + + AE_Certificates + + **/*.txt + **/*.jks + + @@ -263,7 +275,69 @@ + + + org.apache.felix + maven-bundle-plugin + 3.2.0 + true + + + com.microsoft.sqlserver.jdbc,microsoft.sql + !microsoft.sql,* + + + + + bundle-manifest + process-classes + + manifest + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.4 + + + true + + + + + attach-javadocs + + jar + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.19 + + + + ${skipTestTag} + + + + + + org.junit.platform + junit-platform-surefire-provider + ${junit.platform.version} + + + + + diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/AE.java b/src/main/java/com/microsoft/sqlserver/jdbc/AE.java index 6a4ddba13..a59a39f98 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/AE.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/AE.java @@ -8,7 +8,8 @@ package com.microsoft.sqlserver.jdbc; -import java.util.Vector; +import java.util.ArrayList; +import java.util.List; /** * Represents a single encrypted value for a CEK. It contains the encrypted CEK,the store type, name,the key path and encryption algorithm. @@ -45,19 +46,19 @@ class EncryptionKeyInfo { /** * Represents a unique CEK as an entry in the CekTable. A unique (plaintext is unique) CEK can have multiple encrypted CEKs when using multiple CMKs. - * These encrypted CEKs are represented by a member vector. + * These encrypted CEKs are represented by a member ArrayList. */ class CekTableEntry { static final private java.util.logging.Logger aeLogger = java.util.logging.Logger.getLogger("com.microsoft.sqlserver.jdbc.AE"); - Vector columnEncryptionKeyValues; + List columnEncryptionKeyValues; int ordinal; int databaseId; int cekId; int cekVersion; byte[] cekMdVersion; - Vector getColumnEncryptionKeyValues() { + List getColumnEncryptionKeyValues() { return columnEncryptionKeyValues; } @@ -87,7 +88,7 @@ byte[] getCekMdVersion() { cekId = 0; cekVersion = 0; cekMdVersion = null; - columnEncryptionKeyValues = new Vector(); + columnEncryptionKeyValues = new ArrayList<>(); } int getSize() { @@ -236,7 +237,7 @@ short getOrdinal() { } boolean IsAlgorithmInitialized() { - return (null != cipherAlgorithm) ? true : false; + return null != cipherAlgorithm; } } @@ -254,17 +255,9 @@ enum DescribeParameterEncryptionResultSet1 { KeyPath, KeyEncryptionAlgorithm; - private int value; - - // Column indexing starts from 1; - static { - for (int i = 0; i < values().length; ++i) { - values()[i].value = i + 1; - } - } - int value() { - return value; + // Column indexing starts from 1; + return ordinal() + 1; } } @@ -279,17 +272,9 @@ enum DescribeParameterEncryptionResultSet2 { ColumnEncryptionKeyOrdinal, NormalizationRuleVersion; - private int value; - - // Column indexing starts from 1; - static { - for (int i = 0; i < values().length; ++i) { - values()[i].value = i + 1; - } - } - int value() { - return value; + // Column indexing starts from 1; + return ordinal() + 1; } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ActivityCorrelator.java b/src/main/java/com/microsoft/sqlserver/jdbc/ActivityCorrelator.java index 422ff7ca3..a036b5be6 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ActivityCorrelator.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ActivityCorrelator.java @@ -8,32 +8,42 @@ package com.microsoft.sqlserver.jdbc; +import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; /** * ActivityCorrelator provides the APIs to access the ActivityId in TLS */ final class ActivityCorrelator { - private static ThreadLocal ActivityIdTls = new ThreadLocal() { - protected ActivityId initialValue() { - return new ActivityId(); + private static Map ActivityIdTlsMap = new ConcurrentHashMap(); + + static void cleanupActivityId() { + //remove the ActivityId that belongs to this thread. + long uniqueThreadId = Thread.currentThread().getId(); + + if (ActivityIdTlsMap.containsKey(uniqueThreadId)) { + ActivityIdTlsMap.remove(uniqueThreadId); } - }; + } // Get the current ActivityId in TLS static ActivityId getCurrent() { // get the value in TLS, not reference - return ActivityIdTls.get(); + long uniqueThreadId = Thread.currentThread().getId(); + + //Since the Id for each thread is unique, this assures that the below if statement is run only once per thread. + if (!ActivityIdTlsMap.containsKey(uniqueThreadId)) { + ActivityIdTlsMap.put(uniqueThreadId, new ActivityId()); + } + + return ActivityIdTlsMap.get(uniqueThreadId); } // Increment the Sequence number of the ActivityId in TLS // and return the ActivityId with new Sequence number static ActivityId getNext() { - // We need to call get() method on ThreadLocal to get - // the current value of ActivityId stored in TLS, - // then increment the sequence number. - // Get the current ActivityId in TLS ActivityId activityId = getCurrent(); @@ -47,7 +57,6 @@ static void setCurrentActivityIdSentFlag() { ActivityId activityId = getCurrent(); activityId.setSentFlag(); } - } class ActivityId { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/AuthenticationJNI.java b/src/main/java/com/microsoft/sqlserver/jdbc/AuthenticationJNI.java index 705d10ce0..33ccc75b4 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/AuthenticationJNI.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/AuthenticationJNI.java @@ -134,18 +134,6 @@ static FedAuthDllInfo getAccessTokenForWindowsIntegrated(String stsURL, return dllInfo; } - static FedAuthDllInfo getAccessToken(String userName, - String password, - String stsURL, - String servicePrincipalName, - String clientConnectionId, - String clientId, - long expirationFileTime) throws DLLException { - FedAuthDllInfo dllInfo = ADALGetAccessToken(userName, password, stsURL, servicePrincipalName, clientConnectionId, clientId, - expirationFileTime, authLogger); - return dllInfo; - } - // InitDNSName should be called to initialize the DNSName before calling this function byte[] GenerateClientContext(byte[] pin, boolean[] done) throws SQLServerException { @@ -162,7 +150,9 @@ byte[] GenerateClientContext(byte[] pin, threadImpersonationToken, threadUseProcessToken, authLogger); if (failure != 0) { - authLogger.warning(toString() + " Authentication failed code : " + failure); + if (authLogger.isLoggable(Level.WARNING)) { + authLogger.warning(toString() + " Authentication failed code : " + failure); + } con.terminate(SQLServerException.DRIVER_ERROR_NONE, SQLServerException.getErrString("R_integratedAuthenticationFailed"), linkError); } // allocate space based on the size returned @@ -248,15 +238,6 @@ private native static FedAuthDllInfo ADALGetAccessTokenForWindowsIntegrated(Stri long expirationFileTime, java.util.logging.Logger log); - private native static FedAuthDllInfo ADALGetAccessToken(String userName, - String password, - String stsURL, - String servicePrincipalName, - String clientConnectionId, - String clientId, - long expirationFileTime, - java.util.logging.Logger log); - native static byte[] DecryptColumnEncryptionKey(String masterKeyPath, String encryptionAlgorithm, byte[] encryptedColumnEncryptionKey) throws DLLException; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Column.java b/src/main/java/com/microsoft/sqlserver/jdbc/Column.java index 06eaf1fb3..d6d8017bf 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Column.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Column.java @@ -18,7 +18,16 @@ final class Column { private TypeInfo typeInfo; private CryptoMetadata cryptoMetadata; - + private SqlVariant internalVariant; + + final void setInternalVariant(SqlVariant type){ + this.internalVariant = type; + } + + final SqlVariant getInternalVariant(){ + return this.internalVariant; + } + final TypeInfo getTypeInfo() { return typeInfo; } @@ -187,11 +196,12 @@ Object getValue(JDBCType jdbcType, Calendar cal, TDSReader tdsReader) throws SQLServerException { Object value = getterDTV.getValue(jdbcType, typeInfo.getScale(), getterArgs, cal, typeInfo, cryptoMetadata, tdsReader); + setInternalVariant(getterDTV.getInternalVariant()); return (null != filter) ? filter.apply(value, jdbcType) : value; } int getInt(TDSReader tdsReader) throws SQLServerException { - return ((Integer) getValue(JDBCType.INTEGER, null, null, tdsReader)).intValue(); + return (Integer) getValue(JDBCType.INTEGER, null, null, tdsReader); } void updateValue(JDBCType jdbcType, @@ -327,7 +337,7 @@ else if (jdbcType.isBinary()) { // to the server as Unicode rather than MBCS. This is accomplished here by re-tagging // the value with the appropriate corresponding Unicode type. if ((null != cryptoMetadata) && (con.sendStringParametersAsUnicode()) - && (JavaType.STRING == javaType || JavaType.READER == javaType || JavaType.CLOB == javaType)) { + && (JavaType.STRING == javaType || JavaType.READER == javaType || JavaType.CLOB == javaType || JavaType.OBJECT == javaType)) { jdbcType = getSSPAUJDBCType(jdbcType); } @@ -415,6 +425,7 @@ else if (SSType.SMALLDATETIME == basicSSType) return JDBCType.GUID; if (SSType.VARCHARMAX == basicSSType) return JDBCType.LONGVARCHAR; + return jdbcType; default: return jdbcType; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java b/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java index 8010c1b32..77891c37b 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java @@ -54,15 +54,15 @@ static final Object convertIntegerToObject(int intValue, StreamType streamType) { switch (jdbcType) { case INTEGER: - return new Integer(intValue); + return intValue; case SMALLINT: // 2.21 small and tinyint returned as short case TINYINT: - return new Short((short) intValue); + return (short) intValue; case BIT: case BOOLEAN: - return new Boolean(0 != intValue); + return 0 != intValue; case BIGINT: - return new Long(intValue); + return (long) intValue; case DECIMAL: case NUMERIC: case MONEY: @@ -70,9 +70,9 @@ static final Object convertIntegerToObject(int intValue, return new BigDecimal(Integer.toString(intValue)); case FLOAT: case DOUBLE: - return new Double(intValue); + return (double) intValue; case REAL: - return new Float(intValue); + return (float) intValue; case BINARY: return convertIntToBytes(intValue, valueLength); default: @@ -99,15 +99,15 @@ static final Object convertLongToObject(long longVal, StreamType streamType) { switch (jdbcType) { case BIGINT: - return new Long(longVal); + return longVal; case INTEGER: - return new Integer((int) longVal); + return (int) longVal; case SMALLINT: // small and tinyint returned as short case TINYINT: - return new Short((short) longVal); + return (short) longVal; case BIT: case BOOLEAN: - return new Boolean(0 != longVal); + return 0 != longVal; case DECIMAL: case NUMERIC: case MONEY: @@ -115,12 +115,12 @@ static final Object convertLongToObject(long longVal, return new BigDecimal(Long.toString(longVal)); case FLOAT: case DOUBLE: - return new Double(longVal); + return (double) longVal; case REAL: - return new Float(longVal); + return (float) longVal; case BINARY: byte[] convertedBytes = convertLongToBytes(longVal); - int bytesToReturnLength = 0; + int bytesToReturnLength; byte[] bytesToReturn; switch (baseSSType) { @@ -152,23 +152,23 @@ static final Object convertLongToObject(long longVal, case VARBINARY: switch (baseSSType) { case BIGINT: - return new Long(longVal); + return longVal; case INTEGER: - return new Integer((int) longVal); + return (int) longVal; case SMALLINT: // small and tinyint returned as short case TINYINT: - return new Short((short) longVal); + return (short) longVal; case BIT: - return new Boolean(0 != longVal); + return 0 != longVal; case DECIMAL: case NUMERIC: case MONEY: case SMALLMONEY: return new BigDecimal(Long.toString(longVal)); case FLOAT: - return new Double(longVal); + return (double) longVal; case REAL: - return new Float(longVal); + return (float) longVal; case BINARY: return convertLongToBytes(longVal); default: @@ -214,17 +214,17 @@ static final Object convertFloatToObject(float floatVal, StreamType streamType) { switch (jdbcType) { case REAL: - return new Float(floatVal); + return floatVal; case INTEGER: - return new Integer((int) floatVal); + return (int) floatVal; case SMALLINT: // small and tinyint returned as short case TINYINT: - return new Short((short) floatVal); + return (short) floatVal; case BIT: case BOOLEAN: - return new Boolean(0 != Float.compare(0.0f, floatVal)); + return 0 != Float.compare(0.0f, floatVal); case BIGINT: - return new Long((long) floatVal); + return (long) floatVal; case DECIMAL: case NUMERIC: case MONEY: @@ -232,7 +232,7 @@ static final Object convertFloatToObject(float floatVal, return new BigDecimal(Float.toString(floatVal)); case FLOAT: case DOUBLE: - return new Double((new Float(floatVal)).doubleValue()); + return (new Float(floatVal)).doubleValue(); case BINARY: return convertIntToBytes(Float.floatToRawIntBits(floatVal), 4); default: @@ -273,19 +273,19 @@ static final Object convertDoubleToObject(double doubleVal, switch (jdbcType) { case FLOAT: case DOUBLE: - return new Double(doubleVal); + return doubleVal; case REAL: - return new Float((new Double(doubleVal)).floatValue()); + return (new Double(doubleVal)).floatValue(); case INTEGER: - return new Integer((int) doubleVal); + return (int) doubleVal; case SMALLINT: // small and tinyint returned as short case TINYINT: - return new Short((short) doubleVal); + return (short) doubleVal; case BIT: case BOOLEAN: - return new Boolean(0 != Double.compare(0.0d, doubleVal)); + return 0 != Double.compare(0.0d, doubleVal); case BIGINT: - return new Long((long) doubleVal); + return (long) doubleVal; case DECIMAL: case NUMERIC: case MONEY: @@ -355,19 +355,19 @@ static final Object convertBigDecimalToObject(BigDecimal bigDecimalVal, return bigDecimalVal; case FLOAT: case DOUBLE: - return new Double(bigDecimalVal.doubleValue()); + return bigDecimalVal.doubleValue(); case REAL: - return new Float(bigDecimalVal.floatValue()); + return bigDecimalVal.floatValue(); case INTEGER: - return new Integer(bigDecimalVal.intValue()); + return bigDecimalVal.intValue(); case SMALLINT: // small and tinyint returned as short case TINYINT: - return new Short(bigDecimalVal.shortValue()); + return bigDecimalVal.shortValue(); case BIT: case BOOLEAN: - return new Boolean(0 != bigDecimalVal.compareTo(BigDecimal.valueOf(0))); + return 0 != bigDecimalVal.compareTo(BigDecimal.valueOf(0)); case BIGINT: - return new Long(bigDecimalVal.longValue()); + return bigDecimalVal.longValue(); case BINARY: return convertBigDecimalToBytes(bigDecimalVal, bigDecimalVal.scale()); default: @@ -400,19 +400,19 @@ static final Object convertMoneyToObject(BigDecimal bigDecimalVal, return bigDecimalVal; case FLOAT: case DOUBLE: - return new Double(bigDecimalVal.doubleValue()); + return bigDecimalVal.doubleValue(); case REAL: - return new Float(bigDecimalVal.floatValue()); + return bigDecimalVal.floatValue(); case INTEGER: - return new Integer(bigDecimalVal.intValue()); + return bigDecimalVal.intValue(); case SMALLINT: // small and tinyint returned as short case TINYINT: - return new Short(bigDecimalVal.shortValue()); + return bigDecimalVal.shortValue(); case BIT: case BOOLEAN: - return new Boolean(0 != bigDecimalVal.compareTo(BigDecimal.valueOf(0))); + return 0 != bigDecimalVal.compareTo(BigDecimal.valueOf(0)); case BIGINT: - return new Long(bigDecimalVal.longValue()); + return bigDecimalVal.longValue(); case BINARY: return convertToBytes(bigDecimalVal, bigDecimalVal.scale(), numberOfBytes); default: @@ -439,9 +439,7 @@ private static byte[] convertToBytes(BigDecimal value, } } int offset = numBytes - unscaledBytes.length; - for (int i = offset; i < numBytes; ++i) { - ret[i] = unscaledBytes[i - offset]; - } + System.arraycopy(unscaledBytes, offset - offset, ret, offset, numBytes - offset); return ret; } @@ -467,7 +465,7 @@ static final Object convertBytesToObject(byte[] bytesValue, if ((SSType.BINARY == baseTypeInfo.getSSType()) && (str.length() < (baseTypeInfo.getPrecision() * 2))) { - StringBuffer strbuf = new StringBuffer(str); + StringBuilder strbuf = new StringBuilder(str); while (strbuf.length() < (baseTypeInfo.getPrecision() * 2)) { strbuf.append('0'); @@ -781,7 +779,7 @@ static final Object convertTemporalToObject(JDBCType jdbcType, // For other data types, the date and time parts are assumed to be relative to the local time zone. TimeZone componentTimeZone = (SSType.DATETIMEOFFSET == ssType) ? UTC.timeZone : localTimeZone; - int subSecondNanos = 0; + int subSecondNanos; // The date and time parts assume a Gregorian calendar with Gregorian leap year behavior // over the entire supported range of values. Create and initialize such a calendar to @@ -909,7 +907,7 @@ static final Object convertTemporalToObject(JDBCType jdbcType, default: throw new AssertionError("Unexpected SSType: " + ssType); } - int localMillisOffset = 0; + int localMillisOffset; if (null == timeZoneCalendar) { TimeZone tz = TimeZone.getDefault(); GregorianCalendar _cal = new GregorianCalendar(componentTimeZone, Locale.US); @@ -922,7 +920,8 @@ static final Object convertTemporalToObject(JDBCType jdbcType, } // Convert the calendar value (in local time) to the desired Java object type. switch (jdbcType.category) { - case BINARY: { + case BINARY: + case SQL_VARIANT: { switch (ssType) { case DATE: { // Per JDBC spec, the time part of java.sql.Date values is initialized to midnight @@ -1337,7 +1336,6 @@ public void mark(int readLimit) { catch (IOException e) { // unfortunately inputstream mark does not throw an exception so we have to eat any exception from the reader here // likely to be a bug in the original InputStream spec. - return; } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/DLLException.java b/src/main/java/com/microsoft/sqlserver/jdbc/DLLException.java index 63e2faaec..a81252e74 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/DLLException.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/DLLException.java @@ -88,40 +88,40 @@ static void buildException(int errCode, String errMessage = getErrMessage(errCode); MessageFormat form = new MessageFormat(SQLServerException.getErrString(errMessage)); - Object[] msgArgs = {null, null, null}; - - buildMsgParams(errMessage, msgArgs, param1, param2, param3); + String[] msgArgs = buildMsgParams(errMessage, param1, param2, param3); throw new SQLServerException(null, form.format(msgArgs), null, 0, false); } - private static void buildMsgParams(String errMessage, - Object[] msgArgs, + private static String[] buildMsgParams(String errMessage, String parameter1, String parameter2, String parameter3) { - if (errMessage.equalsIgnoreCase("R_AECertLocBad")) { + String[] msgArgs = new String[3]; + + if ("R_AECertLocBad".equalsIgnoreCase(errMessage)) { msgArgs[0] = parameter1; msgArgs[1] = parameter1 + "/" + parameter2 + "/" + parameter3; } - else if (errMessage.equalsIgnoreCase("R_AECertStoreBad")) { + else if ("R_AECertStoreBad".equalsIgnoreCase(errMessage)) { msgArgs[0] = parameter2; msgArgs[1] = parameter1 + "/" + parameter2 + "/" + parameter3; } - else if (errMessage.equalsIgnoreCase("R_AECertHashEmpty")) { + else if ("R_AECertHashEmpty".equalsIgnoreCase(errMessage)) { msgArgs[0] = parameter1 + "/" + parameter2 + "/" + parameter3; - } else { msgArgs[0] = parameter1; msgArgs[1] = parameter2; msgArgs[2] = parameter3; } + + return msgArgs; } private static String getErrMessage(int errCode) { - String message = null; + String message; switch (errCode) { case 1: message = "R_AEKeypathEmpty"; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java b/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java index 4f22288d8..3f6ebdbea 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java @@ -95,7 +95,7 @@ static TDSType valueOf(int intValue) throws IllegalArgumentException { if (!(0 <= intValue && intValue < valuesTypes.length) || null == (tdsType = valuesTypes[intValue])) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unknownSSType")); - Object[] msgArgs = {new Integer(intValue)}; + Object[] msgArgs = {intValue}; throw new IllegalArgumentException(form.format(msgArgs)); } @@ -145,7 +145,7 @@ enum SSType DECIMAL (Category.NUMERIC, "decimal", JDBCType.DECIMAL), NUMERIC (Category.NUMERIC, "numeric", JDBCType.NUMERIC), GUID (Category.GUID, "uniqueidentifier", JDBCType.GUID), - SQL_VARIANT (Category.VARIANT, "sql_variant", JDBCType.VARCHAR), + SQL_VARIANT (Category.SQL_VARIANT, "sql_variant", JDBCType.SQL_VARIANT), UDT (Category.UDT, "udt", JDBCType.VARBINARY), XML (Category.XML, "xml", JDBCType.LONGNVARCHAR), TIMESTAMP (Category.TIMESTAMP, "timestamp", JDBCType.BINARY); @@ -203,7 +203,7 @@ enum Category { TIME, TIMESTAMP, UDT, - VARIANT, + SQL_VARIANT, XML } @@ -358,7 +358,20 @@ enum GetterConversion SSType.Category.GUID, EnumSet.of( JDBCType.Category.BINARY, - JDBCType.Category.CHARACTER)); + JDBCType.Category.CHARACTER)), + + SQL_VARIANT ( + SSType.Category.SQL_VARIANT, + EnumSet.of( + JDBCType.Category.CHARACTER, + JDBCType.Category.SQL_VARIANT, + JDBCType.Category.NUMERIC, + JDBCType.Category.DATE, + JDBCType.Category.TIME, + JDBCType.Category.BINARY, + JDBCType.Category.TIMESTAMP, + JDBCType.Category.NCHARACTER, + JDBCType.Category.GUID)); private final SSType.Category from; private final EnumSet to; @@ -369,7 +382,7 @@ private GetterConversion(SSType.Category from, this.to = to; } - private static final EnumMap> conversionMap = new EnumMap>( + private static final EnumMap> conversionMap = new EnumMap<>( SSType.Category.class); static { @@ -763,7 +776,7 @@ private SetterConversionAE(JavaType from, this.to = to; } - private static final EnumMap> setterConversionAEMap = new EnumMap>(JavaType.class); + private static final EnumMap> setterConversionAEMap = new EnumMap<>(JavaType.class); static { for (JavaType javaType : JavaType.values()) @@ -842,7 +855,9 @@ enum JDBCType TVP (Category.TVP, microsoft.sql.Types.STRUCTURED, "java.lang.Object"), DATETIME (Category.TIMESTAMP, microsoft.sql.Types.DATETIME, "java.sql.Timestamp"), SMALLDATETIME (Category.TIMESTAMP, microsoft.sql.Types.SMALLDATETIME, "java.sql.Timestamp"), - GUID (Category.CHARACTER, microsoft.sql.Types.GUID, "java.lang.String"); + GUID (Category.CHARACTER, microsoft.sql.Types.GUID, "java.lang.String"), + SQL_VARIANT (Category.SQL_VARIANT, microsoft.sql.Types.SQL_VARIANT, "java.lang.Object"); + final Category category; private final int intValue; @@ -889,7 +904,8 @@ enum Category { SQLXML, UNKNOWN, TVP, - GUID; + GUID, + SQL_VARIANT, } // This SetterConversion enum is based on the Category enum @@ -908,7 +924,8 @@ enum SetterConversion { JDBCType.Category.LONG_NCHARACTER, JDBCType.Category.BINARY, JDBCType.Category.LONG_BINARY, - JDBCType.Category.GUID)), + JDBCType.Category.GUID, + JDBCType.Category.SQL_VARIANT)), LONG_CHARACTER ( JDBCType.Category.LONG_CHARACTER, @@ -932,7 +949,8 @@ enum SetterConversion { EnumSet.of( JDBCType.Category.NCHARACTER, JDBCType.Category.LONG_NCHARACTER, - JDBCType.Category.NCLOB)), + JDBCType.Category.NCLOB, + JDBCType.Category.SQL_VARIANT)), LONG_NCHARACTER ( JDBCType.Category.LONG_NCHARACTER, @@ -960,7 +978,8 @@ enum SetterConversion { JDBCType.Category.BINARY, JDBCType.Category.LONG_BINARY, JDBCType.Category.BLOB, - JDBCType.Category.GUID)), + JDBCType.Category.GUID, + JDBCType.Category.SQL_VARIANT)), LONG_BINARY ( JDBCType.Category.LONG_BINARY, @@ -981,7 +1000,8 @@ enum SetterConversion { JDBCType.Category.CHARACTER, JDBCType.Category.LONG_CHARACTER, JDBCType.Category.NCHARACTER, - JDBCType.Category.LONG_NCHARACTER)), + JDBCType.Category.LONG_NCHARACTER, + JDBCType.Category.SQL_VARIANT)), DATE ( JDBCType.Category.DATE, @@ -992,7 +1012,8 @@ enum SetterConversion { JDBCType.Category.CHARACTER, JDBCType.Category.LONG_CHARACTER, JDBCType.Category.NCHARACTER, - JDBCType.Category.LONG_NCHARACTER)), + JDBCType.Category.LONG_NCHARACTER, + JDBCType.Category.SQL_VARIANT)), TIME ( JDBCType.Category.TIME, @@ -1003,7 +1024,8 @@ enum SetterConversion { JDBCType.Category.CHARACTER, JDBCType.Category.LONG_CHARACTER, JDBCType.Category.NCHARACTER, - JDBCType.Category.LONG_NCHARACTER)), + JDBCType.Category.LONG_NCHARACTER, + JDBCType.Category.SQL_VARIANT)), TIMESTAMP ( JDBCType.Category.TIMESTAMP, @@ -1015,7 +1037,8 @@ enum SetterConversion { JDBCType.Category.CHARACTER, JDBCType.Category.LONG_CHARACTER, JDBCType.Category.NCHARACTER, - JDBCType.Category.LONG_NCHARACTER)), + JDBCType.Category.LONG_NCHARACTER, + JDBCType.Category.SQL_VARIANT)), TIME_WITH_TIMEZONE ( JDBCType.Category.TIME_WITH_TIMEZONE, @@ -1063,7 +1086,7 @@ private SetterConversion(JDBCType.Category from, this.to = to; } - private static final EnumMap> conversionMap = new EnumMap>( + private static final EnumMap> conversionMap = new EnumMap<>( JDBCType.Category.class); static { @@ -1103,7 +1126,8 @@ enum UpdaterConversion { SSType.Category.LONG_BINARY, SSType.Category.UDT, SSType.Category.GUID, - SSType.Category.TIMESTAMP)), + SSType.Category.TIMESTAMP, + SSType.Category.SQL_VARIANT)), LONG_CHARACTER ( JDBCType.Category.LONG_CHARACTER, @@ -1128,7 +1152,8 @@ enum UpdaterConversion { EnumSet.of( SSType.Category.NCHARACTER, SSType.Category.LONG_NCHARACTER, - SSType.Category.XML)), + SSType.Category.XML, + SSType.Category.SQL_VARIANT)), LONG_NCHARACTER ( JDBCType.Category.LONG_NCHARACTER, @@ -1157,7 +1182,8 @@ enum UpdaterConversion { SSType.Category.LONG_BINARY, SSType.Category.UDT, SSType.Category.TIMESTAMP, - SSType.Category.GUID)), + SSType.Category.GUID, + SSType.Category.SQL_VARIANT)), LONG_BINARY ( JDBCType.Category.LONG_BINARY, @@ -1184,7 +1210,8 @@ enum UpdaterConversion { SSType.Category.CHARACTER, SSType.Category.LONG_CHARACTER, SSType.Category.NCHARACTER, - SSType.Category.LONG_NCHARACTER)), + SSType.Category.LONG_NCHARACTER, + SSType.Category.SQL_VARIANT)), DATE ( JDBCType.Category.DATE, @@ -1196,7 +1223,8 @@ enum UpdaterConversion { SSType.Category.CHARACTER, SSType.Category.LONG_CHARACTER, SSType.Category.NCHARACTER, - SSType.Category.LONG_NCHARACTER)), + SSType.Category.LONG_NCHARACTER, + SSType.Category.SQL_VARIANT)), TIME ( JDBCType.Category.TIME, @@ -1208,7 +1236,8 @@ enum UpdaterConversion { SSType.Category.CHARACTER, SSType.Category.LONG_CHARACTER, SSType.Category.NCHARACTER, - SSType.Category.LONG_NCHARACTER)), + SSType.Category.LONG_NCHARACTER, + SSType.Category.SQL_VARIANT)), TIMESTAMP ( JDBCType.Category.TIMESTAMP, @@ -1221,7 +1250,8 @@ enum UpdaterConversion { SSType.Category.CHARACTER, SSType.Category.LONG_CHARACTER, SSType.Category.NCHARACTER, - SSType.Category.LONG_NCHARACTER)), + SSType.Category.LONG_NCHARACTER, + SSType.Category.SQL_VARIANT)), DATETIMEOFFSET ( JDBCType.Category.DATETIMEOFFSET, @@ -1259,8 +1289,13 @@ enum UpdaterConversion { SSType.Category.CHARACTER, SSType.Category.LONG_CHARACTER, SSType.Category.NCHARACTER, - SSType.Category.LONG_NCHARACTER)); - + SSType.Category.LONG_NCHARACTER)), + + SQL_VARIANT ( + JDBCType.Category.SQL_VARIANT, + EnumSet.of( + SSType.Category.SQL_VARIANT)); + private final JDBCType.Category from; private final EnumSet to; @@ -1270,7 +1305,7 @@ private UpdaterConversion(JDBCType.Category from, this.to = to; } - private static final EnumMap> conversionMap = new EnumMap>( + private static final EnumMap> conversionMap = new EnumMap<>( JDBCType.Category.class); static { @@ -1379,11 +1414,12 @@ boolean isUnsupported() { * JDBC3 types are expected for SE 5. JDBC4 types are expected for SE 6 and later. */ int asJavaSqlType() { - if (Util.SYSTEM_SPEC_VERSION.equals("1.5")) { + if ("1.5".equals(Util.SYSTEM_SPEC_VERSION)) { switch (this) { case NCHAR: return java.sql.Types.CHAR; case NVARCHAR: + case SQLXML: return java.sql.Types.VARCHAR; case LONGNVARCHAR: return java.sql.Types.LONGVARCHAR; @@ -1391,8 +1427,6 @@ int asJavaSqlType() { return java.sql.Types.CLOB; case ROWID: return java.sql.Types.OTHER; - case SQLXML: - return java.sql.Types.VARCHAR; default: return intValue; } @@ -1582,7 +1616,7 @@ private NormalizationAE(JDBCType from, this.to = to; } - private static final EnumMap> normalizationMapAE = new EnumMap>(JDBCType.class); + private static final EnumMap> normalizationMapAE = new EnumMap<>(JDBCType.class); static { for (JDBCType jdbcType : JDBCType.values()) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/FailOverInfo.java b/src/main/java/com/microsoft/sqlserver/jdbc/FailOverInfo.java index 00aa16f21..0e905bc7d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/FailOverInfo.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/FailOverInfo.java @@ -57,8 +57,8 @@ private void setupInfo(SQLServerConnection con) throws SQLServerException { else { // 3.3006 get the instance name int px = failoverPartner.indexOf('\\'); - String instancePort = null; - String instanceValue = null; + String instancePort; + String instanceValue; // found the instance name with the severname if (px >= 0) { @@ -71,7 +71,7 @@ private void setupInfo(SQLServerConnection con) throws SQLServerException { instancePort = con.getInstancePort(failoverPartner, instanceValue); try { - portNumber = (new Integer(instancePort)).intValue(); + portNumber = new Integer(instancePort); } catch (NumberFormatException e) { // Should not get here as the server should give a proper port number anyway. diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/FailOverMapSingleton.java b/src/main/java/com/microsoft/sqlserver/jdbc/FailOverMapSingleton.java index f3146d926..8bebf1f0e 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/FailOverMapSingleton.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/FailOverMapSingleton.java @@ -13,7 +13,7 @@ final class FailoverMapSingleton { private static int INITIALHASHMAPSIZE = 5; - private static HashMap failoverMap = new HashMap(INITIALHASHMAPSIZE); + private static HashMap failoverMap = new HashMap<>(INITIALHASHMAPSIZE); private FailoverMapSingleton() { /* hide the constructor to stop the instantiation of this class. */} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index 391d0fb50..03f7af0a8 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -19,6 +19,7 @@ import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.math.BigInteger; +import java.math.RoundingMode; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; @@ -65,6 +66,7 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; @@ -135,6 +137,9 @@ final class TDS { static final int FLAG_TVP_DEFAULT_COLUMN = 0x200; static final int FEATURE_EXT_TERMINATOR = -1; + + // Sql_variant length + static final int SQL_VARIANT_LENGTH = 8009; static final String getTokenName(int tdsTokenType) { switch (tdsTokenType) { @@ -498,7 +503,7 @@ class GregorianChange { GregorianCalendar cal = new GregorianCalendar(Locale.US); cal.clear(); - cal.set(1, 1, 577738, 0, 0, 0);// 577738 = 1+577737(no of days since epoch that brings us to oct 15th 1582) + cal.set(1, Calendar.FEBRUARY, 577738, 0, 0, 0);// 577738 = 1+577737(no of days since epoch that brings us to oct 15th 1582) if (cal.get(Calendar.DAY_OF_MONTH) == 15) { // If the date calculation is correct(the above bug is fixed), // post the default gregorian cut over date, the pure gregorian date @@ -520,11 +525,13 @@ private GregorianChange() { } } -// UTC/GMT time zone singleton. The enum type delays initialization until first use. -enum UTC { - INSTANCE; +final class UTC { + // UTC/GMT time zone singleton. static final TimeZone timeZone = new SimpleTimeZone(0, "UTC"); + + private UTC() { + } } final class TDSChannel { @@ -1446,7 +1453,7 @@ public void setOOBInline(boolean on) throws SocketException { * A PermissiveX509TrustManager is used to "verify" the authenticity of the server when the trustServerCertificate connection property is set to * true. */ - private final class PermissiveX509TrustManager extends Object implements X509TrustManager { + private final class PermissiveX509TrustManager implements X509TrustManager { private final TDSChannel tdsChannel; private final Logger logger; private final String logContext; @@ -1479,7 +1486,7 @@ public X509Certificate[] getAcceptedIssuers() { * * This validates the subject name in the certificate with the host name */ - private final class HostNameOverrideX509TrustManager extends Object implements X509TrustManager { + private final class HostNameOverrideX509TrustManager implements X509TrustManager { private final Logger logger; private final String logContext; private final X509TrustManager defaultTrustManager; @@ -1492,7 +1499,7 @@ private final class HostNameOverrideX509TrustManager extends Object implements X this.logContext = tdsChannel.toString() + " (HostNameOverrideX509TrustManager):"; defaultTrustManager = tm; // canonical name is in lower case so convert this to lowercase too. - this.hostName = hostName.toLowerCase(); + this.hostName = hostName.toLowerCase(Locale.ENGLISH); ; } @@ -1574,7 +1581,7 @@ private void validateServerNameInCertificate(X509Certificate cert) throws Certif logger.finer(logContext + " The DN name in certificate:" + nameInCertDN); } - boolean isServerNameValidated = false; + boolean isServerNameValidated; // the name in cert is in RFC2253 format parse it to get the actual subject name String subjectCN = parseCommonName(nameInCertDN); @@ -1615,13 +1622,11 @@ private void validateServerNameInCertificate(X509Certificate cert) throws Certif if (value != null && value instanceof String) { String dnsNameInSANCert = (String) value; - // convert to upper case and then to lower case in english locale - // to avoid Turkish i issues. + // Use English locale to avoid Turkish i issues. // Note that, this conversion was not necessary for // cert.getSubjectX500Principal().getName("canonical"); // as the above API already does this by default as per documentation. - dnsNameInSANCert = dnsNameInSANCert.toUpperCase(Locale.US); - dnsNameInSANCert = dnsNameInSANCert.toLowerCase(Locale.US); + dnsNameInSANCert = dnsNameInSANCert.toLowerCase(Locale.ENGLISH); isServerNameValidated = validateServerName(dnsNameInSANCert); @@ -1687,7 +1692,7 @@ void enableSSL(String host, boolean isFips = false; String trustStoreType = null; - String fipsProvider = null; + String sslProtocol = null; // If anything in here fails, terminate the connection and throw an exception try { @@ -1705,11 +1710,11 @@ void enableSSL(String host, trustStoreType = SQLServerDriverStringProperty.TRUST_STORE_TYPE.getDefaultValue(); } - fipsProvider = con.activeConnectionProperties.getProperty(SQLServerDriverStringProperty.FIPS_PROVIDER.toString()); isFips = Boolean.valueOf(con.activeConnectionProperties.getProperty(SQLServerDriverBooleanProperty.FIPS.toString())); + sslProtocol = con.activeConnectionProperties.getProperty(SQLServerDriverStringProperty.SSL_PROTOCOL.toString()); if (isFips) { - validateFips(fipsProvider, trustStoreType, trustStoreFileName); + validateFips(trustStoreType, trustStoreFileName); } assert TDS.ENCRYPT_OFF == con.getRequestedEncryptionLevel() || // Login only SSL @@ -1730,7 +1735,22 @@ void enableSSL(String host, tm = new TrustManager[] {new PermissiveX509TrustManager(this)}; } - + // Otherwise, we'll check if a specific TrustManager implemenation has been requested and + // if so instantiate it, optionally specifying a constructor argument to customize it. + else if (con.getTrustManagerClass() != null) { + Class tmClass = Class.forName(con.getTrustManagerClass()); + if (!TrustManager.class.isAssignableFrom(tmClass)) { + throw new IllegalArgumentException( + "The class specified by the trustManagerClass property must implement javax.net.ssl.TrustManager"); + } + String constructorArg = con.getTrustManagerConstructorArg(); + if (constructorArg == null) { + tm = new TrustManager[] {(TrustManager) tmClass.getDeclaredConstructor().newInstance()}; + } + else { + tm = new TrustManager[] {(TrustManager) tmClass.getDeclaredConstructor(String.class).newInstance(constructorArg)}; + } + } // Otherwise, we'll validate the certificate using a real TrustManager obtained // from the a security provider that is capable of validating X.509 certificates. else { @@ -1755,12 +1775,8 @@ void enableSSL(String host, if (logger.isLoggable(Level.FINEST)) logger.finest(toString() + " Finding key store interface"); - if (isFips) { - ks = KeyStore.getInstance(trustStoreType, fipsProvider); - } - else { - ks = KeyStore.getInstance(trustStoreType); - } + + ks = KeyStore.getInstance(trustStoreType); ksProvider = ks.getProvider(); // Next, load up the trust store file from the specified location. @@ -1836,7 +1852,7 @@ void enableSSL(String host, if (logger.isLoggable(Level.FINEST)) logger.finest(toString() + " Getting TLS or better SSL context"); - sslContext = SSLContext.getInstance("TLS"); + sslContext = SSLContext.getInstance(sslProtocol); sslContextProvider = sslContext.getProvider(); if (logger.isLoggable(Level.FINEST)) @@ -1853,8 +1869,7 @@ void enableSSL(String host, logger.finest(toString() + " Creating SSL socket"); sslSocket = (SSLSocket) sslContext.getSocketFactory().createSocket(proxySocket, host, port, false); // don't close proxy when SSL socket - // is closed - + // is closed // At long last, start the SSL handshake ... if (logger.isLoggable(Level.FINER)) logger.finer(toString() + " Starting SSL handshake"); @@ -1911,7 +1926,14 @@ void enableSSL(String host, // It is important to get the localized message here, otherwise error messages won't match for different locales. String errMsg = e.getLocalizedMessage(); - + // If the message is null replace it with the non-localized message or a dummy string. This can happen if a custom + // TrustManager implementation is specified that does not provide localized messages. + if (errMsg == null) { + errMsg = e.getMessage(); + } + if (errMsg == null) { + errMsg = ""; + } // The error message may have a connection id appended to it. Extract the message only for comparison. // This client connection id is appended in method checkAndAppendClientConnId(). if (errMsg.contains(SQLServerException.LOG_CLIENT_CONNECTION_ID_PREFIX)) { @@ -1935,57 +1957,40 @@ void enableSSL(String host, * Valid FIPS settings: *
  • Encrypt should be true *
  • trustServerCertificate should be false - *
  • if certificate is not installed FIPSProvider & TrustStoreType should be present. + *
  • if certificate is not installed TrustStoreType should be present. * - * @param fipsProvider - * FIPS Provider * @param trustStoreType * @param trustStoreFileName * @throws SQLServerException * @since 6.1.4 */ - private void validateFips(final String fipsProvider, - final String trustStoreType, + private void validateFips(final String trustStoreType, final String trustStoreFileName) throws SQLServerException { boolean isValid = false; boolean isEncryptOn; boolean isValidTrustStoreType; boolean isValidTrustStore; boolean isTrustServerCertificate; - boolean isValidFipsProvider; String strError = SQLServerException.getErrString("R_invalidFipsConfig"); isEncryptOn = (TDS.ENCRYPT_ON == con.getRequestedEncryptionLevel()); - // Here different FIPS provider supports different KeyStore type along with different JVM Implementation. - isValidFipsProvider = !StringUtils.isEmpty(fipsProvider); isValidTrustStoreType = !StringUtils.isEmpty(trustStoreType); isValidTrustStore = !StringUtils.isEmpty(trustStoreFileName); isTrustServerCertificate = con.trustServerCertificate(); - if (isEncryptOn & !isTrustServerCertificate) { - if (logger.isLoggable(Level.FINER)) - logger.finer(toString() + " Found parameters are encrypt is true & trustServerCertificate false"); - + if (isEncryptOn && !isTrustServerCertificate) { isValid = true; - if (isValidTrustStore) { - // In case of valid trust store we need to check fipsProvider and TrustStoreType. - if (!isValidFipsProvider || !isValidTrustStoreType) { - isValid = false; - strError = SQLServerException.getErrString("R_invalidFipsProviderConfig"); - + // In case of valid trust store we need to check TrustStoreType. + if (!isValidTrustStoreType) { + isValid = false; if (logger.isLoggable(Level.FINER)) - logger.finer(toString() + " FIPS provider & TrustStoreType should pass with TrustStore."); + logger.finer(toString() + "TrustStoreType is required alongside with TrustStore."); } - if (logger.isLoggable(Level.FINER)) - logger.finer(toString() + " Found FIPS parameters seems to be valid."); } } - else { - strError = SQLServerException.getErrString("R_invalidFipsEncryptConfig"); - } if (!isValid) { throw new SQLServerException(strError, null, 0, null); @@ -2099,7 +2104,7 @@ final int read(byte[] data, con.terminate(SQLServerException.ERROR_SOCKET_TIMEOUT, e.getMessage(), e); } else { - con.terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, e.getMessage()); + con.terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, e.getMessage(), e); } return 0; // Keep the compiler happy. @@ -2116,7 +2121,7 @@ final void write(byte[] data, if (logger.isLoggable(Level.FINER)) logger.finer(toString() + " write failed:" + e.getMessage()); - con.terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, e.getMessage()); + con.terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, e.getMessage(), e); } } @@ -2128,7 +2133,7 @@ final void flush() throws SQLServerException { if (logger.isLoggable(Level.FINER)) logger.finer(toString() + " flush failed:" + e.getMessage()); - con.terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, e.getMessage()); + con.terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, e.getMessage(), e); } } @@ -2264,7 +2269,29 @@ final void close() { logMsg.append("\r\n"); } - packetLogger.finest(logMsg.toString()); + if (packetLogger.isLoggable(Level.FINEST)) { + packetLogger.finest(logMsg.toString()); + } + } + + /** + * Get the current socket SO_TIMEOUT value. + * + * @return the current socket timeout value + * @throws IOException thrown if the socket timeout cannot be read + */ + final int getNetworkTimeout() throws IOException { + return tcpSocket.getSoTimeout(); + } + + /** + * Set the socket SO_TIMEOUT value. + * + * @param timeout the socket timeout in milliseconds + * @throws IOException thrown if the socket timeout cannot be set + */ + final void setNetworkTimeout(int timeout) throws IOException { + tcpSocket.setSoTimeout(timeout); } } @@ -2325,7 +2352,7 @@ enum Result { // no of threads that finished their socket connection // attempts and notified socketFinder about their result - private volatile int noOfThreadsThatNotified = 0; + private int noOfThreadsThatNotified = 0; // If valid connected socket is found, selectedSocketInfo.socket will be non-null. // If valid connected socketChannel is established for getting a socket, selectedSocketInfo.socketChannel will be non-null @@ -2411,12 +2438,16 @@ else if (!useTnir) { // Code reaches here only if MSF = true or (TNIR = true and not TNIR first attempt) if (logger.isLoggable(Level.FINER)) { - String loggingString = this.toString() + " Total no of InetAddresses: " + inetAddrs.length + ". They are: "; + StringBuilder loggingString = new StringBuilder(this.toString()); + loggingString.append(" Total no of InetAddresses: "); + loggingString.append(inetAddrs.length); + loggingString.append(". They are: "); + for (InetAddress inetAddr : inetAddrs) { - loggingString = loggingString + inetAddr.toString() + ";"; + loggingString.append(inetAddr.toString() + ";"); } - logger.finer(loggingString); + logger.finer(loggingString.toString()); } if (inetAddrs.length > ipAddressLimit) { @@ -2436,8 +2467,8 @@ else if (!useTnir) { findSocketUsingJavaNIO(inetAddrs, portNumber, timeoutInMilliSeconds); } else { - LinkedList inet4Addrs = new LinkedList(); - LinkedList inet6Addrs = new LinkedList(); + LinkedList inet4Addrs = new LinkedList<>(); + LinkedList inet6Addrs = new LinkedList<>(); for (InetAddress inetAddr : inetAddrs) { if (inetAddr instanceof Inet4Address) { @@ -2516,6 +2547,8 @@ else if (!useTnir) { } catch (InterruptedException ex) { + // re-interrupt the current thread, in order to restore the thread's interrupt status. + Thread.currentThread().interrupt(); close(selectedSocketInfo.socket); SQLServerException.ConvertConnectExceptionToSQLServerException(hostName, portNumber, conn, ex); } @@ -2562,13 +2595,13 @@ private void findSocketUsingJavaNIO(InetAddress[] inetAddrs, assert inetAddrs.length != 0 : "Number of inetAddresses should not be zero in this function"; Selector selector = null; - LinkedList socketChannels = new LinkedList(); + LinkedList socketChannels = new LinkedList<>(); SocketChannel selectedChannel = null; try { selector = Selector.open(); - for (int i = 0; i < inetAddrs.length; i++) { + for (InetAddress inetAddr : inetAddrs) { SocketChannel sChannel = SocketChannel.open(); socketChannels.add(sChannel); @@ -2579,10 +2612,10 @@ private void findSocketUsingJavaNIO(InetAddress[] inetAddrs, int ops = SelectionKey.OP_CONNECT; SelectionKey key = sChannel.register(selector, ops); - sChannel.connect(new InetSocketAddress(inetAddrs[i], portNumber)); + sChannel.connect(new InetSocketAddress(inetAddr, portNumber)); if (logger.isLoggable(Level.FINER)) - logger.finer(this.toString() + " initiated connection to address: " + inetAddrs[i] + ", portNumber: " + portNumber); + logger.finer(this.toString() + " initiated connection to address: " + inetAddr + ", portNumber: " + portNumber); } long timerNow = System.currentTimeMillis(); @@ -2684,9 +2717,8 @@ private void findSocketUsingJavaNIO(InetAddress[] inetAddrs, } } - // if a channel was selected, make the necessary updates + // if a channel was selected, make the necessary updates if (selectedChannel != null) { - // Note that this must be done after selector is closed. Otherwise, // we would get an illegalBlockingMode exception at run time. selectedChannel.configureBlocking(true); @@ -2802,10 +2834,11 @@ private void findSocketUsingThreading(LinkedList inetAddrs, int portNumber, int timeoutInMilliSeconds) throws IOException, InterruptedException { assert timeoutInMilliSeconds != 0 : "The timeout cannot be zero"; + assert inetAddrs.isEmpty() == false : "Number of inetAddresses should not be zero in this function"; - LinkedList sockets = new LinkedList(); - LinkedList socketConnectors = new LinkedList(); + LinkedList sockets = new LinkedList<>(); + LinkedList socketConnectors = new LinkedList<>(); try { @@ -3236,7 +3269,7 @@ void setDataLoggable(boolean value) { private byte valueBytes[] = new byte[256]; // Monotonically increasing packet number associated with the current message - private volatile int packetNum = 0; + private int packetNum = 0; // Bytes for sending decimal/numeric data private final static int BYTES4 = 4; @@ -3401,6 +3434,17 @@ void writeByte(byte value) throws SQLServerException { } } + /** + * writing sqlCollation information for sqlVariant type when sending character types. + * + * @param variantType + * @throws SQLServerException + */ + void writeCollationForSqlVariant(SqlVariant variantType) throws SQLServerException { + writeInt(variantType.getCollation().getCollationInfo()); + writeByte((byte) (variantType.getCollation().getCollationSortID() & 0xFF)); + } + void writeChar(char value) throws SQLServerException { if (stagingBuffer.remaining() >= 2) { stagingBuffer.putChar(value); @@ -3456,19 +3500,7 @@ void writeInt(int value) throws SQLServerException { * the data value */ void writeReal(Float value) throws SQLServerException { - if (false) // stagingBuffer.remaining() >= 4) - { - stagingBuffer.putFloat(value); - if (tdsChannel.isLoggingPackets()) { - if (dataIsLoggable) - logBuffer.putFloat(value); - else - logBuffer.position(logBuffer.position() + 4); - } - } - else { - writeInt(Float.floatToRawIntBits(value.floatValue())); - } + writeInt(Float.floatToRawIntBits(value)); } /** @@ -3508,72 +3540,99 @@ void writeDouble(double value) throws SQLServerException { * the source JDBCType * @param precision * the precision of the data value + * @param scale + * the scale of the column + * @throws SQLServerException */ void writeBigDecimal(BigDecimal bigDecimalVal, int srcJdbcType, - int precision) throws SQLServerException { + int precision, + int scale) throws SQLServerException { /* * Length including sign byte One 1-byte unsigned integer that represents the sign of the decimal value (0 => Negative, 1 => positive) One 4-, - * 8-, 12-, or 16-byte signed integer that represents the decimal value multiplied by 10^scale. The maximum size of this integer is determined - * based on p as follows: 4 bytes if 1 <= p <= 9. 8 bytes if 10 <= p <= 19. 12 bytes if 20 <= p <= 28. 16 bytes if 29 <= p <= 38. + * 8-, 12-, or 16-byte signed integer that represents the decimal value multiplied by 10^scale. + */ + + /* + * setScale of all BigDecimal value based on metadata as scale is not sent seperately for individual value. Use the rounding used in Server. + * Say, for BigDecimal("0.1"), if scale in metdadata is 0, then ArithmeticException would be thrown if RoundingMode is not set + */ + bigDecimalVal = bigDecimalVal.setScale(scale, RoundingMode.HALF_UP); + + // data length + 1 byte for sign + int bLength = BYTES16 + 1; + writeByte((byte) (bLength)); + + // Byte array to hold all the data and padding bytes. + byte[] bytes = new byte[bLength]; + + byte[] valueBytes = DDC.convertBigDecimalToBytes(bigDecimalVal, scale); + // removing the precision and scale information from the valueBytes array + System.arraycopy(valueBytes, 2, bytes, 0, valueBytes.length - 2); + writeBytes(bytes); + } + + /** + * Append a big decimal inside sql_variant in the TDS stream. + * + * @param bigDecimalVal + * the big decimal data value + * @param srcJdbcType + * the source JDBCType + */ + void writeSqlVariantInternalBigDecimal(BigDecimal bigDecimalVal, + int srcJdbcType) throws SQLServerException { + /* + * Length including sign byte One 1-byte unsigned integer that represents the sign of the decimal value (0 => Negative, 1 => positive) One + * 16-byte signed integer that represents the decimal value multiplied by 10^scale. In sql_variant, we send the bigdecimal with precision 38, + * therefore we use 16 bytes for the maximum size of this integer. */ boolean isNegative = (bigDecimalVal.signum() < 0); BigInteger bi = bigDecimalVal.unscaledValue(); if (isNegative) + { bi = bi.negate(); - if (9 >= precision) { - writeByte((byte) (BYTES4 + 1)); - writeByte((byte) (isNegative ? 0 : 1)); - writeInt(bi.intValue()); - } - else if (19 >= precision) { - writeByte((byte) (BYTES8 + 1)); - writeByte((byte) (isNegative ? 0 : 1)); - writeLong(bi.longValue()); } - else { - int bLength; - if (28 >= precision) - bLength = BYTES12; - else - bLength = BYTES16; - writeByte((byte) (bLength + 1)); - writeByte((byte) (isNegative ? 0 : 1)); + int bLength; + bLength = BYTES16; - // Get the bytes of the BigInteger value. It is in reverse order, with - // most significant byte in 0-th element. We need to reverse it first before sending over TDS. - byte[] unscaledBytes = bi.toByteArray(); + writeByte((byte) (isNegative ? 0 : 1)); - if (unscaledBytes.length > bLength) { - // If precession of input is greater than maximum allowed (p><= 38) throw Exception - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_valueOutOfRange")); - Object[] msgArgs = {JDBCType.of(srcJdbcType)}; - throw new SQLServerException(form.format(msgArgs), SQLState.DATA_EXCEPTION_LENGTH_MISMATCH, DriverError.NOT_SET, null); - } + // Get the bytes of the BigInteger value. It is in reverse order, with + // most significant byte in 0-th element. We need to reverse it first before sending over TDS. + byte[] unscaledBytes = bi.toByteArray(); + + if (unscaledBytes.length > bLength) { + // If precession of input is greater than maximum allowed (p><= 38) throw Exception + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_valueOutOfRange")); + Object[] msgArgs = {JDBCType.of(srcJdbcType)}; + throw new SQLServerException(form.format(msgArgs), SQLState.DATA_EXCEPTION_LENGTH_MISMATCH, DriverError.NOT_SET, null); + } - // Byte array to hold all the reversed and padding bytes. - byte[] bytes = new byte[bLength]; + // Byte array to hold all the reversed and padding bytes. + byte[] bytes = new byte[bLength]; - // We need to fill up the rest of the array with zeros, as unscaledBytes may have less bytes - // than the required size for TDS. - int remaining = bLength - unscaledBytes.length; + // We need to fill up the rest of the array with zeros, as unscaledBytes may have less bytes + // than the required size for TDS. + int remaining = bLength - unscaledBytes.length; - // Reverse the bytes. - int i, j; - for (i = 0, j = unscaledBytes.length - 1; i < unscaledBytes.length;) - bytes[i++] = unscaledBytes[j--]; + // Reverse the bytes. + int i, j; + for (i = 0, j = unscaledBytes.length - 1; i < unscaledBytes.length;) + bytes[i++] = unscaledBytes[j--]; - // Fill the rest of the array with zeros. - for (; i < remaining; i++) - bytes[i] = (byte) 0x00; - writeBytes(bytes); + // Fill the rest of the array with zeros. + for (; i < remaining; i++) + { + bytes[i] = (byte) 0x00; } + writeBytes(bytes); } void writeSmalldatetime(String value) throws SQLServerException { GregorianCalendar calendar = initializeCalender(TimeZone.getDefault()); - long utcMillis = 0; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) + long utcMillis; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) java.sql.Timestamp timestampValue = java.sql.Timestamp.valueOf(value); utcMillis = timestampValue.getTime(); @@ -3610,8 +3669,8 @@ void writeSmalldatetime(String value) throws SQLServerException { void writeDatetime(String value) throws SQLServerException { GregorianCalendar calendar = initializeCalender(TimeZone.getDefault()); - long utcMillis = 0; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) - int subSecondNanos = 0; + long utcMillis; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) + int subSecondNanos; java.sql.Timestamp timestampValue = java.sql.Timestamp.valueOf(value); utcMillis = timestampValue.getTime(); subSecondNanos = timestampValue.getNanos(); @@ -3658,7 +3717,7 @@ void writeDatetime(String value) throws SQLServerException { void writeDate(String value) throws SQLServerException { GregorianCalendar calendar = initializeCalender(TimeZone.getDefault()); - long utcMillis = 0; + long utcMillis; java.sql.Date dateValue = java.sql.Date.valueOf(value); utcMillis = dateValue.getTime(); @@ -3673,8 +3732,8 @@ void writeDate(String value) throws SQLServerException { void writeTime(java.sql.Timestamp value, int scale) throws SQLServerException { GregorianCalendar calendar = initializeCalender(TimeZone.getDefault()); - long utcMillis = 0; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) - int subSecondNanos = 0; + long utcMillis; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) + int subSecondNanos; utcMillis = value.getTime(); subSecondNanos = value.getNanos(); @@ -3687,11 +3746,11 @@ void writeTime(java.sql.Timestamp value, void writeDateTimeOffset(Object value, int scale, SSType destSSType) throws SQLServerException { - GregorianCalendar calendar = null; - TimeZone timeZone = TimeZone.getDefault(); // Time zone to associate with the value in the Gregorian calendar - long utcMillis = 0; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) - int subSecondNanos = 0; - int minutesOffset = 0; + GregorianCalendar calendar; + TimeZone timeZone; // Time zone to associate with the value in the Gregorian calendar + long utcMillis; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) + int subSecondNanos; + int minutesOffset; microsoft.sql.DateTimeOffset dtoValue = (microsoft.sql.DateTimeOffset) value; utcMillis = dtoValue.getTimestamp().getTime(); @@ -3717,10 +3776,10 @@ void writeDateTimeOffset(Object value, void writeOffsetDateTimeWithTimezone(OffsetDateTime offsetDateTimeValue, int scale) throws SQLServerException { - GregorianCalendar calendar = null; + GregorianCalendar calendar; TimeZone timeZone; - long utcMillis = 0; - int subSecondNanos = 0; + long utcMillis; + int subSecondNanos; int minutesOffset = 0; try { @@ -3733,7 +3792,7 @@ void writeOffsetDateTimeWithTimezone(OffsetDateTime offsetDateTimeValue, throw new SQLServerException(SQLServerException.getErrString("R_zoneOffsetError"), null, // SQLState is null as this error is generated in // the driver 0, // Use 0 instead of DriverError.NOT_SET to use the correct constructor - null); + e); } subSecondNanos = offsetDateTimeValue.getNano(); @@ -3773,10 +3832,10 @@ void writeOffsetDateTimeWithTimezone(OffsetDateTime offsetDateTimeValue, void writeOffsetTimeWithTimezone(OffsetTime offsetTimeValue, int scale) throws SQLServerException { - GregorianCalendar calendar = null; + GregorianCalendar calendar; TimeZone timeZone; - long utcMillis = 0; - int subSecondNanos = 0; + long utcMillis; + int subSecondNanos; int minutesOffset = 0; try { @@ -3789,7 +3848,7 @@ void writeOffsetTimeWithTimezone(OffsetTime offsetTimeValue, throw new SQLServerException(SQLServerException.getErrString("R_zoneOffsetError"), null, // SQLState is null as this error is generated in // the driver 0, // Use 0 instead of DriverError.NOT_SET to use the correct constructor - null); + e); } subSecondNanos = offsetTimeValue.getNano(); @@ -3891,11 +3950,14 @@ void writeWrappedBytes(byte value[], // what remains in the current staging buffer. However, the value must // be short enough to fit in an empty buffer. assert valueLength <= value.length; - assert stagingBuffer.remaining() < valueLength; + + int remaining = stagingBuffer.remaining(); + assert remaining < valueLength; + assert valueLength <= stagingBuffer.capacity(); // Fill any remaining space in the staging buffer - int remaining = stagingBuffer.remaining(); + remaining = stagingBuffer.remaining(); if (remaining > 0) { stagingBuffer.put(value, 0, remaining); if (tdsChannel.isLoggingPackets()) { @@ -3984,7 +4046,7 @@ void writeStream(InputStream inputStream, // the actual stream length did not match then cancel the request. if (DataTypes.UNKNOWN_STREAM_LENGTH != advertisedLength && actualLength != advertisedLength) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_mismatchedStreamLength")); - Object[] msgArgs = {Long.valueOf(advertisedLength), Long.valueOf(actualLength)}; + Object[] msgArgs = {advertisedLength, actualLength}; error(form.format(msgArgs), SQLState.DATA_EXCEPTION_LENGTH_MISMATCH, DriverError.NOT_SET); } } @@ -4069,10 +4131,10 @@ void writeNonUnicodeReader(Reader reader, // the actual stream length did not match then cancel the request. if (DataTypes.UNKNOWN_STREAM_LENGTH != advertisedLength && actualLength != advertisedLength) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_mismatchedStreamLength")); - Object[] msgArgs = {Long.valueOf(advertisedLength), Long.valueOf(actualLength)}; + Object[] msgArgs = {advertisedLength, actualLength}; error(form.format(msgArgs), SQLState.DATA_EXCEPTION_LENGTH_MISMATCH, DriverError.NOT_SET); } - } + } /* * Note: There is another method with same code logic for non unicode reader, writeNonUnicodeReader(), implemented for performance efficiency. Any @@ -4134,13 +4196,13 @@ void writeReader(Reader reader, // the actual stream length did not match then cancel the request. if (DataTypes.UNKNOWN_STREAM_LENGTH != advertisedLength && actualLength != advertisedLength) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_mismatchedStreamLength")); - Object[] msgArgs = {Long.valueOf(advertisedLength), Long.valueOf(actualLength)}; + Object[] msgArgs = {advertisedLength, actualLength}; error(form.format(msgArgs), SQLState.DATA_EXCEPTION_LENGTH_MISMATCH, DriverError.NOT_SET); } } GregorianCalendar initializeCalender(TimeZone timeZone) { - GregorianCalendar calendar = null; + GregorianCalendar calendar; // Create the calendar that will hold the value. For DateTimeOffset values, the calendar's // time zone is UTC. For other values, the calendar's time zone is a local time zone. @@ -4356,7 +4418,7 @@ void writeRPCBit(String sName, } else { writeByte((byte) 1); // length of datatype - writeByte((byte) (booleanValue.booleanValue() ? 1 : 0)); + writeByte((byte) (booleanValue ? 1 : 0)); } } @@ -4380,7 +4442,7 @@ void writeRPCByte(String sName, } else { writeByte((byte) 1); // length of datatype - writeByte(byteValue.byteValue()); + writeByte(byteValue); } } @@ -4404,7 +4466,7 @@ void writeRPCShort(String sName, } else { writeByte((byte) 2); // length of datatype - writeShort(shortValue.shortValue()); + writeShort(shortValue); } } @@ -4428,7 +4490,7 @@ void writeRPCInt(String sName, } else { writeByte((byte) 4); // length of datatype - writeInt(intValue.intValue()); + writeInt(intValue); } } @@ -4452,7 +4514,7 @@ void writeRPCLong(String sName, } else { writeByte((byte) 8); // length of datatype - writeLong(longValue.longValue()); + writeLong(longValue); } } @@ -4479,7 +4541,19 @@ void writeRPCReal(String sName, else { writeByte((byte) 4); // max length writeByte((byte) 4); // actual length - writeInt(Float.floatToRawIntBits(floatValue.floatValue())); + writeInt(Float.floatToRawIntBits(floatValue)); + } + } + + void writeRPCSqlVariant(String sName, + SqlVariant sqlVariantValue, + boolean bOut) throws SQLServerException { + writeRPCNameValType(sName, bOut, TDSType.SQL_VARIANT); + + // Data and length + if (null == sqlVariantValue) { + writeInt(0); // max length + writeInt(0); // actual length } } @@ -4507,7 +4581,7 @@ void writeRPCDouble(String sName, } else { writeByte((byte) l); // len of data bytes - long bits = Double.doubleToLongBits(doubleValue.doubleValue()); + long bits = Double.doubleToLongBits(doubleValue); long mask = 0xFF; int nShift = 0; for (int i = 0; i < 8; i++) { @@ -4729,14 +4803,58 @@ void writeTVP(TVP value) throws SQLServerException { } void writeTVPRows(TVP value) throws SQLServerException { - boolean isShortValue, isNull; - int dataLength; + boolean tdsWritterCached = false; + ByteBuffer cachedTVPHeaders = null; + TDSCommand cachedCommand = null; + + boolean cachedRequestComplete = false; + boolean cachedInterruptsEnabled = false; + boolean cachedProcessedResponse = false; if (!value.isNull()) { + + // If the preparedStatement and the ResultSet are created by the same connection, and TVP is set with ResultSet and Server Cursor + // is used, the tdsWriter of the calling preparedStatement is overwritten by the SQLServerResultSet#next() method when fetching new rows. + // Therefore, we need to send TVP data row by row before fetching new row. + if (TVPType.ResultSet == value.tvpType) { + if ((null != value.sourceResultSet) && (value.sourceResultSet instanceof SQLServerResultSet)) { + SQLServerResultSet sourceResultSet = (SQLServerResultSet) value.sourceResultSet; + SQLServerStatement src_stmt = (SQLServerStatement) sourceResultSet.getStatement(); + int resultSetServerCursorId = sourceResultSet.getServerCursorId(); + + if (con.equals(src_stmt.getConnection()) && 0 != resultSetServerCursorId) { + cachedTVPHeaders = ByteBuffer.allocate(stagingBuffer.capacity()).order(stagingBuffer.order()); + cachedTVPHeaders.put(stagingBuffer.array(), 0, stagingBuffer.position()); + + cachedCommand = this.command; + + cachedRequestComplete = command.getRequestComplete(); + cachedInterruptsEnabled = command.getInterruptsEnabled(); + cachedProcessedResponse = command.getProcessedResponse(); + + tdsWritterCached = true; + + if (sourceResultSet.isForwardOnly()) { + sourceResultSet.setFetchSize(1); + } + } + } + } + Map columnMetadata = value.getColumnMetadata(); Iterator> columnsIterator; while (value.next()) { + + // restore command and TDS header, which have been overwritten by value.next() + if (tdsWritterCached) { + command = cachedCommand; + + stagingBuffer.clear(); + logBuffer.clear(); + writeBytes(cachedTVPHeaders.array(), 0, cachedTVPHeaders.position()); + } + Object[] rowData = value.getRowData(); // ROW @@ -4765,190 +4883,342 @@ void writeTVPRows(TVP value) throws SQLServerException { } } } - switch (jdbcType) { - case BIGINT: - if (null == currentColumnStringValue) - writeByte((byte) 0); - else { - writeByte((byte) 8); - writeLong(Long.valueOf(currentColumnStringValue).longValue()); - } - break; + writeInternalTVPRowValues(jdbcType, currentColumnStringValue, currentObject, columnPair, false); + currentColumn++; + } - case BIT: - if (null == currentColumnStringValue) - writeByte((byte) 0); - else { - writeByte((byte) 1); - writeByte((byte) (Boolean.valueOf(currentColumnStringValue).booleanValue() ? 1 : 0)); - } - break; + // send this row, read its response (throw exception in case of errors) and reset command status + if (tdsWritterCached) { + // TVP_END_TOKEN + writeByte((byte) 0x00); - case INTEGER: - if (null == currentColumnStringValue) - writeByte((byte) 0); - else { - writeByte((byte) 4); - writeInt(Integer.valueOf(currentColumnStringValue).intValue()); - } - break; + writePacket(TDS.STATUS_BIT_EOM); - case SMALLINT: - case TINYINT: - if (null == currentColumnStringValue) - writeByte((byte) 0); - else { - writeByte((byte) 2); // length of datatype - writeShort(Short.valueOf(currentColumnStringValue).shortValue()); - } - break; + TDSReader tdsReader = tdsChannel.getReader(command); + int tokenType = tdsReader.peekTokenType(); - case DECIMAL: - case NUMERIC: - if (null == currentColumnStringValue) - writeByte((byte) 0); - else { - writeByte((byte) TDSWriter.BIGDECIMAL_MAX_LENGTH); // maximum length - BigDecimal bdValue = new BigDecimal(currentColumnStringValue); + if (TDS.TDS_ERR == tokenType) { + StreamError databaseError = new StreamError(); + databaseError.setFromTDS(tdsReader); - // setScale of all BigDecimal value based on metadata sent - bdValue = bdValue.setScale(columnPair.getValue().scale); - byte[] valueBytes = DDC.convertBigDecimalToBytes(bdValue, bdValue.scale()); + SQLServerException.makeFromDatabaseError(con, null, databaseError.getMessage(), databaseError, false); + } - // 1-byte for sign and 16-byte for integer - byte[] byteValue = new byte[17]; + command.setInterruptsEnabled(true); + command.setRequestComplete(false); + } + } + } - // removing the precision and scale information from the valueBytes array - System.arraycopy(valueBytes, 2, byteValue, 0, valueBytes.length - 2); - writeBytes(byteValue); - } - break; + // reset command status which have been overwritten + if (tdsWritterCached) { + command.setRequestComplete(cachedRequestComplete); + command.setInterruptsEnabled(cachedInterruptsEnabled); + command.setProcessedResponse(cachedProcessedResponse); + } + else { + // TVP_END_TOKEN + writeByte((byte) 0x00); + } + } - case DOUBLE: - if (null == currentColumnStringValue) - writeByte((byte) 0); // len of data bytes - else { - writeByte((byte) 8); // len of data bytes - long bits = Double.doubleToLongBits(Double.valueOf(currentColumnStringValue).doubleValue()); - long mask = 0xFF; - int nShift = 0; - for (int i = 0; i < 8; i++) { - writeByte((byte) ((bits & mask) >> nShift)); - nShift += 8; - mask = mask << 8; - } - } - break; + private void writeInternalTVPRowValues(JDBCType jdbcType, + String currentColumnStringValue, + Object currentObject, + Map.Entry columnPair, + boolean isSqlVariant) throws SQLServerException { + boolean isShortValue, isNull; + int dataLength; + switch (jdbcType) { + case BIGINT: + if (null == currentColumnStringValue) + writeByte((byte) 0); + else { + if (isSqlVariant) { + writeTVPSqlVariantHeader(10, TDSType.INT8.byteValue(), (byte) 0); + } + else { + writeByte((byte) 8); + } + writeLong(Long.valueOf(currentColumnStringValue).longValue()); + } + break; - case FLOAT: - case REAL: - if (null == currentColumnStringValue) - writeByte((byte) 0); // actual length (0 == null) - else { - writeByte((byte) 4); // actual length - writeInt(Float.floatToRawIntBits(Float.valueOf(currentColumnStringValue).floatValue())); - } - break; + case BIT: + if (null == currentColumnStringValue) + writeByte((byte) 0); + else { + if (isSqlVariant) + writeTVPSqlVariantHeader(3, TDSType.BIT1.byteValue(), (byte) 0); + else + writeByte((byte) 1); + writeByte((byte) (Boolean.valueOf(currentColumnStringValue).booleanValue() ? 1 : 0)); + } + break; - case DATE: - case TIME: - case TIMESTAMP: - case DATETIMEOFFSET: - case TIMESTAMP_WITH_TIMEZONE: - case TIME_WITH_TIMEZONE: - case CHAR: - case VARCHAR: - case NCHAR: - case NVARCHAR: - isShortValue = (2 * columnPair.getValue().precision) <= DataTypes.SHORT_VARTYPE_MAX_BYTES; - isNull = (null == currentColumnStringValue); - dataLength = isNull ? 0 : currentColumnStringValue.length() * 2; - if (!isShortValue) { - // check null - if (isNull) - // Null header for v*max types is 0xFFFFFFFFFFFFFFFF. - writeLong(0xFFFFFFFFFFFFFFFFL); - else if (DataTypes.UNKNOWN_STREAM_LENGTH == dataLength) - // Append v*max length. - // UNKNOWN_PLP_LEN is 0xFFFFFFFFFFFFFFFE - writeLong(0xFFFFFFFFFFFFFFFEL); - else - // For v*max types with known length, length is - writeLong(dataLength); - if (!isNull) { - if (dataLength > 0) { - writeInt(dataLength); - writeString(currentColumnStringValue); - } - // Send the terminator PLP chunk. - writeInt(0); - } - } - else { - if (isNull) - writeShort((short) -1); // actual len - else { - writeShort((short) dataLength); - writeString(currentColumnStringValue); - } - } + case INTEGER: + if (null == currentColumnStringValue) + writeByte((byte) 0); + else { + if (!isSqlVariant) + writeByte((byte) 4); + else + writeTVPSqlVariantHeader(6, TDSType.INT4.byteValue(), (byte) 0); + writeInt(Integer.valueOf(currentColumnStringValue).intValue()); + } + break; + + case SMALLINT: + case TINYINT: + if (null == currentColumnStringValue) + writeByte((byte) 0); + else { + if (isSqlVariant) { + writeTVPSqlVariantHeader(6, TDSType.INT4.byteValue(), (byte) 0); + writeInt(Integer.valueOf(currentColumnStringValue)); + } + else { + writeByte((byte) 2); // length of datatype + writeShort(Short.valueOf(currentColumnStringValue).shortValue()); + } + } + break; + + case DECIMAL: + case NUMERIC: + if (null == currentColumnStringValue) + writeByte((byte) 0); + else { + if (isSqlVariant) { + writeTVPSqlVariantHeader(21, TDSType.DECIMALN.byteValue(), (byte) 2); + writeByte((byte) 38); // scale (byte)variantType.getScale() + writeByte((byte) 4); // scale (byte)variantType.getScale() + } + else { + writeByte((byte) TDSWriter.BIGDECIMAL_MAX_LENGTH); // maximum length + } + BigDecimal bdValue = new BigDecimal(currentColumnStringValue); + + /* + * setScale of all BigDecimal value based on metadata as scale is not sent seperately for individual value. Use the rounding used + * in Server. Say, for BigDecimal("0.1"), if scale in metdadata is 0, then ArithmeticException would be thrown if RoundingMode is + * not set + */ + bdValue = bdValue.setScale(columnPair.getValue().scale, RoundingMode.HALF_UP); + + byte[] valueBytes = DDC.convertBigDecimalToBytes(bdValue, bdValue.scale()); + + // 1-byte for sign and 16-byte for integer + byte[] byteValue = new byte[17]; + + // removing the precision and scale information from the valueBytes array + System.arraycopy(valueBytes, 2, byteValue, 0, valueBytes.length - 2); + writeBytes(byteValue); + } + break; + + case DOUBLE: + if (null == currentColumnStringValue) + writeByte((byte) 0); // len of data bytes + else { + if (isSqlVariant) { + writeTVPSqlVariantHeader(10, TDSType.FLOAT8.byteValue(), (byte) 0); + writeDouble(Double.valueOf(currentColumnStringValue)); + break; + } + writeByte((byte) 8); // len of data bytes + long bits = Double.doubleToLongBits(Double.valueOf(currentColumnStringValue).doubleValue()); + long mask = 0xFF; + int nShift = 0; + for (int i = 0; i < 8; i++) { + writeByte((byte) ((bits & mask) >> nShift)); + nShift += 8; + mask = mask << 8; + } + } + break; + + case FLOAT: + case REAL: + if (null == currentColumnStringValue) + writeByte((byte) 0); + else { + if (isSqlVariant) { + writeTVPSqlVariantHeader(6, TDSType.FLOAT4.byteValue(), (byte) 0); + writeInt(Float.floatToRawIntBits(Float.valueOf(currentColumnStringValue).floatValue())); + } + else { + writeByte((byte) 4); + writeInt(Float.floatToRawIntBits(Float.valueOf(currentColumnStringValue).floatValue())); + } + } + break; + + case DATE: + case TIME: + case TIMESTAMP: + case DATETIMEOFFSET: + case DATETIME: + case SMALLDATETIME: + case TIMESTAMP_WITH_TIMEZONE: + case TIME_WITH_TIMEZONE: + case CHAR: + case VARCHAR: + case NCHAR: + case NVARCHAR: + case LONGVARCHAR: + case LONGNVARCHAR: + case SQLXML: + isShortValue = (2L * columnPair.getValue().precision) <= DataTypes.SHORT_VARTYPE_MAX_BYTES; + isNull = (null == currentColumnStringValue); + dataLength = isNull ? 0 : currentColumnStringValue.length() * 2; + if (!isShortValue) { + // check null + if (isNull) { + // Null header for v*max types is 0xFFFFFFFFFFFFFFFF. + writeLong(0xFFFFFFFFFFFFFFFFL); + } + else if (isSqlVariant) { + // for now we send as bigger type, but is sendStringParameterAsUnicoe is set to false we can't send nvarchar + // since we are writing as nvarchar we need to write as tdstype.bigvarchar value because if we + // want to supprot varchar(8000) it becomes as nvarchar, 8000*2 therefore we should send as longvarchar, + // but we cannot send more than 8000 cause sql_variant datatype in sql server does not support it. + // then throw exception if user is sending more than that + if (dataLength > 2 * DataTypes.SHORT_VARTYPE_MAX_BYTES) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidStringValue")); + throw new SQLServerException(null, form.format(new Object[] {}), null, 0, false); + } + int length = currentColumnStringValue.length(); + writeTVPSqlVariantHeader(9 + length, TDSType.BIGVARCHAR.byteValue(), (byte) 0x07); + SQLCollation col = con.getDatabaseCollation(); + // write collation for sql variant + writeInt(col.getCollationInfo()); + writeByte((byte) col.getCollationSortID()); + writeShort((short) (length)); + writeBytes(currentColumnStringValue.getBytes()); + break; + } + + else if (DataTypes.UNKNOWN_STREAM_LENGTH == dataLength) + // Append v*max length. + // UNKNOWN_PLP_LEN is 0xFFFFFFFFFFFFFFFE + writeLong(0xFFFFFFFFFFFFFFFEL); + else + // For v*max types with known length, length is + writeLong(dataLength); + if (!isNull) { + if (dataLength > 0) { + writeInt(dataLength); + writeString(currentColumnStringValue); + } + // Send the terminator PLP chunk. + writeInt(0); + } + } + else { + if (isNull) + writeShort((short) -1); // actual len + else { + if (isSqlVariant) { + // for now we send as bigger type, but is sendStringParameterAsUnicoe is set to false we can't send nvarchar + // check for this + int length = currentColumnStringValue.length() * 2; + writeTVPSqlVariantHeader(9 + length, TDSType.NVARCHAR.byteValue(), (byte) 7); + SQLCollation col = con.getDatabaseCollation(); + // write collation for sql variant + writeInt(col.getCollationInfo()); + writeByte((byte) col.getCollationSortID()); + int stringLength = currentColumnStringValue.length(); + byte[] typevarlen = new byte[2]; + typevarlen[0] = (byte) (2 * stringLength & 0xFF); + typevarlen[1] = (byte) ((2 * stringLength >> 8) & 0xFF); + writeBytes(typevarlen); + writeString(currentColumnStringValue); break; + } + else { + writeShort((short) dataLength); + writeString(currentColumnStringValue); + } + } + } + break; - case BINARY: - case VARBINARY: - // Handle conversions as done in other types. - isShortValue = columnPair.getValue().precision <= DataTypes.SHORT_VARTYPE_MAX_BYTES; - isNull = (null == currentObject); + case BINARY: + case VARBINARY: + case LONGVARBINARY: + // Handle conversions as done in other types. + isShortValue = columnPair.getValue().precision <= DataTypes.SHORT_VARTYPE_MAX_BYTES; + isNull = (null == currentObject); + if (currentObject instanceof String) + dataLength = isNull ? 0 : (toByteArray(currentObject.toString())).length; + else + dataLength = isNull ? 0 : ((byte[]) currentObject).length; + if (!isShortValue) { + // check null + if (isNull) + // Null header for v*max types is 0xFFFFFFFFFFFFFFFF. + writeLong(0xFFFFFFFFFFFFFFFFL); + else if (DataTypes.UNKNOWN_STREAM_LENGTH == dataLength) + // Append v*max length. + // UNKNOWN_PLP_LEN is 0xFFFFFFFFFFFFFFFE + writeLong(0xFFFFFFFFFFFFFFFEL); + else + // For v*max types with known length, length is + writeLong(dataLength); + if (!isNull) { + if (dataLength > 0) { + writeInt(dataLength); if (currentObject instanceof String) - dataLength = isNull ? 0 : (toByteArray(currentObject.toString())).length; + writeBytes(toByteArray(currentObject.toString())); else - dataLength = isNull ? 0 : ((byte[]) currentObject).length; - if (!isShortValue) { - // check null - if (isNull) - // Null header for v*max types is 0xFFFFFFFFFFFFFFFF. - writeLong(0xFFFFFFFFFFFFFFFFL); - else if (DataTypes.UNKNOWN_STREAM_LENGTH == dataLength) - // Append v*max length. - // UNKNOWN_PLP_LEN is 0xFFFFFFFFFFFFFFFE - writeLong(0xFFFFFFFFFFFFFFFEL); - else - // For v*max types with known length, length is - writeLong(dataLength); - if (!isNull) { - if (dataLength > 0) { - writeInt(dataLength); - if (currentObject instanceof String) - writeBytes(toByteArray(currentObject.toString())); - else - writeBytes((byte[]) currentObject); - } - // Send the terminator PLP chunk. - writeInt(0); - } - } - else { - if (isNull) - writeShort((short) -1); // actual len - else { - writeShort((short) dataLength); - if (currentObject instanceof String) - writeBytes(toByteArray(currentObject.toString())); - else - writeBytes((byte[]) currentObject); - } - } - break; - - default: - assert false : "Unexpected JDBC type " + jdbcType.toString(); + writeBytes((byte[]) currentObject); + } + // Send the terminator PLP chunk. + writeInt(0); } - currentColumn++; } - } + else { + if (isNull) + writeShort((short) -1); // actual len + else { + writeShort((short) dataLength); + if (currentObject instanceof String) + writeBytes(toByteArray(currentObject.toString())); + else + writeBytes((byte[]) currentObject); + } + } + break; + case SQL_VARIANT: + boolean isShiloh = (8 >= con.getServerMajorVersion()); + if (isShiloh) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_SQLVariantSupport")); + throw new SQLServerException(null, form.format(new Object[] {}), null, 0, false); + } + JDBCType internalJDBCType; + JavaType javaType = JavaType.of(currentObject); + internalJDBCType = javaType.getJDBCType(SSType.UNKNOWN, jdbcType); + writeInternalTVPRowValues(internalJDBCType, currentColumnStringValue, currentObject, columnPair, true); + break; + default: + assert false : "Unexpected JDBC type " + jdbcType.toString(); } - // TVP_END_TOKEN - writeByte((byte) 0x00); + } + + /** + * writes Header for sql_variant for TVP + * @param length + * @param tdsType + * @param probBytes + * @throws SQLServerException + */ + private void writeTVPSqlVariantHeader(int length, + byte tdsType, + byte probBytes) throws SQLServerException { + writeInt(length); + writeByte(tdsType); + writeByte(probBytes); } private static byte[] toByteArray(String s) { @@ -4962,13 +5232,11 @@ void writeTVPColumnMetaData(TVP value) throws SQLServerException { writeShort((short) value.getTVPColumnCount()); Map columnMetadata = value.getColumnMetadata(); - Iterator> columnsIterator = columnMetadata.entrySet().iterator(); /* * TypeColumnMetaData = UserType Flags TYPE_INFO ColName ; */ - while (columnsIterator.hasNext()) { - Map.Entry pair = columnsIterator.next(); + for (Entry pair : columnMetadata.entrySet()) { JDBCType jdbcType = JDBCType.of(pair.getValue().javaSqlType); boolean useServerDefault = pair.getValue().useServerDefault; // ULONG ; UserType of column @@ -5029,22 +5297,26 @@ void writeTVPColumnMetaData(TVP value) throws SQLServerException { case TIME: case TIMESTAMP: case DATETIMEOFFSET: + case DATETIME: + case SMALLDATETIME: case TIMESTAMP_WITH_TIMEZONE: case TIME_WITH_TIMEZONE: case CHAR: case VARCHAR: case NCHAR: case NVARCHAR: + case LONGVARCHAR: + case LONGNVARCHAR: + case SQLXML: writeByte(TDSType.NVARCHAR.byteValue()); - isShortValue = (2 * pair.getValue().precision) <= DataTypes.SHORT_VARTYPE_MAX_BYTES; + isShortValue = (2L * pair.getValue().precision) <= DataTypes.SHORT_VARTYPE_MAX_BYTES; // Use PLP encoding on Yukon and later with long values - if (!isShortValue) // PLP + if (!isShortValue) // PLP { // Handle Yukon v*max type header here. writeShort((short) 0xFFFF); con.getDatabaseCollation().writeCollation(this); - } - else // non PLP + } else // non PLP { writeShort((short) DataTypes.SHORT_VARTYPE_MAX_BYTES); con.getDatabaseCollation().writeCollation(this); @@ -5054,15 +5326,21 @@ void writeTVPColumnMetaData(TVP value) throws SQLServerException { case BINARY: case VARBINARY: + case LONGVARBINARY: writeByte(TDSType.BIGVARBINARY.byteValue()); isShortValue = pair.getValue().precision <= DataTypes.SHORT_VARTYPE_MAX_BYTES; // Use PLP encoding on Yukon and later with long values - if (!isShortValue) // PLP + if (!isShortValue) // PLP // Handle Yukon v*max type header here. writeShort((short) 0xFFFF); - else // non PLP + else // non PLP writeShort((short) DataTypes.SHORT_VARTYPE_MAX_BYTES); break; + case SQL_VARIANT: + writeByte(TDSType.SQL_VARIANT.byteValue()); + writeInt(TDS.SQL_VARIANT_LENGTH);// write length of sql variant 8009 + + break; default: assert false : "Unexpected JDBC type " + jdbcType.toString(); @@ -5082,7 +5360,7 @@ void writeTvpOrderUnique(TVP value) throws SQLServerException { Map columnMetadata = value.getColumnMetadata(); Iterator> columnsIterator = columnMetadata.entrySet().iterator(); - LinkedList columnList = new LinkedList(); + LinkedList columnList = new LinkedList<>(); while (columnsIterator.hasNext()) { byte flags = 0; @@ -5308,7 +5586,7 @@ void writeRPCDateTime(String sName, int subSecondNanos, boolean bOut) throws SQLServerException { assert (subSecondNanos >= 0) && (subSecondNanos < Nanos.PER_SECOND) : "Invalid subNanoSeconds value: " + subSecondNanos; - assert (cal != null) || (cal == null && subSecondNanos == 0) : "Invalid subNanoSeconds value when calendar is null: " + subSecondNanos; + assert (cal != null) || (subSecondNanos == 0) : "Invalid subNanoSeconds value when calendar is null: " + subSecondNanos; writeRPCNameValType(sName, bOut, TDSType.DATETIMEN); writeByte((byte) 8); // max length of datatype @@ -5448,7 +5726,7 @@ void writeEncryptedRPCDateTime(String sName, boolean bOut, JDBCType jdbcType) throws SQLServerException { assert (subSecondNanos >= 0) && (subSecondNanos < Nanos.PER_SECOND) : "Invalid subNanoSeconds value: " + subSecondNanos; - assert (cal != null) || (cal == null && subSecondNanos == 0) : "Invalid subNanoSeconds value when calendar is null: " + subSecondNanos; + assert (cal != null) || (subSecondNanos == 0) : "Invalid subNanoSeconds value when calendar is null: " + subSecondNanos; writeRPCNameValType(sName, bOut, TDSType.BIGVARBINARY); @@ -5630,6 +5908,7 @@ void writeRPCDateTimeOffset(String sName, writeShort((short) minutesOffset); } + /** * Returns subSecondNanos rounded to the maximum precision supported. The maximum fractional scale is MAX_FRACTIONAL_SECONDS_SCALE(7). Eg1: if you * pass 456,790,123 the function would return 456,790,100 Eg2: if you pass 456,790,150 the function would return 456,790,200 Eg3: if you pass @@ -6067,7 +6346,7 @@ void writeRPCInputStream(String sName, if (streamLength >= maxStreamLength) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLength")); - Object[] msgArgs = {Long.valueOf(streamLength)}; + Object[] msgArgs = {streamLength}; SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), "", true); } @@ -6431,7 +6710,10 @@ synchronized final boolean readPacket() throws SQLServerException { // Make header size is properly bounded and compute length of the packet payload. if (packetLength < TDS.PACKET_HEADER_SIZE || packetLength > con.getTDSPacketSize()) { - logger.warning(toString() + " TDS header contained invalid packet length:" + packetLength + "; packet size:" + con.getTDSPacketSize()); + if (logger.isLoggable(Level.WARNING)) { + logger.warning( + toString() + " TDS header contained invalid packet length:" + packetLength + "; packet size:" + con.getTDSPacketSize()); + } throwInvalidTDS(); } @@ -6550,9 +6832,6 @@ final short peekStatusFlag() throws SQLServerException { return value; } - // as per TDS protocol, TDS_DONE packet should always be followed by status flag - // throw exception if status packet is not available - throwInvalidTDS(); return 0; } @@ -6689,7 +6968,9 @@ final Object readDecimal(int valueLength, JDBCType jdbcType, StreamType streamType) throws SQLServerException { if (valueLength > valueBytes.length) { - logger.warning(toString() + " Invalid value length:" + valueLength); + if (logger.isLoggable(Level.WARNING)) { + logger.warning(toString() + " Invalid value length:" + valueLength); + } throwInvalidTDS(); } @@ -7055,19 +7336,28 @@ final class TimeoutTimer implements Runnable { private final int timeoutSeconds; private final TDSCommand command; private volatile Future task; - + private static final ExecutorService executor = Executors.newCachedThreadPool(new ThreadFactory() { - private final ThreadGroup tg = new ThreadGroup(threadGroupName); - private final String threadNamePrefix = tg.getName() + "-"; + private final AtomicReference tgr = new AtomicReference<>(); private final AtomicInteger threadNumber = new AtomicInteger(0); + @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(tg, r, threadNamePrefix + threadNumber.incrementAndGet()); + public Thread newThread(Runnable r) + { + ThreadGroup tg = tgr.get(); + + if (tg == null || tg.isDestroyed()) + { + tg = new ThreadGroup(threadGroupName); + tgr.set(tg); + } + + Thread t = new Thread(tg, r, tg.getName() + "-" + threadNumber.incrementAndGet()); t.setDaemon(true); return t; } }); - + private volatile boolean canceled = false; TimeoutTimer(int timeoutSeconds, @@ -7088,7 +7378,7 @@ final void stop() { canceled = true; } - public void run() { + public void run() { int secondsRemaining = timeoutSeconds; try { // Poll every second while time is left on the timer. @@ -7102,6 +7392,8 @@ public void run() { while (--secondsRemaining > 0); } catch (InterruptedException e) { + // re-interrupt the current thread, in order to restore the thread's interrupt status. + Thread.currentThread().interrupt(); return; } @@ -7181,6 +7473,10 @@ void startQueryTimeoutTimer(boolean updateInterrupts) { // Volatile ensures visibility to execution thread and interrupt thread private volatile TDSWriter tdsWriter; private volatile TDSReader tdsReader; + + protected TDSWriter getTDSWriter(){ + return tdsWriter; + } // Lock to ensure atomicity when manipulating more than one of the following // shared interrupt state variables below. @@ -7193,6 +7489,16 @@ void startQueryTimeoutTimer(boolean updateInterrupts) { // interrupt is ignored. private volatile boolean interruptsEnabled = false; + protected boolean getInterruptsEnabled() { + return interruptsEnabled; + } + + protected void setInterruptsEnabled(boolean interruptsEnabled) { + synchronized (interruptLock) { + this.interruptsEnabled = interruptsEnabled; + } + } + // Flag set to indicate that an interrupt has happened. private volatile boolean wasInterrupted = false; @@ -7209,6 +7515,16 @@ void startQueryTimeoutTimer(boolean updateInterrupts) { // After the request is complete, the interrupting thread must send the attention signal. private volatile boolean requestComplete; + protected boolean getRequestComplete() { + return requestComplete; + } + + protected void setRequestComplete(boolean requestComplete) { + synchronized (interruptLock) { + this.requestComplete = requestComplete; + } + } + // Flag set when an attention signal has been sent to the server, indicating that a // TDS packet containing the attention ack message is to be expected in the response. // This flag is cleared after the attention ack message has been received and processed. @@ -7223,6 +7539,16 @@ boolean attentionPending() { // ENVCHANGE notifications. private volatile boolean processedResponse; + protected boolean getProcessedResponse() { + return processedResponse; + } + + protected void setProcessedResponse(boolean processedResponse) { + synchronized (interruptLock) { + this.processedResponse = processedResponse; + } + } + // Flag set when this command's response is ready to be read from the server and cleared // after its response has been received, but not necessarily processed, up to and including // any attention ack. The command's response is read either on demand as it is processed, @@ -7370,7 +7696,9 @@ final void close() { // then assume that no attention ack is forthcoming from the server and // terminate the connection to prevent any other command from executing. if (attentionPending) { - logger.severe(this + ": expected attn ack missing or not processed; terminating connection..."); + if (logger.isLoggable(Level.SEVERE)) { + logger.severe(this.toString() + ": expected attn ack missing or not processed; terminating connection..."); + } try { tdsReader.throwInvalidTDS(); @@ -7700,6 +8028,8 @@ abstract class UninterruptableTDSCommand extends TDSCommand { final void interrupt(String reason) throws SQLServerException { // Interrupting an uninterruptable command is a no-op. That is, // it can happen, but it should have no effect. - logger.finest(toString() + " Ignoring interrupt of uninterruptable TDS command; Reason:" + reason); + if (logger.isLoggable(Level.FINEST)) { + logger.finest(toString() + " Ignoring interrupt of uninterruptable TDS command; Reason:" + reason); + } } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/JaasConfiguration.java b/src/main/java/com/microsoft/sqlserver/jdbc/JaasConfiguration.java new file mode 100644 index 000000000..6443724fd --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/JaasConfiguration.java @@ -0,0 +1,71 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc; + +import java.util.HashMap; +import java.util.Map; + +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; + +/** + * This class overrides JAAS Configuration and always provide a configuration is not defined for default configuration. + */ +public class JaasConfiguration extends Configuration { + + private final Configuration delegate; + private AppConfigurationEntry[] defaultValue; + + private static AppConfigurationEntry[] generateDefaultConfiguration() { + if (Util.isIBM()) { + Map confDetailsWithoutPassword = new HashMap<>(); + confDetailsWithoutPassword.put("useDefaultCcache", "true"); + Map confDetailsWithPassword = new HashMap<>(); + // We generated a two configurations fallback that is suitable for password and password-less authentication + // See https://www.ibm.com/support/knowledgecenter/SSYKE2_8.0.0/com.ibm.java.security.component.80.doc/security-component/jgssDocs/jaas_login_user.html + final String ibmLoginModule = "com.ibm.security.auth.module.Krb5LoginModule"; + return new AppConfigurationEntry[] { + new AppConfigurationEntry(ibmLoginModule, AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT, confDetailsWithoutPassword), + new AppConfigurationEntry(ibmLoginModule, AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT, confDetailsWithPassword)}; + } + else { + Map confDetails = new HashMap<>(); + confDetails.put("useTicketCache", "true"); + return new AppConfigurationEntry[] {new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, confDetails)}; + } + } + + /** + * Package protected constructor. + * + * @param delegate + * a possibly null delegate + */ + JaasConfiguration(Configuration delegate) { + this.delegate = delegate; + this.defaultValue = generateDefaultConfiguration(); + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + AppConfigurationEntry[] conf = delegate == null ? null : delegate.getAppConfigurationEntry(name); + // We return our configuration only if user requested default one + // In case where user did request another JAAS Configuration name, we expect he knows what he is doing. + if (conf == null && name.equals(SQLServerDriverStringProperty.JAAS_CONFIG_NAME.getDefaultValue())) { + return defaultValue; + } + return conf; + } + + @Override + public void refresh() { + if (null != delegate) + delegate.refresh(); + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java b/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java index abbead0d6..aa714ca8d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java @@ -8,17 +8,22 @@ package com.microsoft.sqlserver.jdbc; +import java.lang.reflect.Method; import java.net.IDN; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.security.AccessControlContext; import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; -import java.util.HashMap; -import java.util.Map; +import java.text.MessageFormat; +import java.util.Locale; import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.naming.NamingException; import javax.security.auth.Subject; -import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.Configuration; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; @@ -30,11 +35,12 @@ import org.ietf.jgss.GSSName; import org.ietf.jgss.Oid; +import com.microsoft.sqlserver.jdbc.dns.DNSKerberosLocator; + /** * KerbAuthentication for int auth. */ final class KerbAuthentication extends SSPIAuthentication { - private final static String CONFIGNAME = "SQLJDBCDriver"; private final static java.util.logging.Logger authLogger = java.util.logging.Logger .getLogger("com.microsoft.sqlserver.jdbc.internals.KerbAuthentication"); @@ -52,79 +58,9 @@ final class KerbAuthentication extends SSPIAuthentication { private boolean reconnecting = false; static { - // The driver on load will look to see if there is a configuration set for the SQLJDBCDriver, if not it will install its - // own configuration. Note it is possible that there is a configuration exists but it does not contain a configuration entry - // for the driver in that case, we will override the configuration but will flow the configuration requests to existing - // config for anything other than SQLJDBCDriver - // - class SQLJDBCDriverConfig extends Configuration { - Configuration current = null; - AppConfigurationEntry[] driverConf; - - SQLJDBCDriverConfig() { - try { - current = Configuration.getConfiguration(); - } - catch (SecurityException e) { - // if we cant get the configuration, it is likely that no configuration has been specified. So go ahead and set the config - authLogger.finer(toString() + " No configurations provided, setting driver default"); - } - AppConfigurationEntry[] config = null; - - if (null != current) { - config = current.getAppConfigurationEntry(CONFIGNAME); - } - // If there is user provided configuration we leave use that and not install our configuration - if (null == config) { - if (authLogger.isLoggable(Level.FINER)) - authLogger.finer(toString() + " SQLJDBCDriver configuration entry is not provided, setting driver default"); - - AppConfigurationEntry appConf; - if (Util.isIBM()) { - Map confDetails = new HashMap(); - confDetails.put("useDefaultCcache", "true"); - confDetails.put("moduleBanner", "false"); - appConf = new AppConfigurationEntry("com.ibm.security.auth.module.Krb5LoginModule", - AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, confDetails); - if (authLogger.isLoggable(Level.FINER)) - authLogger.finer(toString() + " Setting IBM Krb5LoginModule"); - } - else { - Map confDetails = new HashMap(); - confDetails.put("useTicketCache", "true"); - confDetails.put("doNotPrompt", "true"); - appConf = new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", - AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, confDetails); - if (authLogger.isLoggable(Level.FINER)) - authLogger.finer(toString() + " Setting Sun Krb5LoginModule"); - } - driverConf = new AppConfigurationEntry[1]; - driverConf[0] = appConf; - Configuration.setConfiguration(this); - } - - } - - public AppConfigurationEntry[] getAppConfigurationEntry(String name) { - // we should only handle anything that is related to our part, everything else is handled by the configuration - // already existing configuration if there is one. - if (name.equals(CONFIGNAME)) { - return driverConf; - } - else { - if (null != current) - return current.getAppConfigurationEntry(name); - else - return null; - } - } - - public void refresh() { - if (null != current) - current.refresh(); - } - } - SQLJDBCDriverConfig driverconfig = new SQLJDBCDriverConfig(); + // Overrides the default JAAS configuration loader. + // This one will forward to the default one in all cases but the default configuration is empty. + Configuration.setConfiguration(new JaasConfiguration(Configuration.getConfiguration())); } /** @@ -153,18 +89,39 @@ private void intAuthInit() throws SQLServerException { peerContext.requestInteg(true); } else { + String configName = con.activeConnectionProperties.getProperty(SQLServerDriverStringProperty.JAAS_CONFIG_NAME.toString(), + SQLServerDriverStringProperty.JAAS_CONFIG_NAME.getDefaultValue()); + Subject currentSubject; + KerbCallback callback = new KerbCallback(con); try { AccessControlContext context = AccessController.getContext(); currentSubject = Subject.getSubject(context); if (null == currentSubject) { - lc = new LoginContext(CONFIGNAME); + lc = new LoginContext(configName, callback); lc.login(); // per documentation LoginContext will instantiate a new subject. currentSubject = lc.getSubject(); } } catch (LoginException le) { - con.terminate(SQLServerException.DRIVER_ERROR_NONE, SQLServerException.getErrString("R_integratedAuthenticationFailed"), le); + if (authLogger.isLoggable(Level.FINE)) { + authLogger.fine(toString() + "Failed to login using Kerberos due to " + le.getClass().getName() + ":" + le.getMessage()); + } + try { + // Not very clean since it raises an Exception, but we are sure we are cleaning well everything + con.terminate(SQLServerException.DRIVER_ERROR_NONE, SQLServerException.getErrString("R_integratedAuthenticationFailed"), le); + } catch (SQLServerException alwaysTriggered) { + String message = MessageFormat.format(SQLServerException.getErrString("R_kerberosLoginFailed"), + alwaysTriggered.getMessage(), le.getClass().getName(), le.getMessage()); + if (callback.getUsernameRequested() != null) { + message = MessageFormat.format(SQLServerException.getErrString("R_kerberosLoginFailedForUsername"), + callback.getUsernameRequested(), message); + } + // By throwing Exception with LOGON_FAILED -> we avoid looping for connection + // In this case, authentication will never work anyway -> fail fast + throw new SQLServerException(message, alwaysTriggered.getSQLState(), SQLServerException.LOGON_FAILED, le); + } + return; } if (authLogger.isLoggable(Level.FINER)) { @@ -226,7 +183,9 @@ private byte[] intAuthHandShake(byte[] pin, } else if (null == byteToken) { // The documentation is not clear on when this can happen but it does say this could happen - authLogger.info(toString() + "byteToken is null in initSecContext."); + if (authLogger.isLoggable(Level.INFO)) { + authLogger.info(toString() + "byteToken is null in initSecContext."); + } con.terminate(SQLServerException.DRIVER_ERROR_NONE, SQLServerException.getErrString("R_integratedAuthenticationFailed")); } return byteToken; @@ -272,6 +231,7 @@ private String makeSpn(String server, // Get user provided SPN string; if not provided then build the generic one String userSuppliedServerSpn = con.activeConnectionProperties.getProperty(SQLServerDriverStringProperty.SERVER_SPN.toString()); + String spn; if (null != userSuppliedServerSpn) { // serverNameAsACE is true, translate the user supplied serverSPN to ASCII if (con.serverNameAsACE()) { @@ -285,11 +245,156 @@ private String makeSpn(String server, else { spn = makeSpn(address, port); } - + this.spn = enrichSpnWithRealm(spn, null == userSuppliedServerSpn); + if (!this.spn.equals(spn) && authLogger.isLoggable(Level.FINER)){ + authLogger.finer(toString() + "SPN enriched: " + spn + " := " + this.spn); + } // KerbAuthentication object is created for every connection attempt. Hence it is not necessary to reset these values as the new one will be // created for next connect. threadImpersonationSubject = loginSubject; this.reconnecting = reconnecting; + } + + private static final Pattern SPN_PATTERN = Pattern.compile("MSSQLSvc/(.*):([^:@]+)(@.+)?", Pattern.CASE_INSENSITIVE); + + private String enrichSpnWithRealm(String spn, + boolean allowHostnameCanonicalization) { + if (spn == null) { + return spn; + } + Matcher m = SPN_PATTERN.matcher(spn); + if (!m.matches()) { + return spn; + } + if (m.group(3) != null) { + // Realm is already present, no need to enrich, the job has already been done + return spn; + } + String dnsName = m.group(1); + String portOrInstance = m.group(2); + RealmValidator realmValidator = getRealmValidator(dnsName); + String realm = findRealmFromHostname(realmValidator, dnsName); + if (realm == null && allowHostnameCanonicalization) { + // We failed, try with canonical host name to find a better match + try { + String canonicalHostName = InetAddress.getByName(dnsName).getCanonicalHostName(); + realm = findRealmFromHostname(realmValidator, canonicalHostName); + // Since we have a match, our hostname is the correct one (for instance of server + // name was an IP), so we override dnsName as well + dnsName = canonicalHostName; + } + catch (UnknownHostException cannotCanonicalize) { + // ignored, but we are in a bad shape + } + } + if (realm == null) { + return spn; + } + else { + StringBuilder sb = new StringBuilder("MSSQLSvc/"); + sb.append(dnsName).append(":").append(portOrInstance).append("@").append(realm.toUpperCase(Locale.ENGLISH)); + return sb.toString(); + } + } + + private static RealmValidator validator; + + /** + * Find a suitable way of validating a REALM for given JVM. + * + * @param hostnameToTest + * an example hostname we are gonna use to test our realm validator. + * @return a not null realm Validator. + */ + static RealmValidator getRealmValidator(String hostnameToTest) { + if (validator != null) { + return validator; + } + // JVM Specific, here Sun/Oracle JVM + try { + Class clz = Class.forName("sun.security.krb5.Config"); + Method getInstance = clz.getMethod("getInstance", new Class[0]); + final Method getKDCList = clz.getMethod("getKDCList", new Class[] {String.class}); + final Object instance = getInstance.invoke(null); + RealmValidator oracleRealmValidator = new RealmValidator() { + + @Override + public boolean isRealmValid(String realm) { + try { + Object ret = getKDCList.invoke(instance, realm); + return ret != null; + } + catch (Exception err) { + return false; + } + } + }; + validator = oracleRealmValidator; + // As explained here: https://github.com/Microsoft/mssql-jdbc/pull/40#issuecomment-281509304 + // The default Oracle Resolution mechanism is not bulletproof + // If it resolves a crappy name, drop it. + if (!validator.isRealmValid("this.might.not.exist." + hostnameToTest)) { + // Our realm validator is well working, return it + authLogger.fine("Kerberos Realm Validator: Using Built-in Oracle Realm Validation method."); + return oracleRealmValidator; + } + authLogger.fine("Kerberos Realm Validator: Detected buggy Oracle Realm Validator, using DNSKerberosLocator."); + } + catch (ReflectiveOperationException notTheRightJVMException) { + // Ignored, we simply are not using the right JVM + authLogger.fine("Kerberos Realm Validator: No Oracle Realm Validator Available, using DNSKerberosLocator."); + } + // No implementation found, default one, not any realm is valid + validator = new RealmValidator() { + @Override + public boolean isRealmValid(String realm) { + try { + return DNSKerberosLocator.isRealmValid(realm); + } + catch (NamingException err) { + return false; + } + } + }; + return validator; + } + + /** + * Try to find a REALM in the different parts of a host name. + * + * @param realmValidator + * a function that return true if REALM is valid and exists + * @param hostname + * the name we are looking a REALM for + * @return the realm if found, null otherwise + */ + private String findRealmFromHostname(RealmValidator realmValidator, + String hostname) { + if (hostname == null) { + return null; + } + int index = 0; + while (index != -1 && index < hostname.length() - 2) { + String realm = hostname.substring(index); + if (authLogger.isLoggable(Level.FINEST)) { + authLogger.finest(toString() + " looking up REALM candidate " + realm); + } + if (realmValidator.isRealmValid(realm)) { + return realm.toUpperCase(); + } + index = hostname.indexOf(".", index + 1); + if (index != -1) { + index = index + 1; + } + } + return null; + } + + /** + * JVM Specific implementation to decide whether a realm is valid or not + */ + interface RealmValidator { + boolean isRealmValid(String realm); } /** diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/KerbCallback.java b/src/main/java/com/microsoft/sqlserver/jdbc/KerbCallback.java new file mode 100644 index 000000000..6f861c4bb --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/KerbCallback.java @@ -0,0 +1,58 @@ +package com.microsoft.sqlserver.jdbc; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Properties; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +public class KerbCallback implements CallbackHandler { + + private final SQLServerConnection con; + private String usernameRequested = null; + + KerbCallback(SQLServerConnection con) { + this.con = con; + } + + private static String getAnyOf(Callback callback, + Properties properties, + String... names) throws UnsupportedCallbackException { + for (String name : names) { + String val = properties.getProperty(name); + if (val != null && !val.trim().isEmpty()) { + return val; + } + } + throw new UnsupportedCallbackException(callback, "Cannot get any of properties: " + Arrays.toString(names) + " from con properties"); + } + + /** + * If a name was retrieved By Kerberos, return it. + * + * @return null if callback was not called or username was not provided + */ + public String getUsernameRequested() { + return usernameRequested; + } + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof NameCallback) { + usernameRequested = getAnyOf(callback, con.activeConnectionProperties, "user", SQLServerDriverStringProperty.USER.name()); + ((NameCallback) callback).setName(usernameRequested); + } else if (callback instanceof PasswordCallback) { + String password = getAnyOf(callback, con.activeConnectionProperties, "password", SQLServerDriverStringProperty.PASSWORD.name()); + ((PasswordCallback) callback).setPassword(password.toCharArray()); + + } else { + throw new UnsupportedCallbackException(callback, "Unrecognized Callback type: " + callback.getClass()); + } + } + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/KeyStoreProviderCommon.java b/src/main/java/com/microsoft/sqlserver/jdbc/KeyStoreProviderCommon.java index 324b1232e..8db7b9a61 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/KeyStoreProviderCommon.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/KeyStoreProviderCommon.java @@ -134,7 +134,7 @@ private static byte[] decryptRSAOAEP(byte[] cipherText, catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException e) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_CEKDecryptionFailed")); Object[] msgArgs = {e.getMessage()}; - throw new SQLServerException(form.format(msgArgs), null); + throw new SQLServerException(form.format(msgArgs), e); } return plainCEK; @@ -156,7 +156,7 @@ private static boolean verifyRSASignature(byte[] hash, catch (InvalidKeyException | NoSuchAlgorithmException | SignatureException e) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidCertificateSignature")); Object[] msgArgs = {masterKeyPath}; - throw new SQLServerException(form.format(msgArgs), null); + throw new SQLServerException(form.format(msgArgs), e); } return verificationSucess; @@ -166,7 +166,7 @@ private static boolean verifyRSASignature(byte[] hash, private static short convertTwoBytesToShort(byte[] input, int index) throws SQLServerException { - short shortVal = -1; + short shortVal; if (index + 1 >= input.length) { throw new SQLServerException(null, SQLServerException.getErrString("R_ByteToShortConversion"), null, 0, false); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/KeyVaultCredential.java b/src/main/java/com/microsoft/sqlserver/jdbc/KeyVaultCredential.java index 70d99421a..d0b5693c3 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/KeyVaultCredential.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/KeyVaultCredential.java @@ -8,13 +8,14 @@ package com.microsoft.sqlserver.jdbc; -import java.util.Map; - -import org.apache.http.Header; -import org.apache.http.message.BasicHeader; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import com.microsoft.aad.adal4j.AuthenticationContext; +import com.microsoft.aad.adal4j.AuthenticationResult; +import com.microsoft.aad.adal4j.ClientCredential; import com.microsoft.azure.keyvault.authentication.KeyVaultCredentials; -import com.microsoft.windowsazure.core.pipeline.filter.ServiceRequestContext; /** * @@ -23,42 +24,46 @@ */ class KeyVaultCredential extends KeyVaultCredentials { - // this is the only supported access token type - // https://msdn.microsoft.com/en-us/library/azure/dn645538.aspx - private final String accessTokenType = "Bearer"; - - SQLServerKeyVaultAuthenticationCallback authenticationCallback = null; String clientId = null; String clientKey = null; - String accessToken = null; - KeyVaultCredential(SQLServerKeyVaultAuthenticationCallback authenticationCallback) { - this.authenticationCallback = authenticationCallback; + KeyVaultCredential(String clientId, + String clientKey) { + this.clientId = clientId; + this.clientKey = clientKey; } - /** - * Authenticates the service request - * - * @param request - * the ServiceRequestContext - * @param challenge - * used to get the accessToken - * @return BasicHeader - */ - @Override - public Header doAuthenticate(ServiceRequestContext request, - Map challenge) { - assert null != challenge; - - String authorization = challenge.get("authorization"); - String resource = challenge.get("resource"); - - accessToken = authenticationCallback.getAccessToken(authorization, resource, ""); - return new BasicHeader("Authorization", accessTokenType + " " + accessToken); + public String doAuthenticate(String authorization, + String resource, + String scope) { + AuthenticationResult token = getAccessTokenFromClientCredentials(authorization, resource, clientId, clientKey); + return token.getAccessToken(); } - void setAccessToken(String accessToken) { - this.accessToken = accessToken; - } + private static AuthenticationResult getAccessTokenFromClientCredentials(String authorization, + String resource, + String clientId, + String clientKey) { + AuthenticationContext context = null; + AuthenticationResult result = null; + ExecutorService service = null; + try { + service = Executors.newFixedThreadPool(1); + context = new AuthenticationContext(authorization, false, service); + ClientCredential credentials = new ClientCredential(clientId, clientKey); + Future future = context.acquireToken(resource, credentials, null); + result = future.get(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + finally { + service.shutdown(); + } + if (result == null) { + throw new RuntimeException("authentication result was null"); + } + return result; + } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/PLPInputStream.java b/src/main/java/com/microsoft/sqlserver/jdbc/PLPInputStream.java index 798f40770..43818afd8 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/PLPInputStream.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/PLPInputStream.java @@ -434,8 +434,7 @@ final static PLPXMLInputStream makeXMLStream(TDSReader tdsReader, return null; PLPXMLInputStream is = new PLPXMLInputStream(tdsReader, payloadLength, getterArgs, dtv); - if (null != is) - is.setLoggingInfo(getterArgs.logContext); + is.setLoggingInfo(getterArgs.logContext); return is; } @@ -465,12 +464,12 @@ int readBytes(byte[] b, // Read/Skip BOM bytes first. When all BOM bytes have been consumed ... if (null == b) { - for (int bomBytesSkipped = 0; bytesRead < maxBytes - && 0 != (bomBytesSkipped = (int) bomStream.skip(maxBytes - bytesRead)); bytesRead += bomBytesSkipped) + for (int bomBytesSkipped; bytesRead < maxBytes + && 0 != (bomBytesSkipped = (int) bomStream.skip(((long) maxBytes) - ((long) bytesRead))); bytesRead += bomBytesSkipped) ; } else { - for (int bomBytesRead = 0; bytesRead < maxBytes + for (int bomBytesRead; bytesRead < maxBytes && -1 != (bomBytesRead = bomStream.read(b, offset + bytesRead, maxBytes - bytesRead)); bytesRead += bomBytesRead) ; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java index 6c12683c4..86edb5d8a 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java @@ -26,6 +26,7 @@ import java.util.Calendar; import java.util.Locale; + /** * Parameter represents a JDBC parameter value that is supplied with a prepared or callable statement or an updatable result set. Parameter is JDBC * type specific and is capable of representing any Java native type as well as a number of Java object types including binary and character streams. @@ -253,7 +254,7 @@ void setFromReturnStatus(int returnStatus, if (null == getterDTV) getterDTV = new DTV(); - getterDTV.setValue(null, JDBCType.INTEGER, new Integer(returnStatus), JavaType.INTEGER, null, null, null, con, getForceEncryption()); + getterDTV.setValue(null, JDBCType.INTEGER, returnStatus, JavaType.INTEGER, null, null, null, con, getForceEncryption()); } void setValue(JDBCType jdbcType, @@ -323,7 +324,7 @@ void setValue(JDBCType jdbcType, } if (JavaType.TVP == javaType) { - TVP tvpValue = null; + TVP tvpValue; if (null == value) { tvpValue = new TVP(tvpName); } @@ -331,16 +332,6 @@ else if (value instanceof SQLServerDataTable) { tvpValue = new TVP(tvpName, (SQLServerDataTable) value); } else if (value instanceof ResultSet) { - // if ResultSet and PreparedStatemet/CallableStatement are created from same connection object - // with property SelectMethod=cursor, TVP is not supported - if (con.getSelectMethod().equalsIgnoreCase("cursor") && (value instanceof SQLServerResultSet)) { - SQLServerStatement stmt = (SQLServerStatement) ((SQLServerResultSet) value).getStatement(); - - if (con.equals(stmt.connection)) { - throw new SQLServerException(SQLServerException.getErrString("R_invalidServerCursorForTVP"), null); - } - } - tvpValue = new TVP(tvpName, (ResultSet) value); } else if (value instanceof ISQLServerDataRecord) { @@ -384,7 +375,9 @@ else if (value instanceof ISQLServerDataRecord) { // If set to true, this connection property tells the driver to send textual parameters // to the server as Unicode rather than MBCS. This is accomplished here by re-tagging // the value with the appropriate corresponding Unicode type. - if (con.sendStringParametersAsUnicode() && (JavaType.STRING == javaType || JavaType.READER == javaType || JavaType.CLOB == javaType)) { + // JavaType.OBJECT == javaType when calling setNull() + if (con.sendStringParametersAsUnicode() + && (JavaType.STRING == javaType || JavaType.READER == javaType || JavaType.CLOB == javaType || JavaType.OBJECT == javaType)) { jdbcType = getSSPAUJDBCType(jdbcType); } @@ -406,7 +399,7 @@ boolean isNull() { } boolean isValueGotten() { - return (null != getterDTV) ? (true) : (false); + return null != getterDTV; } @@ -425,7 +418,7 @@ Object getValue(JDBCType jdbcType, int getInt(TDSReader tdsReader) throws SQLServerException { Integer value = (Integer) getValue(JDBCType.INTEGER, null, null, tdsReader); - return null != value ? value.intValue() : 0; + return null != value ? value : 0; } /** @@ -481,8 +474,13 @@ private void setTypeDefinition(DTV dtv) { * specific type info, otherwise generic type info can be used as before. */ param.typeDefinition = SSType.REAL.toString(); - break; } + else { + // use FLOAT if column is not encrypted + param.typeDefinition = SSType.FLOAT.toString(); + } + break; + case FLOAT: case DOUBLE: param.typeDefinition = SSType.FLOAT.toString(); @@ -499,8 +497,8 @@ private void setTypeDefinition(DTV dtv) { // - the specified input scale (if any) // - the registered output scale Integer inScale = dtv.getScale(); - if (null != inScale && scale < inScale.intValue()) - scale = inScale.intValue(); + if (null != inScale && scale < inScale) + scale = inScale; if (param.isOutput() && scale < param.getOutScale()) scale = param.getOutScale(); @@ -883,7 +881,10 @@ else if ((null != jdbcTypeSetByUser) && ((jdbcTypeSetByUser == JDBCType.NVARCHAR case GUID: param.typeDefinition = SSType.GUID.toString(); break; - + + case SQL_VARIANT: + param.typeDefinition = SSType.SQL_VARIANT.toString(); + break; default: assert false : "Unexpected JDBC type " + dtv.getJdbcType(); break; @@ -1138,6 +1139,17 @@ void execute(DTV dtv, setTypeDefinition(dtv); } + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.DTVExecuteOp#execute(com.microsoft.sqlserver.jdbc.DTV, microsoft.sql.SqlVariant) + */ + @Override + void execute(DTV dtv, + SqlVariant SqlVariantValue) throws SQLServerException { + setTypeDefinition(dtv); + } + } /** diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ParsedSQLMetadata.java b/src/main/java/com/microsoft/sqlserver/jdbc/ParsedSQLMetadata.java new file mode 100644 index 000000000..19c34ebec --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ParsedSQLMetadata.java @@ -0,0 +1,28 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ + +package com.microsoft.sqlserver.jdbc; + +/** + * Used for caching of meta data from parsed SQL text. + */ +final class ParsedSQLCacheItem { + /** The SQL text AFTER processing. */ + String processedSQL; + int parameterCount; + String procedureName; + boolean bReturnValueSyntax; + + ParsedSQLCacheItem(String processedSQL, int parameterCount, String procedureName, boolean bReturnValueSyntax) { + this.processedSQL = processedSQL; + this.parameterCount = parameterCount; + this.procedureName = procedureName; + this.bReturnValueSyntax = bReturnValueSyntax; + } +} + diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLCollation.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLCollation.java index 37bfd5ed3..808444319 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLCollation.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLCollation.java @@ -47,6 +47,24 @@ final class SQLCollation implements java.io.Serializable static final int tdsLength() { return 5; } // Length of collation in TDS (in bytes) + /** + * Returns the collation info + * + * @return + */ + int getCollationInfo() { + return this.info; + } + + /** + * return sort ID + * + * @return + */ + int getCollationSortID() { + return this.sortId; + } + /** * Reads TDS collation from TDS buffer into SQLCollation class. * @param tdsReader @@ -516,11 +534,11 @@ private Encoding encodingFromSortId() throws UnsupportedEncodingException { static { // Populate the windows locale and sort order indices - localeIndex = new HashMap(); + localeIndex = new HashMap<>(); for (WindowsLocale locale : EnumSet.allOf(WindowsLocale.class)) localeIndex.put(locale.langID, locale); - sortOrderIndex = new HashMap(); + sortOrderIndex = new HashMap<>(); for (SortOrder sortOrder : EnumSet.allOf(SortOrder.class)) sortOrderIndex.put(sortOrder.sortId, sortOrder); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java index fa32cc006..2aa091ff0 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java @@ -10,7 +10,7 @@ final class SQLJdbcVersion { static final int major = 6; - static final int minor = 1; - static final int patch = 0; + static final int minor = 3; + static final int patch = 4; static final int build = 0; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java index 5a8d91870..adc22e85e 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java @@ -29,7 +29,7 @@ static SqlFedAuthToken getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, return fedAuthToken; } catch (MalformedURLException | InterruptedException e) { - throw new SQLServerException(e.getMessage(), null); + throw new SQLServerException(e.getMessage(), e); } catch (ExecutionException e) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_ADALExecution")); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAeadAes256CbcHmac256Factory.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAeadAes256CbcHmac256Factory.java index 7ee7ab9f0..8f71ec6b6 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAeadAes256CbcHmac256Factory.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAeadAes256CbcHmac256Factory.java @@ -21,7 +21,7 @@ class SQLServerAeadAes256CbcHmac256Factory extends SQLServerEncryptionAlgorithmFactory { // In future we can have more private byte algorithmVersion = 0x1; - private ConcurrentHashMap encryptionAlgorithms = new ConcurrentHashMap(); + private ConcurrentHashMap encryptionAlgorithms = new ConcurrentHashMap<>(); @Override SQLServerEncryptionAlgorithm create(SQLServerSymmetricKey columnEncryptionKey, @@ -36,9 +36,8 @@ SQLServerEncryptionAlgorithm create(SQLServerSymmetricKey columnEncryptionKey, throw new SQLServerException(this, form.format(msgArgs), null, 0, false); } - String factoryKey = ""; - StringBuffer factoryKeyBuilder = new StringBuffer(); + StringBuilder factoryKeyBuilder = new StringBuilder(); factoryKeyBuilder.append(DatatypeConverter.printBase64Binary(new String(columnEncryptionKey.getRootKey(), UTF_8).getBytes())); factoryKeyBuilder.append(":"); @@ -46,7 +45,7 @@ SQLServerEncryptionAlgorithm create(SQLServerSymmetricKey columnEncryptionKey, factoryKeyBuilder.append(":"); factoryKeyBuilder.append(algorithmVersion); - factoryKey = factoryKeyBuilder.toString(); + String factoryKey = factoryKeyBuilder.toString(); SQLServerAeadAes256CbcHmac256Algorithm aesAlgorithm; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBlob.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBlob.java index 9b4bcd882..47927d6a6 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBlob.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBlob.java @@ -40,13 +40,13 @@ public final class SQLServerBlob implements java.sql.Blob, java.io.Serializable // Initial size of the array is based on an assumption that a Blob object is // typically used either for input or output, and then only once. The array size // grows automatically if multiple streams are used. - ArrayList activeStreams = new ArrayList(1); + ArrayList activeStreams = new ArrayList<>(1); static private final Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.internals.SQLServerBlob"); - static private final AtomicInteger baseID = new AtomicInteger(0); // Unique id generator for each instance (used for logging). + static private final AtomicInteger baseID = new AtomicInteger(0); // Unique id generator for each instance (used for logging). final private String traceID; - + final public String toString() { return traceID; } @@ -63,6 +63,7 @@ private static int nextInstanceID() { * the database connection this blob is implemented on * @param data * the BLOB's data + * @deprecated Use {@link SQLServerConnection#createBlob()} instead. */ @Deprecated public SQLServerBlob(SQLServerConnection connection, @@ -94,7 +95,7 @@ public SQLServerBlob(SQLServerConnection connection, SQLServerBlob(BaseInputStream stream) throws SQLServerException { traceID = " SQLServerBlob:" + nextInstanceID(); - value = stream.getBytes(); + activeStreams.add(stream); if (logger.isLoggable(Level.FINE)) logger.fine(toString() + " created by (null connection)"); } @@ -106,8 +107,6 @@ public SQLServerBlob(SQLServerConnection connection, * multiple times, the subsequent calls to free are treated as a no-op. */ public void free() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - if (!isClosed) { // Close active streams, ignoring any errors, since nothing can be done with them after that point anyway. if (null != activeStreams) { @@ -142,13 +141,26 @@ private void checkClosed() throws SQLServerException { public InputStream getBinaryStream() throws SQLException { checkClosed(); - return getBinaryStreamInternal(0, value.length); + if (null == value && !activeStreams.isEmpty()) { + InputStream stream = (InputStream) activeStreams.get(0); + try { + stream.reset(); + } + catch (IOException e) { + throw new SQLServerException(e.getMessage(), null, 0, e); + } + return (InputStream) activeStreams.get(0); + } + else { + if (value == null) { + throw new SQLServerException("Unexpected Error: blob value is null while all streams are closed.", null); + } + return getBinaryStreamInternal(0, value.length); + } } public InputStream getBinaryStream(long pos, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - // Not implemented - partial materialization throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); } @@ -182,15 +194,16 @@ public byte[] getBytes(long pos, int length) throws SQLException { checkClosed(); + getBytesFromStream(); if (pos < 1) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPositionIndex")); - Object[] msgArgs = {new Long(pos)}; + Object[] msgArgs = {pos}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } if (length < 0) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLength")); - Object[] msgArgs = {new Integer(length)}; + Object[] msgArgs = {length}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } @@ -219,9 +232,26 @@ public byte[] getBytes(long pos, */ public long length() throws SQLException { checkClosed(); - + getBytesFromStream(); return value.length; } + + /** + * Converts stream to byte[] + * @throws SQLServerException + */ + private void getBytesFromStream() throws SQLServerException { + if (null == value) { + BaseInputStream stream = (BaseInputStream) activeStreams.get(0); + try { + stream.reset(); + } + catch (IOException e) { + throw new SQLServerException(e.getMessage(), null, 0, e); + } + value = stream.getBytes(); + } + } /** * Retrieves the byte position in the BLOB value designated by this Blob object at which pattern begins. The search begins at position start. @@ -237,10 +267,11 @@ public long length() throws SQLException { public long position(Blob pattern, long start) throws SQLException { checkClosed(); - + + getBytesFromStream(); if (start < 1) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPositionIndex")); - Object[] msgArgs = {new Long(start)}; + Object[] msgArgs = {start}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } @@ -265,10 +296,10 @@ public long position(Blob pattern, public long position(byte[] bPattern, long start) throws SQLException { checkClosed(); - + getBytesFromStream(); if (start < 1) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPositionIndex")); - Object[] msgArgs = {new Long(start)}; + Object[] msgArgs = {start}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } @@ -290,8 +321,9 @@ public long position(byte[] bPattern, } } - if (match) - return pos + 1; + if (match) { + return pos + 1L; + } } return -1; @@ -309,10 +341,11 @@ public long position(byte[] bPattern, */ public void truncate(long len) throws SQLException { checkClosed(); - + getBytesFromStream(); + if (len < 0) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLength")); - Object[] msgArgs = {new Long(len)}; + Object[] msgArgs = {len}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } @@ -357,7 +390,8 @@ public java.io.OutputStream setBinaryStream(long pos) throws SQLException { public int setBytes(long pos, byte[] bytes) throws SQLException { checkClosed(); - + + getBytesFromStream(); if (null == bytes) SQLServerException.makeFromDriverError(con, null, SQLServerException.getErrString("R_cantSetNull"), null, true); @@ -389,6 +423,7 @@ public int setBytes(long pos, int offset, int len) throws SQLException { checkClosed(); + getBytesFromStream(); if (null == bytes) SQLServerException.makeFromDriverError(con, null, SQLServerException.getErrString("R_cantSetNull"), null, true); @@ -396,14 +431,14 @@ public int setBytes(long pos, // Offset must be within incoming bytes boundary. if (offset < 0 || offset > bytes.length) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidOffset")); - Object[] msgArgs = {new Integer(offset)}; + Object[] msgArgs = {offset}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } // len must be within incoming bytes boundary. if (len < 0 || len > bytes.length - offset) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLength")); - Object[] msgArgs = {new Integer(len)}; + Object[] msgArgs = {len}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } @@ -412,7 +447,7 @@ public int setBytes(long pos, // past the end of data to request "append" mode. if (pos <= 0 || pos > value.length + 1) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPositionIndex")); - Object[] msgArgs = {new Long(pos)}; + Object[] msgArgs = {pos}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java index 56190a1d7..4ff063dab 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java @@ -11,10 +11,12 @@ import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.math.RoundingMode; +import java.sql.Types; import java.text.DecimalFormat; import java.text.MessageFormat; import java.time.OffsetDateTime; @@ -152,12 +154,69 @@ else if (null == delimiter) { } catch (UnsupportedEncodingException unsupportedEncoding) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unsupportedEncoding")); - throw new SQLServerException(form.format(new Object[] {encoding}), null, 0, null); + throw new SQLServerException(form.format(new Object[] {encoding}), null, 0, unsupportedEncoding); } catch (Exception e) { throw new SQLServerException(null, e.getMessage(), null, 0, false); } - columnMetadata = new HashMap(); + columnMetadata = new HashMap<>(); + + loggerExternal.exiting(loggerClassName, "SQLServerBulkCSVFileRecord"); + } + + /** + * Creates a simple reader to parse data from a delimited file with the given encoding. + * + * @param fileToParse + * InputStream to parse data from + * @param encoding + * Charset encoding to use for reading the file, or NULL for the default encoding. + * @param delimiter + * Delimiter to used to separate each column + * @param firstLineIsColumnNames + * True if the first line of the file should be parsed as column names; false otherwise + * @throws SQLServerException + * If the arguments are invalid, there are any errors in reading the file, or the file is empty + */ + public SQLServerBulkCSVFileRecord(InputStream fileToParse, + String encoding, + String delimiter, + boolean firstLineIsColumnNames) throws SQLServerException { + loggerExternal.entering(loggerClassName, "SQLServerBulkCSVFileRecord", + new Object[] {fileToParse, encoding, delimiter, firstLineIsColumnNames}); + + if (null == fileToParse) { + throwInvalidArgument("fileToParse"); + } + else if (null == delimiter) { + throwInvalidArgument("delimiter"); + } + + this.delimiter = delimiter; + try { + if (null == encoding || 0 == encoding.length()) { + sr = new InputStreamReader(fileToParse); + } + else { + sr = new InputStreamReader(fileToParse, encoding); + } + fileReader = new BufferedReader(sr); + + if (firstLineIsColumnNames) { + currentLine = fileReader.readLine(); + if (null != currentLine) { + columnNames = currentLine.split(delimiter, -1); + } + } + } + catch (UnsupportedEncodingException unsupportedEncoding) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unsupportedEncoding")); + throw new SQLServerException(form.format(new Object[] {encoding}), null, 0, unsupportedEncoding); + } + catch (Exception e) { + throw new SQLServerException(null, e.getMessage(), null, 0, false); + } + columnMetadata = new HashMap<>(); loggerExternal.exiting(loggerClassName, "SQLServerBulkCSVFileRecord"); } @@ -467,9 +526,7 @@ public Object[] getRowData() throws SQLServerException { // Cannot go directly from String[] to Object[] and expect it to act as an array. Object[] dataRow = new Object[data.length]; - Iterator> it = columnMetadata.entrySet().iterator(); - while (it.hasNext()) { - Entry pair = it.next(); + for (Entry pair : columnMetadata.entrySet()) { ColumnMetadata cm = pair.getValue(); // Reading a column not available in csv @@ -482,7 +539,7 @@ public Object[] getRowData() throws SQLServerException { // Source header has more columns than current line read if (columnNames != null && (columnNames.length > data.length)) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_BulkCSVDataSchemaMismatch")); + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_CSVDataSchemaMismatch")); Object[] msgArgs = {}; throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); } @@ -498,7 +555,7 @@ public Object[] getRowData() throws SQLServerException { * Both BCP and BULK INSERT considers double quotes as part of the data and throws error if any data (say "10") is to be * inserted into an numeric column. Our implementation does the same. */ - case java.sql.Types.INTEGER: { + case Types.INTEGER: { // Formatter to remove the decimal part as SQL Server floors the decimal in integer types DecimalFormat decimalFormatter = new DecimalFormat("#"); String formatedfInput = decimalFormatter.format(Double.parseDouble(data[pair.getKey() - 1])); @@ -506,8 +563,8 @@ public Object[] getRowData() throws SQLServerException { break; } - case java.sql.Types.TINYINT: - case java.sql.Types.SMALLINT: { + case Types.TINYINT: + case Types.SMALLINT: { // Formatter to remove the decimal part as SQL Server floors the decimal in integer types DecimalFormat decimalFormatter = new DecimalFormat("#"); String formatedfInput = decimalFormatter.format(Double.parseDouble(data[pair.getKey() - 1])); @@ -515,52 +572,50 @@ public Object[] getRowData() throws SQLServerException { break; } - case java.sql.Types.BIGINT: { + case Types.BIGINT: { BigDecimal bd = new BigDecimal(data[pair.getKey() - 1].trim()); try { dataRow[pair.getKey() - 1] = bd.setScale(0, BigDecimal.ROUND_DOWN).longValueExact(); - } - catch (ArithmeticException ex) { + } catch (ArithmeticException ex) { String value = "'" + data[pair.getKey() - 1] + "'"; MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorConvertingValue")); - throw new SQLServerException(form.format(new Object[] {value, JDBCType.of(cm.columnType)}), null, 0, null); + throw new SQLServerException(form.format(new Object[]{value, JDBCType.of(cm.columnType)}), null, 0, ex); } break; } - case java.sql.Types.DECIMAL: - case java.sql.Types.NUMERIC: { + case Types.DECIMAL: + case Types.NUMERIC: { BigDecimal bd = new BigDecimal(data[pair.getKey() - 1].trim()); dataRow[pair.getKey() - 1] = bd.setScale(cm.scale, RoundingMode.HALF_UP); break; } - case java.sql.Types.BIT: { + case Types.BIT: { // "true" => 1, "false" => 0 // Any non-zero value (integer/double) => 1, 0/0.0 => 0 try { dataRow[pair.getKey() - 1] = (0 == Double.parseDouble(data[pair.getKey() - 1])) ? Boolean.FALSE : Boolean.TRUE; - } - catch (NumberFormatException e) { + } catch (NumberFormatException e) { dataRow[pair.getKey() - 1] = Boolean.parseBoolean(data[pair.getKey() - 1]); } break; } - case java.sql.Types.REAL: { + case Types.REAL: { dataRow[pair.getKey() - 1] = Float.parseFloat(data[pair.getKey() - 1]); break; } - case java.sql.Types.DOUBLE: { + case Types.DOUBLE: { dataRow[pair.getKey() - 1] = Double.parseDouble(data[pair.getKey() - 1]); break; } - case java.sql.Types.BINARY: - case java.sql.Types.VARBINARY: - case java.sql.Types.LONGVARBINARY: - case java.sql.Types.BLOB: { + case Types.BINARY: + case Types.VARBINARY: + case Types.LONGVARBINARY: + case Types.BLOB: { /* * For binary data, the value in file may or may not have the '0x' prefix. We will try to match our implementation with * 'BULK INSERT' except that we will allow 0x prefix whereas 'BULK INSERT' command does not allow 0x prefix. A BULK INSERT @@ -572,17 +627,16 @@ public Object[] getRowData() throws SQLServerException { String binData = data[pair.getKey() - 1].trim(); if (binData.startsWith("0x") || binData.startsWith("0X")) { dataRow[pair.getKey() - 1] = binData.substring(2); - } - else { + } else { dataRow[pair.getKey() - 1] = binData; } break; } - case 2013: // java.sql.Types.TIME_WITH_TIMEZONE + case 2013: // java.sql.Types.TIME_WITH_TIMEZONE { DriverJDBCVersion.checkSupportsJDBC42(); - OffsetTime offsetTimeValue = null; + OffsetTime offsetTimeValue; // The per-column DateTimeFormatter gets priority. if (null != cm.dateTimeFormatter) @@ -599,7 +653,7 @@ else if (timeFormatter != null) case 2014: // java.sql.Types.TIMESTAMP_WITH_TIMEZONE { DriverJDBCVersion.checkSupportsJDBC42(); - OffsetDateTime offsetDateTimeValue = null; + OffsetDateTime offsetDateTimeValue; // The per-column DateTimeFormatter gets priority. if (null != cm.dateTimeFormatter) @@ -613,19 +667,19 @@ else if (dateTimeFormatter != null) break; } - case java.sql.Types.NULL: { + case Types.NULL: { dataRow[pair.getKey() - 1] = null; break; } - case java.sql.Types.DATE: - case java.sql.Types.CHAR: - case java.sql.Types.NCHAR: - case java.sql.Types.VARCHAR: - case java.sql.Types.NVARCHAR: - case java.sql.Types.LONGVARCHAR: - case java.sql.Types.LONGNVARCHAR: - case java.sql.Types.CLOB: + case Types.DATE: + case Types.CHAR: + case Types.NCHAR: + case Types.VARCHAR: + case Types.NVARCHAR: + case Types.LONGVARCHAR: + case Types.LONGNVARCHAR: + case Types.CLOB: default: { // The string is copied as is. /* @@ -644,14 +698,12 @@ else if (dateTimeFormatter != null) break; } } - } - catch (IllegalArgumentException e) { + } catch (IllegalArgumentException e) { String value = "'" + data[pair.getKey() - 1] + "'"; MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorConvertingValue")); - throw new SQLServerException(form.format(new Object[] {value, JDBCType.of(cm.columnType)}), null, 0, null); - } - catch (ArrayIndexOutOfBoundsException e) { - throw new SQLServerException(SQLServerException.getErrString("R_BulkCSVDataSchemaMismatch"), null); + throw new SQLServerException(form.format(new Object[]{value, JDBCType.of(cm.columnType)}), null, 0, e); + } catch (ArrayIndexOutOfBoundsException e) { + throw new SQLServerException(SQLServerException.getErrString("R_CSVDataSchemaMismatch"), e); } } @@ -665,7 +717,7 @@ public boolean next() throws SQLServerException { currentLine = fileReader.readLine(); } catch (IOException e) { - throw new SQLServerException(null, e.getMessage(), null, 0, false); + throw new SQLServerException(e.getMessage(), null, 0, e); } return (null != currentLine); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java index 2be5d0a37..a383e1946 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java @@ -29,6 +29,7 @@ import java.time.OffsetDateTime; import java.time.OffsetTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.HashMap; @@ -41,7 +42,6 @@ import java.util.SimpleTimeZone; import java.util.TimeZone; import java.util.UUID; -import java.util.Vector; import java.util.logging.Level; import javax.sql.RowSet; @@ -289,6 +289,8 @@ public void run() { while (--secondsRemaining > 0); } catch (InterruptedException e) { + // re-interrupt the current thread, in order to restore the thread's interrupt status. + Thread.currentThread().interrupt(); return; } @@ -307,6 +309,12 @@ public void run() { } private BulkTimeoutTimer timeoutTimer = null; + + /** + * The maximum temporal precision we can send when using varchar(precision) in bulkcommand, to send a smalldatetime/datetime + * value. + */ + private static final int sourceBulkRecordTemporalMaxPrecision = 50; /** * Initializes a new instance of the SQLServerBulkCopy class using the specified open instance of SQLServerConnection. @@ -350,7 +358,7 @@ public SQLServerBulkCopy(Connection connection) throws SQLServerException { */ public SQLServerBulkCopy(String connectionUrl) throws SQLServerException { loggerExternal.entering(loggerClassName, "SQLServerBulkCopy", "connectionUrl not traced."); - if ((connectionUrl == null) || connectionUrl.trim().equals("")) { + if ((connectionUrl == null) || "".equals(connectionUrl.trim())) { throw new SQLServerException(null, SQLServerException.getErrString("R_nullConnection"), null, 0, false); } @@ -667,7 +675,7 @@ public void writeToServer(ISQLServerBulkRecord sourceData) throws SQLServerExcep * Initializes the defaults for member variables that require it. */ private void initializeDefaults() { - columnMappings = new LinkedList(); + columnMappings = new LinkedList<>(); destinationTableName = null; sourceBulkRecord = null; sourceResultSet = null; @@ -736,10 +744,10 @@ final boolean doExecute() throws SQLServerException { */ private void writeColumnMetaDataColumnData(TDSWriter tdsWriter, int idx) throws SQLServerException { - int srcColumnIndex = 0, destPrecision = 0; - int bulkJdbcType = 0, bulkPrecision = 0, bulkScale = 0; - SQLCollation collation = null; - SSType destSSType = null; + int srcColumnIndex, destPrecision; + int bulkJdbcType, bulkPrecision, bulkScale; + SQLCollation collation; + SSType destSSType; boolean isStreaming, srcNullable; // For varchar, precision is the size of the varchar type. /* @@ -1128,7 +1136,10 @@ private void writeTypeInfo(TDSWriter tdsWriter, tdsWriter.writeByte((byte) srcScale); } break; - + case microsoft.sql.Types.SQL_VARIANT: //0x62 + tdsWriter.writeByte(TDSType.SQL_VARIANT.byteValue()); + tdsWriter.writeInt(TDS.SQL_VARIANT_LENGTH); + break; default: MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_BulkTypeNotSupported")); String unsupportedDataType = JDBCType.of(srcJdbcType).toString().toLowerCase(Locale.ENGLISH); @@ -1207,7 +1218,7 @@ private void checkForTimeoutException(SQLException e, connection.rollback(); } - throw new SQLServerException(SQLServerException.getErrString("R_queryTimedOut"), SQLState.STATEMENT_CANCELED, DriverError.NOT_SET, null); + throw new SQLServerException(SQLServerException.getErrString("R_queryTimedOut"), SQLState.STATEMENT_CANCELED, DriverError.NOT_SET, e); } } @@ -1238,11 +1249,12 @@ private String getDestTypeFromSrcType(int srcColIndx, int destColIndx, TDSWriter tdsWriter) throws SQLServerException { boolean isStreaming; + SSType destSSType = (null != destColumnMetadata.get(destColIndx).cryptoMeta) ? destColumnMetadata.get(destColIndx).cryptoMeta.baseTypeInfo.getSSType() : destColumnMetadata.get(destColIndx).ssType; - int bulkJdbcType = 0, bulkPrecision = 0, bulkScale = 0; - int srcPrecision = 0; + int bulkJdbcType, bulkPrecision, bulkScale; + int srcPrecision; bulkJdbcType = srcColumnMetadata.get(srcColIndx).jdbcType; // For char/varchar precision is the size. @@ -1376,14 +1388,14 @@ private String getDestTypeFromSrcType(int srcColIndx, switch (destSSType) { case SMALLDATETIME: if (null != sourceBulkRecord) { - return "varchar(" + ((0 == bulkPrecision) ? destPrecision : bulkPrecision) + ")"; + return "varchar(" + ((0 == bulkPrecision) ? sourceBulkRecordTemporalMaxPrecision : bulkPrecision) + ")"; } else { return "smalldatetime"; } case DATETIME: if (null != sourceBulkRecord) { - return "varchar(" + ((0 == bulkPrecision) ? destPrecision : bulkPrecision) + ")"; + return "varchar(" + ((0 == bulkPrecision) ? sourceBulkRecordTemporalMaxPrecision : bulkPrecision) + ")"; } else { return "datetime"; @@ -1446,7 +1458,8 @@ private String getDestTypeFromSrcType(int srcColIndx, else { return "datetimeoffset(" + bulkScale + ")"; } - + case microsoft.sql.Types.SQL_VARIANT: + return "sql_variant"; default: { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_BulkTypeNotSupported")); Object[] msgArgs = {JDBCType.of(bulkJdbcType).toString().toLowerCase(Locale.ENGLISH)}; @@ -1458,7 +1471,7 @@ private String getDestTypeFromSrcType(int srcColIndx, private String createInsertBulkCommand(TDSWriter tdsWriter) throws SQLServerException { StringBuilder bulkCmd = new StringBuilder(); - List bulkOptions = new Vector(); + List bulkOptions = new ArrayList<>(); String endColumn = " , "; bulkCmd.append("INSERT BULK " + destinationTableName + " ("); @@ -1474,7 +1487,7 @@ private String createInsertBulkCommand(TDSWriter tdsWriter) throws SQLServerExce .toUpperCase(Locale.ENGLISH); if (null != columnCollation && columnCollation.trim().length() > 0) { // we are adding collate in command only for char and varchar - if (null != destType && (destType.toLowerCase().trim().startsWith("char") || destType.toLowerCase().trim().startsWith("varchar"))) + if (null != destType && (destType.toLowerCase(Locale.ENGLISH).trim().startsWith("char") || destType.toLowerCase(Locale.ENGLISH).trim().startsWith("varchar"))) addCollate = " COLLATE " + columnCollation; } bulkCmd.append("[" + colMapping.destinationColumnName + "] " + destType + addCollate + endColumn); @@ -1508,7 +1521,7 @@ private String createInsertBulkCommand(TDSWriter tdsWriter) throws SQLServerExce if (it.hasNext()) { bulkCmd.append(" with ("); while (it.hasNext()) { - bulkCmd.append(it.next().toString()); + bulkCmd.append(it.next()); if (it.hasNext()) { bulkCmd.append(", "); } @@ -1527,25 +1540,48 @@ private boolean doInsertBulk(TDSCommand command) throws SQLServerException { // Begin a manual transaction for this batch. connection.setAutoCommit(false); } + + boolean insertRowByRow = false; - // Create and send the initial command for bulk copy ("INSERT BULK ..."). - TDSWriter tdsWriter = command.startRequest(TDS.PKT_QUERY); - String bulkCmd = createInsertBulkCommand(tdsWriter); - tdsWriter.writeString(bulkCmd); - TDSParser.parse(command.startResponse(), command.getLogContext()); + if (null != sourceResultSet && sourceResultSet instanceof SQLServerResultSet) { + SQLServerStatement src_stmt = (SQLServerStatement) ((SQLServerResultSet) sourceResultSet).getStatement(); + int resultSetServerCursorId = ((SQLServerResultSet) sourceResultSet).getServerCursorId(); - // Send the bulk data. This is the BulkLoadBCP TDS stream. - tdsWriter = command.startRequest(TDS.PKT_BULK); + if (connection.equals(src_stmt.getConnection()) && 0 != resultSetServerCursorId) { + insertRowByRow = true; + } + + if (((SQLServerResultSet) sourceResultSet).isForwardOnly()) { + try { + sourceResultSet.setFetchSize(1); + } + catch (SQLException e) { + SQLServerException.makeFromDriverError(connection, sourceResultSet, e.getMessage(), e.getSQLState(), true); + } + } + } + TDSWriter tdsWriter = null; boolean moreDataAvailable = false; + try { - // Write the COLUMNMETADATA token in the stream. - writeColumnMetaData(tdsWriter); + if (!insertRowByRow) { + tdsWriter = sendBulkCopyCommand(command); + } - // Write all ROW tokens in the stream. - moreDataAvailable = writeBatchData(tdsWriter); + try { + // Write all ROW tokens in the stream. + moreDataAvailable = writeBatchData(tdsWriter, command, insertRowByRow); + } + finally { + tdsWriter = command.getTDSWriter(); + } } catch (SQLServerException ex) { + if (null == tdsWriter) { + tdsWriter = command.getTDSWriter(); + } + // Close the TDS packet before handling the exception writePacketDataDone(tdsWriter); @@ -1559,18 +1595,25 @@ private boolean doInsertBulk(TDSCommand command) throws SQLServerException { throw ex; } finally { + if (null == tdsWriter) { + tdsWriter = command.getTDSWriter(); + } + // reset the cryptoMeta in IOBuffer tdsWriter.setCryptoMetaData(null); } - // Write the DONE token in the stream. We may have to append the DONE token with every packet that is sent. - // For the current packets the driver does not generate a DONE token, but the BulkLoadBCP stream needs a DONE token - // after every packet. For now add it manually here for one packet. - // Note: This may break if more than one packet is sent. - // This is an example from https://msdn.microsoft.com/en-us/library/dd340549.aspx - writePacketDataDone(tdsWriter); + + if (!insertRowByRow) { + // Write the DONE token in the stream. We may have to append the DONE token with every packet that is sent. + // For the current packets the driver does not generate a DONE token, but the BulkLoadBCP stream needs a DONE token + // after every packet. For now add it manually here for one packet. + // Note: This may break if more than one packet is sent. + // This is an example from https://msdn.microsoft.com/en-us/library/dd340549.aspx + writePacketDataDone(tdsWriter); - // Send to the server and read response. - TDSParser.parse(command.startResponse(), command.getLogContext()); + // Send to the server and read response. + TDSParser.parse(command.startResponse(), command.getLogContext()); + } if (copyOptions.isUseInternalTransaction()) { // Commit the transaction for this batch. @@ -1580,6 +1623,22 @@ private boolean doInsertBulk(TDSCommand command) throws SQLServerException { return moreDataAvailable; } + private TDSWriter sendBulkCopyCommand(TDSCommand command) throws SQLServerException { + // Create and send the initial command for bulk copy ("INSERT BULK ..."). + TDSWriter tdsWriter = command.startRequest(TDS.PKT_QUERY); + String bulkCmd = createInsertBulkCommand(tdsWriter); + tdsWriter.writeString(bulkCmd); + TDSParser.parse(command.startResponse(), command.getLogContext()); + + // Send the bulk data. This is the BulkLoadBCP TDS stream. + tdsWriter = command.startRequest(TDS.PKT_BULK); + + // Write the COLUMNMETADATA token in the stream. + writeColumnMetaData(tdsWriter); + + return tdsWriter; + } + private void writePacketDataDone(TDSWriter tdsWriter) throws SQLServerException { // This is an example from https://msdn.microsoft.com/en-us/library/dd340549.aspx tdsWriter.writeByte((byte) 0xFD); @@ -1639,14 +1698,20 @@ private void writeToServer() throws SQLServerException { private void validateStringBinaryLengths(Object colValue, int srcCol, int destCol) throws SQLServerException { - int sourcePrecision = 0; + int sourcePrecision; int destPrecision = destColumnMetadata.get(destCol).precision; int srcJdbcType = srcColumnMetadata.get(srcCol).jdbcType; SSType destSSType = destColumnMetadata.get(destCol).ssType; if ((Util.isCharType(srcJdbcType) && Util.isCharType(destSSType)) || (Util.isBinaryType(srcJdbcType) && Util.isBinaryType(destSSType))) { if (colValue instanceof String) { - sourcePrecision = ((String) colValue).length(); + if (Util.isBinaryType(destSSType)) { + // if the dest value is binary and the value is of type string. + //Repro in test case: ImpISQLServerBulkRecord_IssuesTest#testSendValidValueforBinaryColumnAsString + sourcePrecision = (((String) colValue).getBytes().length) / 2; + } + else + sourcePrecision = ((String) colValue).length(); } else if (colValue instanceof byte[]) { sourcePrecision = ((byte[]) colValue).length; @@ -1682,7 +1747,7 @@ private void getDestinationMetadata() throws SQLServerException { .executeQueryInternal("SET FMTONLY ON SELECT * FROM " + destinationTableName + " SET FMTONLY OFF "); destColumnCount = rs.getMetaData().getColumnCount(); - destColumnMetadata = new HashMap(); + destColumnMetadata = new HashMap<>(); destCekTable = rs.getCekTable(); if (!connection.getServerSupportsColumnEncryption()) { @@ -1728,7 +1793,7 @@ private void getDestinationMetadata() throws SQLServerException { * source metadata from the same place for both ResultSet and File. */ private void getSourceMetadata() throws SQLServerException { - srcColumnMetadata = new HashMap(); + srcColumnMetadata = new HashMap<>(); int currentColumn; if (null != sourceResultSet) { try { @@ -1736,7 +1801,7 @@ private void getSourceMetadata() throws SQLServerException { for (int i = 1; i <= srcColumnCount; ++i) { srcColumnMetadata.put(i, new BulkColumnMetaData(sourceResultSetMetaData.getColumnName(i), - ((ResultSetMetaData.columnNoNulls == sourceResultSetMetaData.isNullable(i)) ? false : true), + (ResultSetMetaData.columnNoNulls != sourceResultSetMetaData.isNullable(i)), sourceResultSetMetaData.getPrecision(i), sourceResultSetMetaData.getScale(i), sourceResultSetMetaData.getColumnType(i), null)); } @@ -1748,14 +1813,13 @@ private void getSourceMetadata() throws SQLServerException { } else if (null != sourceBulkRecord) { Set columnOrdinals = sourceBulkRecord.getColumnOrdinals(); - srcColumnCount = columnOrdinals.size(); - if (0 == srcColumnCount) { + if (null == columnOrdinals || 0 == columnOrdinals.size()) { throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), null); } else { - Iterator columnsIterator = columnOrdinals.iterator(); - while (columnsIterator.hasNext()) { - currentColumn = columnsIterator.next(); + srcColumnCount = columnOrdinals.size(); + for (Integer columnOrdinal : columnOrdinals) { + currentColumn = columnOrdinal; srcColumnMetadata.put(currentColumn, new BulkColumnMetaData(sourceBulkRecord.getColumnName(currentColumn), true, sourceBulkRecord.getPrecision(currentColumn), sourceBulkRecord.getScale(currentColumn), sourceBulkRecord.getColumnType(currentColumn), @@ -1879,9 +1943,7 @@ else if (0 > cm.destinationColumnOrdinal || destColumnCount < cm.destinationColu } else { Set columnOrdinals = sourceBulkRecord.getColumnOrdinals(); - Iterator columnsIterator = columnOrdinals.iterator(); - while (columnsIterator.hasNext()) { - int currentColumn = columnsIterator.next(); + for (Integer currentColumn : columnOrdinals) { if (sourceBulkRecord.getColumnName(currentColumn).equals(cm.sourceColumnName)) { foundColumn = true; cm.sourceColumnOrdinal = currentColumn; @@ -1984,6 +2046,9 @@ private void writeNullToTdsWriter(TDSWriter tdsWriter, case microsoft.sql.Types.DATETIMEOFFSET: tdsWriter.writeByte((byte) 0x00); return; + case microsoft.sql.Types.SQL_VARIANT: + tdsWriter.writeInt((byte) 0x00); + return; default: MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_BulkTypeNotSupported")); Object[] msgArgs = {JDBCType.of(srcJdbcType).toString().toLowerCase(Locale.ENGLISH)}; @@ -2033,417 +2098,720 @@ else if (null != sourceBulkRecord) { } } - // We are sending the data using JDBCType and not using SSType as SQL Server will automatically do the conversion. - switch (bulkJdbcType) { - case java.sql.Types.INTEGER: - if (null == colValue) { - writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); - } - else { - if (bulkNullable) { - tdsWriter.writeByte((byte) 0x04); + try { + // We are sending the data using JDBCType and not using SSType as SQL Server will automatically do the conversion. + switch (bulkJdbcType) { + case java.sql.Types.INTEGER: + if (null == colValue) { + writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); } - tdsWriter.writeInt((int) colValue); - } - break; - - case java.sql.Types.SMALLINT: - if (null == colValue) { - writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); - } - else { - if (bulkNullable) { - tdsWriter.writeByte((byte) 0x02); + else { + if (bulkNullable) { + tdsWriter.writeByte((byte) 0x04); + } + tdsWriter.writeInt((int) colValue); } - tdsWriter.writeShort(((Number) colValue).shortValue()); - } - break; + break; - case java.sql.Types.BIGINT: - if (null == colValue) { - writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); - } - else { - if (bulkNullable) { - tdsWriter.writeByte((byte) 0x08); + case java.sql.Types.SMALLINT: + if (null == colValue) { + writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); } - tdsWriter.writeLong((long) colValue); - } - break; + else { + if (bulkNullable) { + tdsWriter.writeByte((byte) 0x02); + } + tdsWriter.writeShort(((Number) colValue).shortValue()); + } + break; - case java.sql.Types.BIT: - if (null == colValue) { - writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); - } - else { - if (bulkNullable) { - tdsWriter.writeByte((byte) 0x01); + case java.sql.Types.BIGINT: + if (null == colValue) { + writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); } - tdsWriter.writeByte((byte) (((Boolean) colValue).booleanValue() ? 1 : 0)); - } - break; + else { + if (bulkNullable) { + tdsWriter.writeByte((byte) 0x08); + } + tdsWriter.writeLong((long) colValue); + } + break; - case java.sql.Types.TINYINT: - if (null == colValue) { - writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); - } - else { - if (bulkNullable) { - tdsWriter.writeByte((byte) 0x01); + case java.sql.Types.BIT: + if (null == colValue) { + writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); } - // TINYINT JDBC type is returned as a short in getObject. - // MYSQL returns TINYINT as an Integer. Convert it to a Number to get the short value. - tdsWriter.writeByte((byte) ((((Number) colValue).shortValue()) & 0xFF)); + else { + if (bulkNullable) { + tdsWriter.writeByte((byte) 0x01); + } + tdsWriter.writeByte((byte) ((Boolean) colValue ? 1 : 0)); + } + break; - } - break; + case java.sql.Types.TINYINT: + if (null == colValue) { + writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); + } + else { + if (bulkNullable) { + tdsWriter.writeByte((byte) 0x01); + } + // TINYINT JDBC type is returned as a short in getObject. + // MYSQL returns TINYINT as an Integer. Convert it to a Number to get the short value. + tdsWriter.writeByte((byte) ((((Number) colValue).shortValue()) & 0xFF)); - case java.sql.Types.DOUBLE: - if (null == colValue) { - writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); - } - else { - if (bulkNullable) { - tdsWriter.writeByte((byte) 0x08); } - tdsWriter.writeDouble((double) colValue); - } - break; + break; - case java.sql.Types.REAL: - if (null == colValue) { - writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); - } - else { - if (bulkNullable) { - tdsWriter.writeByte((byte) 0x04); + case java.sql.Types.DOUBLE: + if (null == colValue) { + writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); } - tdsWriter.writeReal((float) colValue); - } - break; + else { + if (bulkNullable) { + tdsWriter.writeByte((byte) 0x08); + } + tdsWriter.writeDouble((double) colValue); + } + break; - case microsoft.sql.Types.MONEY: - case microsoft.sql.Types.SMALLMONEY: - case java.sql.Types.DECIMAL: - case java.sql.Types.NUMERIC: - if (null == colValue) { - writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); - } - else { - tdsWriter.writeBigDecimal((BigDecimal) colValue, bulkJdbcType, bulkPrecision); - } - break; + case java.sql.Types.REAL: + if (null == colValue) { + writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); + } + else { + if (bulkNullable) { + tdsWriter.writeByte((byte) 0x04); + } + tdsWriter.writeReal((float) colValue); + } + break; - case microsoft.sql.Types.GUID: - case java.sql.Types.LONGVARCHAR: - case java.sql.Types.CHAR: // Fixed-length, non-Unicode string data. - case java.sql.Types.VARCHAR: // Variable-length, non-Unicode string data. - if (isStreaming) // PLP - { - // PLP_BODY rule in TDS - // Use ResultSet.getString for non-streaming data and ResultSet.getCharacterStream() for streaming data, - // so that if the source data source does not have streaming enabled, the smaller size data will still work. + case microsoft.sql.Types.MONEY: + case microsoft.sql.Types.SMALLMONEY: + case java.sql.Types.DECIMAL: + case java.sql.Types.NUMERIC: if (null == colValue) { writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); } else { - // Send length as unknown. - tdsWriter.writeLong(PLPInputStream.UNKNOWN_PLP_LEN); - try { - // Read and Send the data as chunks - // VARBINARYMAX --- only when streaming. - Reader reader = null; - if (colValue instanceof Reader) { - reader = (Reader) colValue; + /* + * if the precision that user provides is smaller than the precision of the actual value, the driver assumes the precision + * that user provides is the correct precision, and throws exception + */ + if (bulkPrecision < Util.getValueLengthBaseOnJavaType(colValue, JavaType.of(colValue), null, null, + JDBCType.of(bulkJdbcType))) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_valueOutOfRange")); + Object[] msgArgs = {SSType.DECIMAL}; + throw new SQLServerException(form.format(msgArgs), SQLState.DATA_EXCEPTION_LENGTH_MISMATCH, DriverError.NOT_SET, null); + } + tdsWriter.writeBigDecimal((BigDecimal) colValue, bulkJdbcType, bulkPrecision, bulkScale); + } + break; + + case microsoft.sql.Types.GUID: + case java.sql.Types.LONGVARCHAR: + case java.sql.Types.CHAR: // Fixed-length, non-Unicode string data. + case java.sql.Types.VARCHAR: // Variable-length, non-Unicode string data. + if (isStreaming) // PLP + { + // PLP_BODY rule in TDS + // Use ResultSet.getString for non-streaming data and ResultSet.getCharacterStream() for streaming data, + // so that if the source data source does not have streaming enabled, the smaller size data will still work. + if (null == colValue) { + writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); + } + else { + // Send length as unknown. + tdsWriter.writeLong(PLPInputStream.UNKNOWN_PLP_LEN); + try { + // Read and Send the data as chunks + // VARBINARYMAX --- only when streaming. + Reader reader; + if (colValue instanceof Reader) { + reader = (Reader) colValue; + } + else { + reader = new StringReader(colValue.toString()); + } + + if ((SSType.BINARY == destSSType) || (SSType.VARBINARY == destSSType) || (SSType.VARBINARYMAX == destSSType) + || (SSType.IMAGE == destSSType)) { + tdsWriter.writeNonUnicodeReader(reader, DataTypes.UNKNOWN_STREAM_LENGTH, true, null); + } + else { + SQLCollation destCollation = destColumnMetadata.get(destColOrdinal).collation; + if (null != destCollation) { + tdsWriter.writeNonUnicodeReader(reader, DataTypes.UNKNOWN_STREAM_LENGTH, false, destCollation.getCharset()); + } + else { + tdsWriter.writeNonUnicodeReader(reader, DataTypes.UNKNOWN_STREAM_LENGTH, false, null); + } + } + reader.close(); } - else { - reader = new StringReader(colValue.toString()); + catch (IOException e) { + throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), e); } - - if ((SSType.BINARY == destSSType) || (SSType.VARBINARY == destSSType) || (SSType.VARBINARYMAX == destSSType) - || (SSType.IMAGE == destSSType)) { - tdsWriter.writeNonUnicodeReader(reader, DataTypes.UNKNOWN_STREAM_LENGTH, true, null); + } + } + else // Non-PLP + { + if (null == colValue) { + writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); + } + else { + String colValueStr = colValue.toString(); + if ((SSType.BINARY == destSSType) || (SSType.VARBINARY == destSSType)) { + byte[] bytes = null; + try { + bytes = ParameterUtils.HexToBin(colValueStr); + } + catch (SQLServerException e) { + throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), e); + } + tdsWriter.writeShort((short) bytes.length); + tdsWriter.writeBytes(bytes); } else { + tdsWriter.writeShort((short) (colValueStr.length())); + // converting string into destination collation using Charset + SQLCollation destCollation = destColumnMetadata.get(destColOrdinal).collation; if (null != destCollation) { - tdsWriter.writeNonUnicodeReader(reader, DataTypes.UNKNOWN_STREAM_LENGTH, false, destCollation.getCharset()); + tdsWriter.writeBytes(colValueStr.getBytes(destColumnMetadata.get(destColOrdinal).collation.getCharset())); + } else { - tdsWriter.writeNonUnicodeReader(reader, DataTypes.UNKNOWN_STREAM_LENGTH, false, null); + tdsWriter.writeBytes(colValueStr.getBytes()); } } - reader.close(); - } - catch (IOException e) { - throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), e); } } - } - else // Non-PLP - { - if (null == colValue) { - writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); + break; + + /* + * The length value associated with these data types is specified within a USHORT. see MS-TDS.pdf page 38. However, nchar(n) + * nvarchar(n) supports n = 1 .. 4000 (see MSDN SQL 2014, SQL 2016 Transact-SQL) NVARCHAR/NCHAR/LONGNVARCHAR is not compatible with + * BINARY/VARBINARY as specified in enum UpdaterConversion of DataTypes.java + */ + case java.sql.Types.LONGNVARCHAR: + case java.sql.Types.NCHAR: + case java.sql.Types.NVARCHAR: + if (isStreaming) { + // PLP_BODY rule in TDS + // Use ResultSet.getString for non-streaming data and ResultSet.getNCharacterStream() for streaming data, + // so that if the source data source does not have streaming enabled, the smaller size data will still work. + if (null == colValue) { + writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); + } + else { + // Send length as unknown. + tdsWriter.writeLong(PLPInputStream.UNKNOWN_PLP_LEN); + try { + // Read and Send the data as chunks. + Reader reader; + if (colValue instanceof Reader) { + reader = (Reader) colValue; + } + else { + reader = new StringReader(colValue.toString()); + } + + // writeReader is unicode. + tdsWriter.writeReader(reader, DataTypes.UNKNOWN_STREAM_LENGTH, true); + reader.close(); + } + catch (IOException e) { + throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), e); + } + } } else { - String colValueStr = colValue.toString(); - if ((SSType.BINARY == destSSType) || (SSType.VARBINARY == destSSType)) { - byte[] bytes = null; + if (null == colValue) { + writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); + } + else { + int stringLength = colValue.toString().length(); + byte[] typevarlen = new byte[2]; + typevarlen[0] = (byte) (2 * stringLength & 0xFF); + typevarlen[1] = (byte) ((2 * stringLength >> 8) & 0xFF); + tdsWriter.writeBytes(typevarlen); + tdsWriter.writeString(colValue.toString()); + } + } + break; + + case java.sql.Types.LONGVARBINARY: + case java.sql.Types.BINARY: + case java.sql.Types.VARBINARY: + if (isStreaming) // PLP + { + // Check for null separately for streaming and non-streaming data types, there could be source data sources who + // does not support streaming data. + if (null == colValue) { + writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); + } + else { + // Send length as unknown. + tdsWriter.writeLong(PLPInputStream.UNKNOWN_PLP_LEN); try { - bytes = ParameterUtils.HexToBin(colValueStr); + // Read and Send the data as chunks + InputStream iStream; + if (colValue instanceof InputStream) { + iStream = (InputStream) colValue; + } + else { + if (colValue instanceof byte[]) { + iStream = new ByteArrayInputStream((byte[]) colValue); + } + else + iStream = new ByteArrayInputStream(ParameterUtils.HexToBin(colValue.toString())); + } + // We do not need to check for null values here as it is already checked above. + tdsWriter.writeStream(iStream, DataTypes.UNKNOWN_STREAM_LENGTH, true); + iStream.close(); } - catch (SQLServerException e) { + catch (IOException e) { throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), e); } - tdsWriter.writeShort((short) bytes.length); - tdsWriter.writeBytes(bytes); + } + } + else // Non-PLP + { + if (null == colValue) { + writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); } else { - tdsWriter.writeShort((short) (colValueStr.length())); - // converting string into destination collation using Charset - - SQLCollation destCollation = destColumnMetadata.get(destColOrdinal).collation; - if (null != destCollation) { - tdsWriter.writeBytes(colValueStr.getBytes(destColumnMetadata.get(destColOrdinal).collation.getCharset())); - + byte[] srcBytes; + if (colValue instanceof byte[]) { + srcBytes = (byte[]) colValue; } else { - tdsWriter.writeBytes(colValueStr.getBytes()); + try { + srcBytes = ParameterUtils.HexToBin(colValue.toString()); + } + catch (SQLServerException e) { + throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), e); + } } + tdsWriter.writeShort((short) srcBytes.length); + tdsWriter.writeBytes(srcBytes); } } - } - break; + break; - /* - * The length value associated with these data types is specified within a USHORT. see MS-TDS.pdf page 38. However, nchar(n) nvarchar(n) - * supports n = 1 .. 4000 (see MSDN SQL 2014, SQL 2016 Transact-SQL) NVARCHAR/NCHAR/LONGNVARCHAR is not compatible with BINARY/VARBINARY - * as specified in enum UpdaterConversion of DataTypes.java - */ - case java.sql.Types.LONGNVARCHAR: - case java.sql.Types.NCHAR: - case java.sql.Types.NVARCHAR: - if (isStreaming) { - // PLP_BODY rule in TDS - // Use ResultSet.getString for non-streaming data and ResultSet.getNCharacterStream() for streaming data, - // so that if the source data source does not have streaming enabled, the smaller size data will still work. + case microsoft.sql.Types.DATETIME: + case microsoft.sql.Types.SMALLDATETIME: + case java.sql.Types.TIMESTAMP: if (null == colValue) { writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); } else { - // Send length as unknown. - tdsWriter.writeLong(PLPInputStream.UNKNOWN_PLP_LEN); - try { - // Read and Send the data as chunks. - Reader reader = null; - if (colValue instanceof Reader) { - reader = (Reader) colValue; - } - else { - reader = new StringReader(colValue.toString()); - } - - // writeReader is unicode. - tdsWriter.writeReader(reader, DataTypes.UNKNOWN_STREAM_LENGTH, true); - reader.close(); - } - catch (IOException e) { - throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), e); + switch (destSSType) { + case SMALLDATETIME: + if (bulkNullable) + tdsWriter.writeByte((byte) 0x04); + tdsWriter.writeSmalldatetime(colValue.toString()); + break; + case DATETIME: + if (bulkNullable) + tdsWriter.writeByte((byte) 0x08); + tdsWriter.writeDatetime(colValue.toString()); + break; + default: // DATETIME2 + if (bulkNullable) { + if (2 >= bulkScale) + tdsWriter.writeByte((byte) 0x06); + else if (4 >= bulkScale) + tdsWriter.writeByte((byte) 0x07); + else + tdsWriter.writeByte((byte) 0x08); + } + String timeStampValue = colValue.toString(); + tdsWriter.writeTime(java.sql.Timestamp.valueOf(timeStampValue), bulkScale); + // Send only the date part + tdsWriter.writeDate(timeStampValue.substring(0, timeStampValue.lastIndexOf(' '))); } } - } - else { + break; + + case java.sql.Types.DATE: if (null == colValue) { writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); } else { - int stringLength = colValue.toString().length(); - byte[] typevarlen = new byte[2]; - typevarlen[0] = (byte) (2 * stringLength & 0xFF); - typevarlen[1] = (byte) ((2 * stringLength >> 8) & 0xFF); - tdsWriter.writeBytes(typevarlen); - tdsWriter.writeString(colValue.toString()); + tdsWriter.writeByte((byte) 0x03); + tdsWriter.writeDate(colValue.toString()); } - } - break; + break; - case java.sql.Types.LONGVARBINARY: - case java.sql.Types.BINARY: - case java.sql.Types.VARBINARY: - if (isStreaming) // PLP - { - // Check for null separately for streaming and non-streaming data types, there could be source data sources who - // does not support streaming data. + case java.sql.Types.TIME: + // java.sql.Types.TIME allows maximum of 3 fractional second precision + // SQL Server time(n) allows maximum of 7 fractional second precision, to avoid truncation + // values are read as java.sql.Types.TIMESTAMP if srcJdbcType is java.sql.Types.TIME if (null == colValue) { writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); } else { - // Send length as unknown. - tdsWriter.writeLong(PLPInputStream.UNKNOWN_PLP_LEN); - try { - // Read and Send the data as chunks - InputStream iStream = null; - if (colValue instanceof InputStream) { - iStream = (InputStream) colValue; - } - else { - if (colValue instanceof byte[]) { - iStream = new ByteArrayInputStream((byte[]) colValue); - } - else - iStream = new ByteArrayInputStream(ParameterUtils.HexToBin(colValue.toString())); - } - // We do not need to check for null values here as it is already checked above. - tdsWriter.writeStream(iStream, DataTypes.UNKNOWN_STREAM_LENGTH, true); - iStream.close(); - } - catch (IOException e) { - throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), e); - } + if (2 >= bulkScale) + tdsWriter.writeByte((byte) 0x03); + else if (4 >= bulkScale) + tdsWriter.writeByte((byte) 0x04); + else + tdsWriter.writeByte((byte) 0x05); + + tdsWriter.writeTime((java.sql.Timestamp) colValue, bulkScale); } - } - else // Non-PLP - { + break; + + case 2013: // java.sql.Types.TIME_WITH_TIMEZONE if (null == colValue) { writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); } else { - byte[] srcBytes; - if (colValue instanceof byte[]) { - srcBytes = (byte[]) colValue; - } - else { - try { - srcBytes = ParameterUtils.HexToBin(colValue.toString()); - } - catch (SQLServerException e) { - throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), e); - } - } - tdsWriter.writeShort((short) srcBytes.length); - tdsWriter.writeBytes(srcBytes); + if (2 >= bulkScale) + tdsWriter.writeByte((byte) 0x08); + else if (4 >= bulkScale) + tdsWriter.writeByte((byte) 0x09); + else + tdsWriter.writeByte((byte) 0x0A); + + tdsWriter.writeOffsetTimeWithTimezone((OffsetTime) colValue, bulkScale); } - } - break; + break; - case microsoft.sql.Types.DATETIME: - case microsoft.sql.Types.SMALLDATETIME: - case java.sql.Types.TIMESTAMP: - if (null == colValue) { - writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); + case 2014: // java.sql.Types.TIMESTAMP_WITH_TIMEZONE + if (null == colValue) { + writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); + } + else { + if (2 >= bulkScale) + tdsWriter.writeByte((byte) 0x08); + else if (4 >= bulkScale) + tdsWriter.writeByte((byte) 0x09); + else + tdsWriter.writeByte((byte) 0x0A); + + tdsWriter.writeOffsetDateTimeWithTimezone((OffsetDateTime) colValue, bulkScale); + } + break; + + case microsoft.sql.Types.DATETIMEOFFSET: + // We can safely cast the result set to a SQLServerResultSet as the DatetimeOffset type is only available in the JDBC driver. + if (null == colValue) { + writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); + } + else { + if (2 >= bulkScale) + tdsWriter.writeByte((byte) 0x08); + else if (4 >= bulkScale) + tdsWriter.writeByte((byte) 0x09); + else + tdsWriter.writeByte((byte) 0x0A); + + tdsWriter.writeDateTimeOffset(colValue, bulkScale, destSSType); + } + break; + case microsoft.sql.Types.SQL_VARIANT: + boolean isShiloh = (8 >= connection.getServerMajorVersion()); + if (isShiloh) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_SQLVariantSupport")); + throw new SQLServerException(null, form.format(new Object[] {}), null, 0, false); + } + writeSqlVariant(tdsWriter, colValue, sourceResultSet, srcColOrdinal, destColOrdinal, bulkJdbcType, bulkScale, isStreaming); + break; + default: + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_BulkTypeNotSupported")); + Object[] msgArgs = {JDBCType.of(bulkJdbcType).toString().toLowerCase(Locale.ENGLISH)}; + SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), null, true); + break; + } // End of switch + } + catch (ClassCastException ex) { + if (null == colValue) { + // this should not really happen, since ClassCastException should only happen when colValue is not null. + // just do one more checking here to make sure + throwInvalidArgument("colValue"); + } + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorConvertingValue")); + Object[] msgArgs = {colValue.getClass().getSimpleName(), JDBCType.of(bulkJdbcType)}; + throw new SQLServerException(form.format(msgArgs), SQLState.DATA_EXCEPTION_NOT_SPECIFIC, DriverError.NOT_SET, ex); + } + } + + /** + * Writes sql_variant data based on the baseType for bulkcopy + * + * @throws SQLServerException + */ + private void writeSqlVariant(TDSWriter tdsWriter, + Object colValue, + ResultSet sourceResultSet, + int srcColOrdinal, + int destColOrdinal, + int bulkJdbcType, + int bulkScale, + boolean isStreaming) throws SQLServerException { + if (null == colValue) { + writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); + return; + } + SqlVariant variantType = ((SQLServerResultSet) sourceResultSet).getVariantInternalType(srcColOrdinal); + int baseType = variantType.getBaseType(); + // for sql variant we normally should return the colvalue for time as time string. but for + // bulkcopy we need it to be timestamp. so we have to retrieve it again once we are in bulkcopy + // and make sure that the base type is time. + if (TDSType.TIMEN == TDSType.valueOf(baseType)) { + variantType.setIsBaseTypeTimeValue(true); + ((SQLServerResultSet) sourceResultSet).setInternalVariantType(srcColOrdinal, variantType); + colValue = ((SQLServerResultSet) sourceResultSet).getObject(srcColOrdinal); + } + switch (TDSType.valueOf(baseType)) { + case INT8: + writeBulkCopySqlVariantHeader(10, TDSType.INT8.byteValue(), (byte) 0, tdsWriter); + tdsWriter.writeLong(Long.valueOf(colValue.toString())); + break; + + case INT4: + writeBulkCopySqlVariantHeader(6, TDSType.INT4.byteValue(), (byte) 0, tdsWriter); + tdsWriter.writeInt(Integer.valueOf(colValue.toString())); + break; + + case INT2: + writeBulkCopySqlVariantHeader(4, TDSType.INT2.byteValue(), (byte) 0, tdsWriter); + tdsWriter.writeShort(Short.valueOf(colValue.toString())); + break; + + case INT1: + writeBulkCopySqlVariantHeader(3, TDSType.INT1.byteValue(), (byte) 0, tdsWriter); + tdsWriter.writeByte(Byte.valueOf(colValue.toString())); + break; + + case FLOAT8: + writeBulkCopySqlVariantHeader(10, TDSType.FLOAT8.byteValue(), (byte) 0, tdsWriter); + tdsWriter.writeDouble(Double.valueOf(colValue.toString())); + break; + + case FLOAT4: + writeBulkCopySqlVariantHeader(6, TDSType.FLOAT4.byteValue(), (byte) 0, tdsWriter); + tdsWriter.writeReal(Float.valueOf(colValue.toString())); + break; + + case MONEY8: + // For decimalN we right TDSWriter.BIGDECIMAL_MAX_LENGTH as maximum length = 17 + // 17 + 2 for basetype and probBytes + 2 for precision and length = 21 the length of data in header + writeBulkCopySqlVariantHeader(21, TDSType.DECIMALN.byteValue(), (byte) 2, tdsWriter); + tdsWriter.writeByte((byte) 38); + tdsWriter.writeByte((byte) 4); + tdsWriter.writeSqlVariantInternalBigDecimal((BigDecimal) colValue, bulkJdbcType); + break; + + case MONEY4: + writeBulkCopySqlVariantHeader(21, TDSType.DECIMALN.byteValue(), (byte) 2, tdsWriter); + tdsWriter.writeByte((byte) 38); + tdsWriter.writeByte((byte) 4); + tdsWriter.writeSqlVariantInternalBigDecimal((BigDecimal) colValue, bulkJdbcType); + break; + + case BIT1: + writeBulkCopySqlVariantHeader(3, TDSType.BIT1.byteValue(), (byte) 0, tdsWriter); + tdsWriter.writeByte((byte) (((Boolean) colValue).booleanValue() ? 1 : 0)); + break; + + case DATEN: + writeBulkCopySqlVariantHeader(5, TDSType.DATEN.byteValue(), (byte) 0, tdsWriter); + tdsWriter.writeDate(colValue.toString()); + break; + + case TIMEN: + bulkScale = variantType.getScale(); + int timeHeaderLength = 0x08; // default + if (2 >= bulkScale) { + timeHeaderLength = 0x06; + } + else if (4 >= bulkScale) { + timeHeaderLength = 0x07; } else { - switch (destSSType) { - case SMALLDATETIME: - if (bulkNullable) - tdsWriter.writeByte((byte) 0x04); - tdsWriter.writeSmalldatetime(colValue.toString()); - break; - case DATETIME: - if (bulkNullable) - tdsWriter.writeByte((byte) 0x08); - tdsWriter.writeDatetime(colValue.toString()); - break; - default: // DATETIME2 - if (bulkNullable) { - if (2 >= bulkScale) - tdsWriter.writeByte((byte) 0x06); - else if (4 >= bulkScale) - tdsWriter.writeByte((byte) 0x07); - else - tdsWriter.writeByte((byte) 0x08); - } - String timeStampValue = colValue.toString(); - tdsWriter.writeTime(java.sql.Timestamp.valueOf(timeStampValue), bulkScale); - // Send only the date part - tdsWriter.writeDate(timeStampValue.substring(0, timeStampValue.lastIndexOf(' '))); - } + timeHeaderLength = 0x08; } + writeBulkCopySqlVariantHeader(timeHeaderLength, TDSType.TIMEN.byteValue(), (byte) 1, tdsWriter); // depending on scale, the header + // length + // defers + tdsWriter.writeByte((byte) bulkScale); + tdsWriter.writeTime((java.sql.Timestamp) colValue, bulkScale); break; - - case java.sql.Types.DATE: - if (null == colValue) { - writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); + + case DATETIME8: + writeBulkCopySqlVariantHeader(10, TDSType.DATETIME8.byteValue(), (byte) 0, tdsWriter); + tdsWriter.writeDatetime(colValue.toString()); + break; + + case DATETIME4: + // when the type is ambiguous, we write to bigger type + writeBulkCopySqlVariantHeader(10, TDSType.DATETIME8.byteValue(), (byte) 0, tdsWriter); + tdsWriter.writeDatetime(colValue.toString()); + break; + + case DATETIME2N: + writeBulkCopySqlVariantHeader(10, TDSType.DATETIME2N.byteValue(), (byte) 1, tdsWriter); // 1 is probbytes for time + tdsWriter.writeByte((byte) 0x03); + String timeStampValue = colValue.toString(); + tdsWriter.writeTime(java.sql.Timestamp.valueOf(timeStampValue), 0x03); // datetime2 in sql_variant has up to scale 3 support + // Send only the date part + tdsWriter.writeDate(timeStampValue.substring(0, timeStampValue.lastIndexOf(' '))); + break; + + case BIGCHAR: + int length = colValue.toString().length(); + writeBulkCopySqlVariantHeader(9 + length, TDSType.BIGCHAR.byteValue(), (byte) 7, tdsWriter); + tdsWriter.writeCollationForSqlVariant(variantType); // writes collation info and sortID + tdsWriter.writeShort((short) (length)); + SQLCollation destCollation = destColumnMetadata.get(destColOrdinal).collation; + if (null != destCollation) { + tdsWriter.writeBytes(colValue.toString().getBytes(destColumnMetadata.get(destColOrdinal).collation.getCharset())); } else { - tdsWriter.writeByte((byte) 0x03); - tdsWriter.writeDate(colValue.toString()); + tdsWriter.writeBytes(colValue.toString().getBytes()); } break; + + case BIGVARCHAR: + length = colValue.toString().length(); + writeBulkCopySqlVariantHeader(9 + length, TDSType.BIGVARCHAR.byteValue(), (byte) 7, tdsWriter); + tdsWriter.writeCollationForSqlVariant(variantType); // writes collation info and sortID + tdsWriter.writeShort((short) (length)); - case java.sql.Types.TIME: - // java.sql.Types.TIME allows maximum of 3 fractional second precision - // SQL Server time(n) allows maximum of 7 fractional second precision, to avoid truncation - // values are read as java.sql.Types.TIMESTAMP if srcJdbcType is java.sql.Types.TIME - if (null == colValue) { - writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); + destCollation = destColumnMetadata.get(destColOrdinal).collation; + if (null != destCollation) { + tdsWriter.writeBytes(colValue.toString().getBytes(destColumnMetadata.get(destColOrdinal).collation.getCharset())); } else { - if (2 >= bulkScale) - tdsWriter.writeByte((byte) 0x03); - else if (4 >= bulkScale) - tdsWriter.writeByte((byte) 0x04); - else - tdsWriter.writeByte((byte) 0x05); - - tdsWriter.writeTime((java.sql.Timestamp) colValue, bulkScale); + tdsWriter.writeBytes(colValue.toString().getBytes()); } break; - - case 2013: // java.sql.Types.TIME_WITH_TIMEZONE - if (null == colValue) { - writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); + + case NCHAR: + length = colValue.toString().length() * 2; + writeBulkCopySqlVariantHeader(9 + length, TDSType.NCHAR.byteValue(), (byte) 7, tdsWriter); + tdsWriter.writeCollationForSqlVariant(variantType); // writes collation info and sortID + int stringLength = colValue.toString().length(); + byte[] typevarlen = new byte[2]; + typevarlen[0] = (byte) (2 * stringLength & 0xFF); + typevarlen[1] = (byte) ((2 * stringLength >> 8) & 0xFF); + tdsWriter.writeBytes(typevarlen); + tdsWriter.writeString(colValue.toString()); + break; + + case NVARCHAR: + length = colValue.toString().length() * 2; + writeBulkCopySqlVariantHeader(9 + length, TDSType.NVARCHAR.byteValue(), (byte) 7, tdsWriter); + tdsWriter.writeCollationForSqlVariant(variantType); // writes collation info and sortID + stringLength = colValue.toString().length(); + typevarlen = new byte[2]; + typevarlen[0] = (byte) (2 * stringLength & 0xFF); + typevarlen[1] = (byte) ((2 * stringLength >> 8) & 0xFF); + tdsWriter.writeBytes(typevarlen); + tdsWriter.writeString(colValue.toString()); + break; + + case GUID: + length = colValue.toString().length(); + writeBulkCopySqlVariantHeader(9 + length, TDSType.BIGCHAR.byteValue(), (byte) 7, tdsWriter); + // since while reading collation from sourceMetaData in guid we don't read collation, cause we are reading binary + // but in writing it we are using char, we need to get the collation. + SQLCollation collation = (null != destColumnMetadata.get(srcColOrdinal).collation) ? destColumnMetadata.get(srcColOrdinal).collation + : connection.getDatabaseCollation(); + variantType.setCollation(collation); + tdsWriter.writeCollationForSqlVariant(variantType); // writes collation info and sortID + tdsWriter.writeShort((short) (length)); + // converting string into destination collation using Charset + destCollation = destColumnMetadata.get(destColOrdinal).collation; + if (null != destCollation) { + tdsWriter.writeBytes(colValue.toString().getBytes(destColumnMetadata.get(destColOrdinal).collation.getCharset())); } else { - if (2 >= bulkScale) - tdsWriter.writeByte((byte) 0x08); - else if (4 >= bulkScale) - tdsWriter.writeByte((byte) 0x09); - else - tdsWriter.writeByte((byte) 0x0A); - - tdsWriter.writeOffsetTimeWithTimezone((OffsetTime) colValue, bulkScale); + tdsWriter.writeBytes(colValue.toString().getBytes()); } break; - - case 2014: // java.sql.Types.TIMESTAMP_WITH_TIMEZONE + + case BIGBINARY: + byte[] b = (byte[]) colValue; + length = b.length; + writeBulkCopySqlVariantHeader(4 + length, TDSType.BIGVARBINARY.byteValue(), (byte) 2, tdsWriter); + tdsWriter.writeShort((short) (variantType.getMaxLength())); // length if (null == colValue) { writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); } else { - if (2 >= bulkScale) - tdsWriter.writeByte((byte) 0x08); - else if (4 >= bulkScale) - tdsWriter.writeByte((byte) 0x09); - else - tdsWriter.writeByte((byte) 0x0A); - - tdsWriter.writeOffsetDateTimeWithTimezone((OffsetDateTime) colValue, bulkScale); + byte[] srcBytes; + if (colValue instanceof byte[]) { + srcBytes = (byte[]) colValue; + } + else { + try { + srcBytes = ParameterUtils.HexToBin(colValue.toString()); + } + catch (SQLServerException e) { + throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), e); + } + } + tdsWriter.writeBytes(srcBytes); } break; - - case microsoft.sql.Types.DATETIMEOFFSET: - // We can safely cast the result set to a SQLServerResultSet as the DatetimeOffset type is only available in the JDBC driver. + + case BIGVARBINARY: + b = (byte[]) colValue; + length = b.length; + writeBulkCopySqlVariantHeader(4 + length, TDSType.BIGVARBINARY.byteValue(), (byte) 2, tdsWriter); + tdsWriter.writeShort((short) (variantType.getMaxLength())); // length if (null == colValue) { writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming); } else { - if (2 >= bulkScale) - tdsWriter.writeByte((byte) 0x08); - else if (4 >= bulkScale) - tdsWriter.writeByte((byte) 0x09); - else - tdsWriter.writeByte((byte) 0x0A); - - tdsWriter.writeDateTimeOffset(colValue, bulkScale, destSSType); + byte[] srcBytes; + if (colValue instanceof byte[]) { + srcBytes = (byte[]) colValue; + } + else { + try { + srcBytes = ParameterUtils.HexToBin(colValue.toString()); + } + catch (SQLServerException e) { + throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), e); + } + } + tdsWriter.writeBytes(srcBytes); } break; - + default: MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_BulkTypeNotSupported")); Object[] msgArgs = {JDBCType.of(bulkJdbcType).toString().toLowerCase(Locale.ENGLISH)}; SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), null, true); - } // End of switch + break; + } + } + + /** + * Write header for sql_variant + * + * @param length: + * length of base type + Basetype + probBytes + * @param tdsType + * @param probBytes + * @param tdsWriter + * @throws SQLServerException + */ + private void writeBulkCopySqlVariantHeader(int length, + byte tdsType, + byte probBytes, + TDSWriter tdsWriter) throws SQLServerException { + tdsWriter.writeInt(length); + tdsWriter.writeByte(tdsType); + tdsWriter.writeByte(probBytes); } private Object readColumnFromResultSet(int srcColOrdinal, @@ -2534,29 +2902,29 @@ private Object readColumnFromResultSet(int srcColOrdinal, case microsoft.sql.Types.DATETIME: case microsoft.sql.Types.SMALLDATETIME: case java.sql.Types.TIMESTAMP: - return sourceResultSet.getTimestamp(srcColOrdinal); - - case java.sql.Types.DATE: - return sourceResultSet.getDate(srcColOrdinal); - case java.sql.Types.TIME: // java.sql.Types.TIME allows maximum of 3 fractional second precision // SQL Server time(n) allows maximum of 7 fractional second precision, to avoid truncation // values are read as java.sql.Types.TIMESTAMP if srcJdbcType is java.sql.Types.TIME return sourceResultSet.getTimestamp(srcColOrdinal); + case java.sql.Types.DATE: + return sourceResultSet.getDate(srcColOrdinal); + case microsoft.sql.Types.DATETIMEOFFSET: // We can safely cast the result set to a SQLServerResultSet as the DatetimeOffset type is only available in the JDBC driver. return ((SQLServerResultSet) sourceResultSet).getDateTimeOffset(srcColOrdinal); + case microsoft.sql.Types.SQL_VARIANT: + return sourceResultSet.getObject(srcColOrdinal); default: MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_BulkTypeNotSupported")); Object[] msgArgs = {JDBCType.of(srcJdbcType).toString().toLowerCase(Locale.ENGLISH)}; SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), null, true); // This return will never be executed, but it is needed as Eclipse complains otherwise. return null; - } // End of switch - }// End of Try + } + } catch (SQLException e) { throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), e); } @@ -2569,9 +2937,9 @@ private void writeColumn(TDSWriter tdsWriter, int srcColOrdinal, int destColOrdinal, Object colValue) throws SQLServerException { - int srcPrecision = 0, srcScale = 0, destPrecision = 0, srcJdbcType = 0; + int srcPrecision, srcScale, destPrecision, srcJdbcType; SSType destSSType = null; - boolean isStreaming = false, srcNullable; + boolean isStreaming, srcNullable; srcPrecision = srcColumnMetadata.get(srcColOrdinal).precision; srcScale = srcColumnMetadata.get(srcColOrdinal).scale; srcJdbcType = srcColumnMetadata.get(srcColOrdinal).jdbcType; @@ -2604,6 +2972,10 @@ private void writeColumn(TDSWriter tdsWriter, validateDataTypeConversions(srcColOrdinal, destColOrdinal); } } + //If we are using ISQLBulkRecord and the data we are passing is char type, we need to check the source and dest precision + else if (null != sourceBulkRecord && (null == destCryptoMeta)) { + validateStringBinaryLengths(colValue, srcColOrdinal, destColOrdinal); + } else if ((null != sourceBulkRecord) && (null != destCryptoMeta)) { // From CSV to encrypted column. Convert to respective object. if ((java.sql.Types.DATE == srcJdbcType) || (java.sql.Types.TIME == srcJdbcType) || (java.sql.Types.TIMESTAMP == srcJdbcType) @@ -2723,16 +3095,14 @@ else if (2014 == srcJdbcType) { switch (srcJdbcType) { case java.sql.Types.TIMESTAMP: case java.sql.Types.TIME: - return null; case java.sql.Types.DATE: - return null; case microsoft.sql.Types.DATETIMEOFFSET: return null; } } // If we are here value is non-null. - Calendar cal = null; + Calendar cal; // Get the temporal values from the formatter DateTimeFormatter dateTimeFormatter = srcColumnMetadata.get(srcColOrdinal).dateTimeFormatter; @@ -2777,7 +3147,7 @@ else if (2014 == srcJdbcType) { startIndx = ++endIndx; // skip the : endIndx = valueStr.indexOf('.', startIndx); - int seconds = 0, offsethour, offsetMinute, totalOffset = 0, fractionalSeconds = 0; + int seconds, offsethour, offsetMinute, totalOffset = 0, fractionalSeconds = 0; boolean isNegativeOffset = false; boolean hasTimeZone = false; int fractionalSecondsLength = 0; @@ -2807,7 +3177,7 @@ else if (2014 == srcJdbcType) { } else { seconds = Integer.parseInt(valueStr.substring(startIndx)); - startIndx = ++endIndx; // skip the space + ++endIndx; // skip the space } } if (hasTimeZone) { @@ -2867,8 +3237,8 @@ private byte[] getEncryptedTemporalBytes(TDSWriter tdsWriter, Object colValue, int srcColOrdinal, int scale) throws SQLServerException { - long utcMillis = 0; - GregorianCalendar calendar = null; + long utcMillis; + GregorianCalendar calendar; switch (srcTemporalJdbcType) { case DATE: @@ -2886,7 +3256,7 @@ private byte[] getEncryptedTemporalBytes(TDSWriter tdsWriter, calendar.clear(); utcMillis = ((java.sql.Timestamp) colValue).getTime(); calendar.setTimeInMillis(utcMillis); - int subSecondNanos = 0; + int subSecondNanos; if (colValue instanceof java.sql.Timestamp) { subSecondNanos = ((java.sql.Timestamp) colValue).getNanos(); } @@ -2947,62 +3317,62 @@ private byte[] normalizedValue(JDBCType destJdbcType, try { switch (destJdbcType) { case BIT: - longValue = Long.valueOf((Boolean) value ? 1 : 0); - return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN).putLong(longValue.longValue()).array(); + longValue = (long) ((Boolean) value ? 1 : 0); + return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN).putLong(longValue).array(); case TINYINT: case SMALLINT: switch (srcJdbcType) { case BIT: - longValue = new Long((Boolean) value ? 1 : 0); + longValue = (long) ((Boolean) value ? 1 : 0); break; default: if (value instanceof Integer) { int intValue = (int) value; short shortValue = (short) intValue; - longValue = new Long(shortValue); + longValue = (long) shortValue; } else - longValue = new Long((short) value); + longValue = (long) (short) value; } - return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN).putLong(longValue.longValue()).array(); + return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN).putLong(longValue).array(); case INTEGER: switch (srcJdbcType) { case BIT: - longValue = new Long((Boolean) value ? 1 : 0); + longValue = (long) ((Boolean) value ? 1 : 0); break; case TINYINT: case SMALLINT: - longValue = new Long((short) value); + longValue = (long) (short) value; break; default: longValue = new Long((Integer) value); } - return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN).putLong(longValue.longValue()).array(); + return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN).putLong(longValue).array(); case BIGINT: switch (srcJdbcType) { case BIT: - longValue = new Long((Boolean) value ? 1 : 0); + longValue = (long) ((Boolean) value ? 1 : 0); break; case TINYINT: case SMALLINT: - longValue = new Long((short) value); + longValue = (long) (short) value; break; case INTEGER: longValue = new Long((Integer) value); break; default: - longValue = new Long((long) value); + longValue = (long) value; } - return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN).putLong(longValue.longValue()).array(); + return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN).putLong(longValue).array(); case BINARY: case VARBINARY: case LONGVARBINARY: - byte[] byteArrayValue = null; + byte[] byteArrayValue; if (value instanceof String) { byteArrayValue = ParameterUtils.HexToBin((String) value); } @@ -3132,7 +3502,9 @@ private boolean goToNextRow() throws SQLServerException { * Writes data for a batch of rows to the TDSWriter object. Writes the following part in the BulkLoadBCP stream * (https://msdn.microsoft.com/en-us/library/dd340549.aspx) ... */ - private boolean writeBatchData(TDSWriter tdsWriter) throws SQLServerException { + private boolean writeBatchData(TDSWriter tdsWriter, + TDSCommand command, + boolean insertRowByRow) throws SQLServerException { int batchsize = copyOptions.getBatchSize(); int row = 0; while (true) { @@ -3144,6 +3516,13 @@ private boolean writeBatchData(TDSWriter tdsWriter) throws SQLServerException { // No more data available, return false so we do not execute any more batches. if (!goToNextRow()) return false; + + if (insertRowByRow) { + // read response gotten from goToNextRow() + ((SQLServerResultSet) sourceResultSet).getTDSReader().readPacket(); + + tdsWriter = sendBulkCopyCommand(command); + } // Write row header for each row. tdsWriter.writeByte((byte) TDS.TDS_ROW); @@ -3153,29 +3532,45 @@ private boolean writeBatchData(TDSWriter tdsWriter) throws SQLServerException { if (null != sourceResultSet) { // Loop for each destination column. The mappings is a many to one mapping // where multiple source columns can be mapped to one destination column. - for (int i = 0; i < mappingColumnCount; ++i) { - writeColumn(tdsWriter, columnMappings.get(i).sourceColumnOrdinal, columnMappings.get(i).destinationColumnOrdinal, null // cell - // value is - // retrieved - // inside - // writeRowData() - // method. + for (ColumnMapping columnMapping : columnMappings) { + writeColumn(tdsWriter, columnMapping.sourceColumnOrdinal, columnMapping.destinationColumnOrdinal, null // cell + // value is + // retrieved + // inside + // writeRowData() + // method. ); } } // Copy from a file. else { // Get all the column values of the current row. - Object[] rowObjects = sourceBulkRecord.getRowData(); + Object[] rowObjects; - for (int i = 0; i < mappingColumnCount; ++i) { + try { + rowObjects = sourceBulkRecord.getRowData(); + } + catch (Exception ex) { + // if no more data available to retrive + throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), ex); + } + + for (ColumnMapping columnMapping : columnMappings) { // If the SQLServerBulkCSVRecord does not have metadata for columns, it returns strings in the object array. // COnvert the strings using destination table types. - writeColumn(tdsWriter, columnMappings.get(i).sourceColumnOrdinal, columnMappings.get(i).destinationColumnOrdinal, - rowObjects[columnMappings.get(i).sourceColumnOrdinal - 1]); + writeColumn(tdsWriter, columnMapping.sourceColumnOrdinal, columnMapping.destinationColumnOrdinal, + rowObjects[columnMapping.sourceColumnOrdinal - 1]); } } row++; + + if (insertRowByRow) { + writePacketDataDone(tdsWriter); + tdsWriter.setCryptoMetaData(null); + + // Send to the server and read response. + TDSParser.parse(command.startResponse(), command.getLogContext()); + } } } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy42Helper.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy42Helper.java index 5b5533bc3..ea9ff510f 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy42Helper.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy42Helper.java @@ -56,8 +56,7 @@ static Object getTemporalObjectFromCSVWithFormatter(String valueStrUntrimmed, if (ta.isSupported(ChronoField.YEAR)) taYear = ta.get(ChronoField.YEAR); - Calendar cal = null; - cal = new GregorianCalendar(new SimpleTimeZone(taOffsetSec * 1000, "")); + Calendar cal = new GregorianCalendar(new SimpleTimeZone(taOffsetSec * 1000, "")); cal.clear(); cal.set(Calendar.HOUR_OF_DAY, taHour); cal.set(Calendar.MINUTE, taMin); @@ -76,7 +75,7 @@ static Object getTemporalObjectFromCSVWithFormatter(String valueStrUntrimmed, return ts; case java.sql.Types.TIME: // Time is returned as Timestamp to preserve nano seconds. - cal.set(connection.baseYear(), 00, 01); + cal.set(connection.baseYear(), Calendar.JANUARY, 01); ts = new java.sql.Timestamp(cal.getTimeInMillis()); ts.setNanos(taNano); return new java.sql.Timestamp(ts.getTime()); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopyOptions.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopyOptions.java index 774cd39cb..c247b1c28 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopyOptions.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopyOptions.java @@ -139,7 +139,7 @@ public int getBulkCopyTimeout() { * @param timeout * Number of seconds before operation times out. * @throws SQLServerException - * If the batchSize being set is invalid. + * If the timeout being set is invalid. */ public void setBulkCopyTimeout(int timeout) throws SQLServerException { if (timeout >= 0) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java index bd3cbba8a..a4bc185b7 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java @@ -30,8 +30,7 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.Calendar; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.util.UUID; /** * CallableStatement implements JDBC callable statements. CallableStatement allows the caller to specify the procedure name to call along with input @@ -91,11 +90,11 @@ String getClassNameInternal() { public void registerOutParameter(int index, int sqlType) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {new Integer(index), new Integer(sqlType)}); + loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {index, sqlType}); checkClosed(); if (index < 1 || index > inOutParam.length) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_indexOutOfRange")); - Object[] msgArgs = {new Integer(index)}; + Object[] msgArgs = {index}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), "7009", false); } @@ -326,7 +325,7 @@ boolean onRetValue(TDSReader tdsReader) throws SQLServerException { // If we were asked to retain the OUT parameters as we skip past them, // then report an error if we did not find any. MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_valueNotSetForParameter")); - Object[] msgArgs = {new Integer(outParamIndex + 1)}; + Object[] msgArgs = {outParamIndex + 1}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, false); } @@ -354,7 +353,7 @@ boolean onRetValue(TDSReader tdsReader) throws SQLServerException { int sqlType, String typeName) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {new Integer(index), new Integer(sqlType), typeName}); + loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {index, sqlType, typeName}); checkClosed(); @@ -368,7 +367,7 @@ boolean onRetValue(TDSReader tdsReader) throws SQLServerException { int scale) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "registerOutParameter", - new Object[] {new Integer(index), new Integer(sqlType), new Integer(scale)}); + new Object[] {index, sqlType, scale}); checkClosed(); @@ -384,7 +383,7 @@ public void registerOutParameter(int index, int scale) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "registerOutParameter", - new Object[] {new Integer(index), new Integer(sqlType), new Integer(scale), new Integer(precision)}); + new Object[] {index, sqlType, scale, precision}); checkClosed(); @@ -403,14 +402,14 @@ private Parameter getterGetParam(int index) throws SQLServerException { // Check for valid index if (index < 1 || index > inOutParam.length) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidOutputParameter")); - Object[] msgArgs = {new Integer(index)}; + Object[] msgArgs = {index}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), "07009", false); } // Check index refers to a registered OUT parameter if (!inOutParam[index - 1].isOutput()) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_outputParameterNotRegisteredForOutput")); - Object[] msgArgs = {new Integer(index)}; + Object[] msgArgs = {index}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), "07009", true); } @@ -465,7 +464,7 @@ public int getInt(int index) throws SQLServerException { checkClosed(); Integer value = (Integer) getValue(index, JDBCType.INTEGER); loggerExternal.exiting(getClassNameLogging(), "getInt", value); - return null != value ? value.intValue() : 0; + return null != value ? value : 0; } public int getInt(String sCol) throws SQLServerException { @@ -473,28 +472,34 @@ public int getInt(String sCol) throws SQLServerException { checkClosed(); Integer value = (Integer) getValue(findColumn(sCol), JDBCType.INTEGER); loggerExternal.exiting(getClassNameLogging(), "getInt", value); - return null != value ? value.intValue() : 0; + return null != value ? value : 0; } public String getString(int index) throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getString", index); checkClosed(); - String value = (String) getValue(index, JDBCType.CHAR); + String value = null; + Object objectValue = getValue(index, JDBCType.CHAR); + if (null != objectValue) { + value = objectValue.toString(); + } loggerExternal.exiting(getClassNameLogging(), "getString", value); return value; } - + public String getString(String sCol) throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getString", sCol); checkClosed(); - String value = (String) getValue(findColumn(sCol), JDBCType.CHAR); + String value = null; + Object objectValue = getValue(findColumn(sCol), JDBCType.CHAR); + if (null != objectValue) { + value = objectValue.toString(); + } loggerExternal.exiting(getClassNameLogging(), "getString", value); return value; } public final String getNString(int parameterIndex) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - loggerExternal.entering(getClassNameLogging(), "getNString", parameterIndex); checkClosed(); String value = (String) getValue(parameterIndex, JDBCType.NCHAR); @@ -503,8 +508,6 @@ public final String getNString(int parameterIndex) throws SQLException { } public final String getNString(String parameterName) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - loggerExternal.entering(getClassNameLogging(), "getNString", parameterName); checkClosed(); String value = (String) getValue(findColumn(parameterName), JDBCType.NCHAR); @@ -516,7 +519,7 @@ public final String getNString(String parameterName) throws SQLException { public BigDecimal getBigDecimal(int parameterIndex, int scale) throws SQLException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "getBigDecimal", new Object[] {Integer.valueOf(parameterIndex), Integer.valueOf(scale)}); + loggerExternal.entering(getClassNameLogging(), "getBigDecimal", new Object[] {parameterIndex, scale}); checkClosed(); BigDecimal value = (BigDecimal) getValue(parameterIndex, JDBCType.DECIMAL); if (null != value) @@ -529,7 +532,7 @@ public BigDecimal getBigDecimal(int parameterIndex, public BigDecimal getBigDecimal(String parameterName, int scale) throws SQLException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "getBigDecimal", new Object[] {parameterName, Integer.valueOf(scale)}); + loggerExternal.entering(getClassNameLogging(), "getBigDecimal", new Object[] {parameterName, scale}); checkClosed(); BigDecimal value = (BigDecimal) getValue(findColumn(parameterName), JDBCType.DECIMAL); if (null != value) @@ -543,7 +546,7 @@ public boolean getBoolean(int index) throws SQLServerException { checkClosed(); Boolean value = (Boolean) getValue(index, JDBCType.BIT); loggerExternal.exiting(getClassNameLogging(), "getBoolean", value); - return null != value ? value.booleanValue() : false; + return null != value ? value : false; } public boolean getBoolean(String sCol) throws SQLServerException { @@ -551,7 +554,7 @@ public boolean getBoolean(String sCol) throws SQLServerException { checkClosed(); Boolean value = (Boolean) getValue(findColumn(sCol), JDBCType.BIT); loggerExternal.exiting(getClassNameLogging(), "getBoolean", value); - return null != value ? value.booleanValue() : false; + return null != value ? value : false; } public byte getByte(int index) throws SQLServerException { @@ -629,7 +632,7 @@ public double getDouble(int index) throws SQLServerException { checkClosed(); Double value = (Double) getValue(index, JDBCType.DOUBLE); loggerExternal.exiting(getClassNameLogging(), "getDouble", value); - return null != value ? value.doubleValue() : 0; + return null != value ? value : 0; } public double getDouble(String sCol) throws SQLServerException { @@ -637,7 +640,7 @@ public double getDouble(String sCol) throws SQLServerException { checkClosed(); Double value = (Double) getValue(findColumn(sCol), JDBCType.DOUBLE); loggerExternal.exiting(getClassNameLogging(), "getDouble", value); - return null != value ? value.doubleValue() : 0; + return null != value ? value : 0; } public float getFloat(int index) throws SQLServerException { @@ -645,7 +648,7 @@ public float getFloat(int index) throws SQLServerException { checkClosed(); Float value = (Float) getValue(index, JDBCType.REAL); loggerExternal.exiting(getClassNameLogging(), "getFloat", value); - return null != value ? value.floatValue() : 0; + return null != value ? value : 0; } public float getFloat(String sCol) throws SQLServerException { @@ -654,7 +657,7 @@ public float getFloat(String sCol) throws SQLServerException { checkClosed(); Float value = (Float) getValue(findColumn(sCol), JDBCType.REAL); loggerExternal.exiting(getClassNameLogging(), "getFloat", value); - return null != value ? value.floatValue() : 0; + return null != value ? value : 0; } public long getLong(int index) throws SQLServerException { @@ -663,7 +666,7 @@ public long getLong(int index) throws SQLServerException { checkClosed(); Long value = (Long) getValue(index, JDBCType.BIGINT); loggerExternal.exiting(getClassNameLogging(), "getLong", value); - return null != value ? value.longValue() : 0; + return null != value ? value : 0; } public long getLong(String sCol) throws SQLServerException { @@ -671,7 +674,7 @@ public long getLong(String sCol) throws SQLServerException { checkClosed(); Long value = (Long) getValue(findColumn(sCol), JDBCType.BIGINT); loggerExternal.exiting(getClassNameLogging(), "getLong", value); - return null != value ? value.longValue() : 0; + return null != value ? value : 0; } public Object getObject(int index) throws SQLServerException { @@ -686,10 +689,84 @@ public Object getObject(int index) throws SQLServerException { public T getObject(int index, Class type) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC41(); - - // The driver currently does not implement the optional JDBC APIs - throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); + loggerExternal.entering(getClassNameLogging(), "getObject", index); + checkClosed(); + Object returnValue; + if (type == String.class) { + returnValue = getString(index); + } + else if (type == Byte.class) { + byte byteValue = getByte(index); + returnValue = wasNull() ? null : byteValue; + } + else if (type == Short.class) { + short shortValue = getShort(index); + returnValue = wasNull() ? null : shortValue; + } + else if (type == Integer.class) { + int intValue = getInt(index); + returnValue = wasNull() ? null : intValue; + } + else if (type == Long.class) { + long longValue = getLong(index); + returnValue = wasNull() ? null : longValue; + } + else if (type == BigDecimal.class) { + returnValue = getBigDecimal(index); + } + else if (type == Boolean.class) { + boolean booleanValue = getBoolean(index); + returnValue = wasNull() ? null : booleanValue; + } + else if (type == java.sql.Date.class) { + returnValue = getDate(index); + } + else if (type == java.sql.Time.class) { + returnValue = getTime(index); + } + else if (type == java.sql.Timestamp.class) { + returnValue = getTimestamp(index); + } + else if (type == microsoft.sql.DateTimeOffset.class) { + returnValue = getDateTimeOffset(index); + } + else if (type == UUID.class) { + // read binary, avoid string allocation and parsing + byte[] guid = getBytes(index); + returnValue = guid != null ? Util.readGUIDtoUUID(guid) : null; + } + else if (type == SQLXML.class) { + returnValue = getSQLXML(index); + } + else if (type == Blob.class) { + returnValue = getBlob(index); + } + else if (type == Clob.class) { + returnValue = getClob(index); + } + else if (type == NClob.class) { + returnValue = getNClob(index); + } + else if (type == byte[].class) { + returnValue = getBytes(index); + } + else if (type == Float.class) { + float floatValue = getFloat(index); + returnValue = wasNull() ? null : floatValue; + } + else if (type == Double.class) { + double doubleValue = getDouble(index); + returnValue = wasNull() ? null : doubleValue; + } + else { + // if the type is not supported the specification says the should + // a SQLException instead of SQLFeatureNotSupportedException + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unsupportedConversionTo")); + Object[] msgArgs = {type}; + throw new SQLServerException(form.format(msgArgs), SQLState.DATA_EXCEPTION_NOT_SPECIFIC, DriverError.NOT_SET, null); + } + loggerExternal.exiting(getClassNameLogging(), "getObject", index); + return type.cast(returnValue); } public Object getObject(String sCol) throws SQLServerException { @@ -704,10 +781,12 @@ public Object getObject(String sCol) throws SQLServerException { public T getObject(String sCol, Class type) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC41(); - - // The driver currently does not implement the optional JDBC APIs - throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); + loggerExternal.entering(getClassNameLogging(), "getObject", sCol); + checkClosed(); + int parameterIndex = findColumn(sCol); + T value = getObject(parameterIndex, type); + loggerExternal.exiting(getClassNameLogging(), "getObject", value); + return value; } public short getShort(int index) throws SQLServerException { @@ -716,7 +795,7 @@ public short getShort(int index) throws SQLServerException { checkClosed(); Short value = (Short) getValue(index, JDBCType.SMALLINT); loggerExternal.exiting(getClassNameLogging(), "getShort", value); - return null != value ? value.shortValue() : 0; + return null != value ? value : 0; } public short getShort(String sCol) throws SQLServerException { @@ -724,7 +803,7 @@ public short getShort(String sCol) throws SQLServerException { checkClosed(); Short value = (Short) getValue(findColumn(sCol), JDBCType.SMALLINT); loggerExternal.exiting(getClassNameLogging(), "getShort", value); - return null != value ? value.shortValue() : 0; + return null != value ? value : 0; } public Time getTime(int index) throws SQLServerException { @@ -1212,8 +1291,6 @@ public final java.io.Reader getCharacterStream(int paramIndex) throws SQLServerE } public final java.io.Reader getCharacterStream(String parameterName) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - loggerExternal.entering(getClassNameLogging(), "getCharacterStream", parameterName); checkClosed(); Reader reader = (Reader) getStream(findColumn(parameterName), StreamType.CHARACTER); @@ -1222,7 +1299,6 @@ public final java.io.Reader getCharacterStream(String parameterName) throws SQLE } public final java.io.Reader getNCharacterStream(int parameterIndex) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "getNCharacterStream", parameterIndex); checkClosed(); Reader reader = (Reader) getStream(parameterIndex, StreamType.NCHARACTER); @@ -1231,8 +1307,6 @@ public final java.io.Reader getNCharacterStream(int parameterIndex) throws SQLEx } public final java.io.Reader getNCharacterStream(String parameterName) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - loggerExternal.entering(getClassNameLogging(), "getNCharacterStream", parameterName); checkClosed(); Reader reader = (Reader) getStream(findColumn(parameterName), StreamType.NCHARACTER); @@ -1271,7 +1345,6 @@ public Clob getClob(String sCol) throws SQLServerException { } public NClob getNClob(int parameterIndex) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "getNClob", parameterIndex); checkClosed(); NClob nClob = (NClob) getValue(parameterIndex, JDBCType.NCLOB); @@ -1280,7 +1353,6 @@ public NClob getNClob(int parameterIndex) throws SQLException { } public NClob getNClob(String parameterName) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "getNClob", parameterName); checkClosed(); NClob nClob = (NClob) getValue(findColumn(parameterName), JDBCType.NCLOB); @@ -1332,91 +1404,29 @@ public NClob getNClob(String parameterName) throws SQLException { * @return the index */ /* L3 */ private int findColumn(String columnName) throws SQLServerException { - - final class ThreePartNamesParser { - - private String procedurePart = null; - private String ownerPart = null; - private String databasePart = null; - - String getProcedurePart() { - return procedurePart; - } - - String getOwnerPart() { - return ownerPart; - } - - String getDatabasePart() { - return databasePart; - } - - /* - * Three part names parsing For metdata calls we parse the procedure name into parts so we can use it in sp_sproc_columns sp_sproc_columns - * [[@procedure_name =] 'name'] [,[@procedure_owner =] 'owner'] [,[@procedure_qualifier =] 'qualifier'] - * - */ - private final Pattern threePartName = Pattern.compile(JDBCSyntaxTranslator.getSQLIdentifierWithGroups()); - - final void parseProcedureNameIntoParts(String theProcName) { - Matcher matcher; - if (null != theProcName) { - matcher = threePartName.matcher(theProcName); - if (matcher.matches()) { - if (matcher.group(2) != null) { - databasePart = matcher.group(1); - - // if we have two parts look to see if the last part can be broken even more - matcher = threePartName.matcher(matcher.group(2)); - if (matcher.matches()) { - if (null != matcher.group(2)) { - ownerPart = matcher.group(1); - procedurePart = matcher.group(2); - } - else { - ownerPart = databasePart; - databasePart = null; - procedurePart = matcher.group(1); - } - } - - } - else - procedurePart = matcher.group(1); - - } - else { - procedurePart = theProcName; - } - } - - } - - } - if (paramNames == null) { + SQLServerStatement s = null; try { // Note we are concatenating the information from the passed in sql, not any arguments provided by the user // if the user can execute the sql, any fragments of it is potentially executed via the meta data call through injection // is not a security issue. - SQLServerStatement s = (SQLServerStatement) connection.createStatement(); - ThreePartNamesParser translator = new ThreePartNamesParser(); - translator.parseProcedureNameIntoParts(procedureName); + s = (SQLServerStatement) connection.createStatement(); + ThreePartName threePartName = ThreePartName.parse(procedureName); StringBuilder metaQuery = new StringBuilder("exec sp_sproc_columns "); - if (null != translator.getDatabasePart()) { + if (null != threePartName.getDatabasePart()) { metaQuery.append("@procedure_qualifier="); - metaQuery.append(translator.getDatabasePart()); + metaQuery.append(threePartName.getDatabasePart()); metaQuery.append(", "); } - if (null != translator.getOwnerPart()) { + if (null != threePartName.getOwnerPart()) { metaQuery.append("@procedure_owner="); - metaQuery.append(translator.getOwnerPart()); + metaQuery.append(threePartName.getOwnerPart()); metaQuery.append(", "); } - if (null != translator.getProcedurePart()) { + if (null != threePartName.getProcedurePart()) { // we should always have a procedure name part metaQuery.append("@procedure_name="); - metaQuery.append(translator.getProcedurePart()); + metaQuery.append(threePartName.getProcedurePart()); metaQuery.append(" , @ODBCVer=3"); } else { @@ -1428,7 +1438,7 @@ final void parseProcedureNameIntoParts(String theProcName) { } ResultSet rs = s.executeQueryInternal(metaQuery.toString()); - paramNames = new ArrayList(); + paramNames = new ArrayList<>(); while (rs.next()) { String sCol = rs.getString(4); paramNames.add(sCol.trim()); @@ -1437,12 +1447,25 @@ final void parseProcedureNameIntoParts(String theProcName) { catch (SQLException e) { SQLServerException.makeFromDriverError(connection, this, e.toString(), null, false); } + finally { + if (null != s) + s.close(); + } } int l = 0; if (paramNames != null) l = paramNames.size(); + // handle `@name` as well as `name`, since `@name` is what's returned + // by DatabaseMetaData#getProcedureColumns + String columnNameWithoutAtSign = null; + if (columnName.startsWith("@")) { + columnNameWithoutAtSign = columnName.substring(1, columnName.length()); + } else { + columnNameWithoutAtSign = columnName; + } + // In order to be as accurate as possible when locating parameter name // indexes, as well as be deterministic when running on various client // locales, we search for parameter names using the following scheme: @@ -1450,14 +1473,14 @@ final void parseProcedureNameIntoParts(String theProcName) { // 1. Search using case-sensitive non-locale specific (binary) compare first. // 2. Search using case-insensitive, non-locale specific (binary) compare last. - int i = 0; + int i; int matchPos = -1; // Search using case-sensitive, non-locale specific (binary) compare. // If the user supplies a true match for the parameter name, we will find it here. for (i = 0; i < l; i++) { String sParam = paramNames.get(i); sParam = sParam.substring(1, sParam.length()); - if (sParam.equals(columnName)) { + if (sParam.equals(columnNameWithoutAtSign)) { matchPos = i; break; } @@ -1469,7 +1492,7 @@ final void parseProcedureNameIntoParts(String theProcName) { for (i = 0; i < l; i++) { String sParam = paramNames.get(i); sParam = sParam.substring(1, sParam.length()); - if (sParam.equalsIgnoreCase(columnName)) { + if (sParam.equalsIgnoreCase(columnNameWithoutAtSign)) { matchPos = i; break; } @@ -1615,7 +1638,6 @@ public void setDate(String sCol, public final void setCharacterStream(String parameterName, Reader reader) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setCharacterStream", new Object[] {parameterName, reader}); checkClosed(); @@ -1637,7 +1659,6 @@ public final void setCharacterStream(String parameterName, Reader reader, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setCharacterStream", new Object[] {parameterName, reader, length}); checkClosed(); @@ -1647,7 +1668,6 @@ public final void setCharacterStream(String parameterName, public final void setNCharacterStream(String parameterName, Reader value) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setNCharacterStream", new Object[] {parameterName, value}); checkClosed(); @@ -1658,7 +1678,6 @@ public final void setNCharacterStream(String parameterName, public final void setNCharacterStream(String parameterName, Reader value, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setNCharacterStream", new Object[] {parameterName, value, length}); checkClosed(); @@ -1668,7 +1687,6 @@ public final void setNCharacterStream(String parameterName, public final void setClob(String parameterName, Clob x) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setClob", new Object[] {parameterName, x}); checkClosed(); @@ -1678,7 +1696,6 @@ public final void setClob(String parameterName, public final void setClob(String parameterName, Reader reader) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setClob", new Object[] {parameterName, reader}); checkClosed(); @@ -1689,7 +1706,6 @@ public final void setClob(String parameterName, public final void setClob(String parameterName, Reader value, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setClob", new Object[] {parameterName, value, length}); checkClosed(); @@ -1699,7 +1715,6 @@ public final void setClob(String parameterName, public final void setNClob(String parameterName, NClob value) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setNClob", new Object[] {parameterName, value}); checkClosed(); @@ -1709,7 +1724,6 @@ public final void setNClob(String parameterName, public final void setNClob(String parameterName, Reader reader) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setNClob", new Object[] {parameterName, reader}); checkClosed(); @@ -1720,7 +1734,6 @@ public final void setNClob(String parameterName, public final void setNClob(String parameterName, Reader reader, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setNClob", new Object[] {parameterName, reader, length}); checkClosed(); @@ -1730,7 +1743,6 @@ public final void setNClob(String parameterName, public final void setNString(String parameterName, String value) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setNString", new Object[] {parameterName, value}); checkClosed(); @@ -1758,7 +1770,6 @@ public final void setNString(String parameterName, public final void setNString(String parameterName, String value, boolean forceEncrypt) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setNString", new Object[] {parameterName, value, forceEncrypt}); checkClosed(); @@ -1850,7 +1861,7 @@ public void setObject(String sCol, // For all other types, this value will be ignored. setObject(setterGetParam(findColumn(sCol)), o, JavaType.of(o), JDBCType.of(n), - (java.sql.Types.NUMERIC == n || java.sql.Types.DECIMAL == n) ? Integer.valueOf(m) : null, null, forceEncrypt, findColumn(sCol), null); + (java.sql.Types.NUMERIC == n || java.sql.Types.DECIMAL == n) ? m : null, null, forceEncrypt, findColumn(sCol), null); loggerExternal.exiting(getClassNameLogging(), "setObject"); } @@ -1900,7 +1911,7 @@ public final void setObject(String sCol, setObject(setterGetParam(findColumn(sCol)), x, JavaType.of(x), JDBCType.of(targetSqlType), (java.sql.Types.NUMERIC == targetSqlType || java.sql.Types.DECIMAL == targetSqlType - || InputStream.class.isInstance(x) || Reader.class.isInstance(x)) ? Integer.valueOf(scale) : null, + || InputStream.class.isInstance(x) || Reader.class.isInstance(x)) ? scale : null, precision, false, findColumn(sCol), null); loggerExternal.exiting(getClassNameLogging(), "setObject"); @@ -1910,7 +1921,6 @@ public final void setAsciiStream(String parameterName, InputStream x) throws SQLException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setAsciiStream", new Object[] {parameterName, x}); - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); setStream(findColumn(parameterName), StreamType.ASCII, x, JavaType.INPUTSTREAM, DataTypes.UNKNOWN_STREAM_LENGTH); loggerExternal.exiting(getClassNameLogging(), "setAsciiStream"); @@ -1931,7 +1941,6 @@ public final void setAsciiStream(String parameterName, long length) throws SQLException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setAsciiStream", new Object[] {parameterName, x, length}); - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); setStream(findColumn(parameterName), StreamType.ASCII, x, JavaType.INPUTSTREAM, length); loggerExternal.exiting(getClassNameLogging(), "setAsciiStream"); @@ -1940,7 +1949,6 @@ public final void setAsciiStream(String parameterName, public final void setBinaryStream(String parameterName, InputStream x) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setBinaryStream", new Object[] {parameterName, x}); checkClosed(); @@ -1961,7 +1969,6 @@ public final void setBinaryStream(String parameterName, public final void setBinaryStream(String parameterName, InputStream x, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setBinaryStream", new Object[] {parameterName, x, length}); checkClosed(); @@ -1971,7 +1978,6 @@ public final void setBinaryStream(String parameterName, public final void setBlob(String parameterName, Blob inputStream) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setBlob", new Object[] {parameterName, inputStream}); checkClosed(); @@ -1981,7 +1987,6 @@ public final void setBlob(String parameterName, public final void setBlob(String parameterName, InputStream value) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setBlob", new Object[] {parameterName, value}); @@ -1993,7 +1998,6 @@ public final void setBlob(String parameterName, public final void setBlob(String parameterName, InputStream inputStream, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setBlob", new Object[] {parameterName, inputStream, length}); checkClosed(); @@ -2378,7 +2382,7 @@ public void setByte(String sCol, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setByte", new Object[] {sCol, b}); checkClosed(); - setValue(findColumn(sCol), JDBCType.TINYINT, Byte.valueOf(b), JavaType.BYTE, false); + setValue(findColumn(sCol), JDBCType.TINYINT, b, JavaType.BYTE, false); loggerExternal.exiting(getClassNameLogging(), "setByte"); } @@ -2404,7 +2408,7 @@ public void setByte(String sCol, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setByte", new Object[] {sCol, b, forceEncrypt}); checkClosed(); - setValue(findColumn(sCol), JDBCType.TINYINT, Byte.valueOf(b), JavaType.BYTE, forceEncrypt); + setValue(findColumn(sCol), JDBCType.TINYINT, b, JavaType.BYTE, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "setByte"); } @@ -2611,7 +2615,7 @@ public void setDouble(String sCol, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setDouble", new Object[] {sCol, d}); checkClosed(); - setValue(findColumn(sCol), JDBCType.DOUBLE, Double.valueOf(d), JavaType.DOUBLE, false); + setValue(findColumn(sCol), JDBCType.DOUBLE, d, JavaType.DOUBLE, false); loggerExternal.exiting(getClassNameLogging(), "setDouble"); } @@ -2637,7 +2641,7 @@ public void setDouble(String sCol, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setDouble", new Object[] {sCol, d, forceEncrypt}); checkClosed(); - setValue(findColumn(sCol), JDBCType.DOUBLE, Double.valueOf(d), JavaType.DOUBLE, forceEncrypt); + setValue(findColumn(sCol), JDBCType.DOUBLE, d, JavaType.DOUBLE, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "setDouble"); } @@ -2646,7 +2650,7 @@ public void setFloat(String sCol, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setFloat", new Object[] {sCol, f}); checkClosed(); - setValue(findColumn(sCol), JDBCType.REAL, Float.valueOf(f), JavaType.FLOAT, false); + setValue(findColumn(sCol), JDBCType.REAL, f, JavaType.FLOAT, false); loggerExternal.exiting(getClassNameLogging(), "setFloat"); } @@ -2672,7 +2676,7 @@ public void setFloat(String sCol, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setFloat", new Object[] {sCol, f, forceEncrypt}); checkClosed(); - setValue(findColumn(sCol), JDBCType.REAL, Float.valueOf(f), JavaType.FLOAT, forceEncrypt); + setValue(findColumn(sCol), JDBCType.REAL, f, JavaType.FLOAT, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "setFloat"); } @@ -2681,7 +2685,7 @@ public void setInt(String sCol, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setInt", new Object[] {sCol, i}); checkClosed(); - setValue(findColumn(sCol), JDBCType.INTEGER, Integer.valueOf(i), JavaType.INTEGER, false); + setValue(findColumn(sCol), JDBCType.INTEGER, i, JavaType.INTEGER, false); loggerExternal.exiting(getClassNameLogging(), "setInt"); } @@ -2707,7 +2711,7 @@ public void setInt(String sCol, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setInt", new Object[] {sCol, i, forceEncrypt}); checkClosed(); - setValue(findColumn(sCol), JDBCType.INTEGER, Integer.valueOf(i), JavaType.INTEGER, forceEncrypt); + setValue(findColumn(sCol), JDBCType.INTEGER, i, JavaType.INTEGER, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "setInt"); } @@ -2716,7 +2720,7 @@ public void setLong(String sCol, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setLong", new Object[] {sCol, l}); checkClosed(); - setValue(findColumn(sCol), JDBCType.BIGINT, Long.valueOf(l), JavaType.LONG, false); + setValue(findColumn(sCol), JDBCType.BIGINT, l, JavaType.LONG, false); loggerExternal.exiting(getClassNameLogging(), "setLong"); } @@ -2742,7 +2746,7 @@ public void setLong(String sCol, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setLong", new Object[] {sCol, l, forceEncrypt}); checkClosed(); - setValue(findColumn(sCol), JDBCType.BIGINT, Long.valueOf(l), JavaType.LONG, forceEncrypt); + setValue(findColumn(sCol), JDBCType.BIGINT, l, JavaType.LONG, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "setLong"); } @@ -2751,7 +2755,7 @@ public void setShort(String sCol, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setShort", new Object[] {sCol, s}); checkClosed(); - setValue(findColumn(sCol), JDBCType.SMALLINT, Short.valueOf(s), JavaType.SHORT, false); + setValue(findColumn(sCol), JDBCType.SMALLINT, s, JavaType.SHORT, false); loggerExternal.exiting(getClassNameLogging(), "setShort"); } @@ -2777,7 +2781,7 @@ public void setShort(String sCol, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setShort", new Object[] {sCol, s, forceEncrypt}); checkClosed(); - setValue(findColumn(sCol), JDBCType.SMALLINT, Short.valueOf(s), JavaType.SHORT, forceEncrypt); + setValue(findColumn(sCol), JDBCType.SMALLINT, s, JavaType.SHORT, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "setShort"); } @@ -2786,7 +2790,7 @@ public void setBoolean(String sCol, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setBoolean", new Object[] {sCol, b}); checkClosed(); - setValue(findColumn(sCol), JDBCType.BIT, Boolean.valueOf(b), JavaType.BOOLEAN, false); + setValue(findColumn(sCol), JDBCType.BIT, b, JavaType.BOOLEAN, false); loggerExternal.exiting(getClassNameLogging(), "setBoolean"); } @@ -2812,7 +2816,7 @@ public void setBoolean(String sCol, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setBoolean", new Object[] {sCol, b, forceEncrypt}); checkClosed(); - setValue(findColumn(sCol), JDBCType.BIT, Boolean.valueOf(b), JavaType.BOOLEAN, forceEncrypt); + setValue(findColumn(sCol), JDBCType.BIT, b, JavaType.BOOLEAN, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "setBoolean"); } @@ -2925,7 +2929,6 @@ public URL getURL(String s) throws SQLServerException { public final void setSQLXML(String parameterName, SQLXML xmlObject) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setSQLXML", new Object[] {parameterName, xmlObject}); checkClosed(); @@ -2934,7 +2937,6 @@ public final void setSQLXML(String parameterName, } public final SQLXML getSQLXML(int parameterIndex) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "getSQLXML", parameterIndex); checkClosed(); SQLServerSQLXML value = (SQLServerSQLXML) getSQLXMLInternal(parameterIndex); @@ -2943,7 +2945,6 @@ public final SQLXML getSQLXML(int parameterIndex) throws SQLException { } public final SQLXML getSQLXML(String parameterName) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "getSQLXML", parameterName); checkClosed(); SQLServerSQLXML value = (SQLServerSQLXML) getSQLXMLInternal(findColumn(parameterName)); @@ -2953,21 +2954,18 @@ public final SQLXML getSQLXML(String parameterName) throws SQLException { public final void setRowId(String parameterName, RowId x) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); // Not implemented throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); } public final RowId getRowId(int parameterIndex) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); // Not implemented throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); } public final RowId getRowId(String parameterName) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); // Not implemented throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); @@ -2977,7 +2975,7 @@ public void registerOutParameter(String s, int n, String s1) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {s, new Integer(n), s1}); + loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {s, n, s1}); checkClosed(); registerOutParameter(findColumn(s), n, s1); loggerExternal.exiting(getClassNameLogging(), "registerOutParameter"); @@ -2988,7 +2986,7 @@ public void registerOutParameter(String parameterName, int scale) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "registerOutParameter", - new Object[] {parameterName, new Integer(sqlType), new Integer(scale)}); + new Object[] {parameterName, sqlType, scale}); checkClosed(); registerOutParameter(findColumn(parameterName), sqlType, scale); loggerExternal.exiting(getClassNameLogging(), "registerOutParameter"); @@ -3000,7 +2998,7 @@ public void registerOutParameter(String parameterName, int scale) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "registerOutParameter", - new Object[] {parameterName, new Integer(sqlType), new Integer(scale)}); + new Object[] {parameterName, sqlType, scale}); checkClosed(); registerOutParameter(findColumn(parameterName), sqlType, precision, scale); loggerExternal.exiting(getClassNameLogging(), "registerOutParameter"); @@ -3009,7 +3007,7 @@ public void registerOutParameter(String parameterName, public void registerOutParameter(String s, int n) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {s, new Integer(n)}); + loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {s, n}); checkClosed(); registerOutParameter(findColumn(s), n); loggerExternal.exiting(getClassNameLogging(), "registerOutParameter"); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement42.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement42.java index ee73cc176..cf5e1f246 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement42.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement42.java @@ -34,10 +34,10 @@ public void registerOutParameter(int index, DriverJDBCVersion.checkSupportsJDBC42(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {new Integer(index), sqlType}); + loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {index, sqlType}); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - registerOutParameter(index, sqlType.getVendorTypeNumber().intValue()); + registerOutParameter(index, sqlType.getVendorTypeNumber()); loggerExternal.exiting(getClassNameLogging(), "registerOutParameter"); } @@ -47,10 +47,10 @@ public void registerOutParameter(int index, DriverJDBCVersion.checkSupportsJDBC42(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {new Integer(index), sqlType, typeName}); + loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {index, sqlType, typeName}); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - registerOutParameter(index, sqlType.getVendorTypeNumber().intValue(), typeName); + registerOutParameter(index, sqlType.getVendorTypeNumber(), typeName); loggerExternal.exiting(getClassNameLogging(), "registerOutParameter"); } @@ -61,10 +61,10 @@ public void registerOutParameter(int index, DriverJDBCVersion.checkSupportsJDBC42(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {new Integer(index), sqlType, new Integer(scale)}); + loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {index, sqlType, scale}); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - registerOutParameter(index, sqlType.getVendorTypeNumber().intValue(), scale); + registerOutParameter(index, sqlType.getVendorTypeNumber(), scale); loggerExternal.exiting(getClassNameLogging(), "registerOutParameter"); } @@ -76,10 +76,10 @@ public void registerOutParameter(int index, DriverJDBCVersion.checkSupportsJDBC42(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {new Integer(index), sqlType, new Integer(scale)}); + loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {index, sqlType, scale}); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - registerOutParameter(index, sqlType.getVendorTypeNumber().intValue(), precision, scale); + registerOutParameter(index, sqlType.getVendorTypeNumber(), precision, scale); loggerExternal.exiting(getClassNameLogging(), "registerOutParameter"); } @@ -93,7 +93,7 @@ public void setObject(String sCol, loggerExternal.entering(getClassNameLogging(), "setObject", new Object[] {sCol, obj, jdbcType}); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - setObject(sCol, obj, jdbcType.getVendorTypeNumber().intValue()); + setObject(sCol, obj, jdbcType.getVendorTypeNumber()); loggerExternal.exiting(getClassNameLogging(), "setObject"); } @@ -108,7 +108,7 @@ public void setObject(String sCol, loggerExternal.entering(getClassNameLogging(), "setObject", new Object[] {sCol, obj, jdbcType, scale}); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - setObject(sCol, obj, jdbcType.getVendorTypeNumber().intValue(), scale); + setObject(sCol, obj, jdbcType.getVendorTypeNumber(), scale); loggerExternal.exiting(getClassNameLogging(), "setObject"); } @@ -124,7 +124,7 @@ public void setObject(String sCol, loggerExternal.entering(getClassNameLogging(), "setObject", new Object[] {sCol, obj, jdbcType, scale, forceEncrypt}); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - setObject(sCol, obj, jdbcType.getVendorTypeNumber().intValue(), scale, forceEncrypt); + setObject(sCol, obj, jdbcType.getVendorTypeNumber(), scale, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "setObject"); } @@ -138,7 +138,7 @@ public void registerOutParameter(String parameterName, loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {parameterName, sqlType, typeName}); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - registerOutParameter(parameterName, sqlType.getVendorTypeNumber().intValue(), typeName); + registerOutParameter(parameterName, sqlType.getVendorTypeNumber(), typeName); loggerExternal.exiting(getClassNameLogging(), "registerOutParameter"); } @@ -149,10 +149,10 @@ public void registerOutParameter(String parameterName, DriverJDBCVersion.checkSupportsJDBC42(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {parameterName, sqlType, new Integer(scale)}); + loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {parameterName, sqlType, scale}); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - registerOutParameter(parameterName, sqlType.getVendorTypeNumber().intValue(), scale); + registerOutParameter(parameterName, sqlType.getVendorTypeNumber(), scale); loggerExternal.exiting(getClassNameLogging(), "registerOutParameter"); } @@ -164,10 +164,10 @@ public void registerOutParameter(String parameterName, DriverJDBCVersion.checkSupportsJDBC42(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {parameterName, sqlType, new Integer(scale)}); + loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {parameterName, sqlType, scale}); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - registerOutParameter(parameterName, sqlType.getVendorTypeNumber().intValue(), precision, scale); + registerOutParameter(parameterName, sqlType.getVendorTypeNumber(), precision, scale); loggerExternal.exiting(getClassNameLogging(), "registerOutParameter"); } @@ -180,7 +180,7 @@ public void registerOutParameter(String parameterName, loggerExternal.entering(getClassNameLogging(), "registerOutParameter", new Object[] {parameterName, sqlType}); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - registerOutParameter(parameterName, sqlType.getVendorTypeNumber().intValue()); + registerOutParameter(parameterName, sqlType.getVendorTypeNumber()); loggerExternal.exiting(getClassNameLogging(), "registerOutParameter"); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerClob.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerClob.java index f26c2bb79..a9556baf3 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerClob.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerClob.java @@ -14,6 +14,7 @@ import java.io.Closeable; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.Reader; import java.io.Serializable; import java.io.StringReader; @@ -43,24 +44,25 @@ public class SQLServerClob extends SQLServerClobBase implements Clob { * @param connection * the database connection this blob is implemented on * @param data - * the BLOB's data + * the CLOB's data + * @deprecated Use {@link SQLServerConnection#createClob()} instead. */ @Deprecated public SQLServerClob(SQLServerConnection connection, String data) { - super(connection, data, (null == connection) ? null : connection.getDatabaseCollation(), logger); + super(connection, data, (null == connection) ? null : connection.getDatabaseCollation(), logger, null); if (null == data) throw new NullPointerException(SQLServerException.getErrString("R_cantSetNull")); } SQLServerClob(SQLServerConnection connection) { - super(connection, "", connection.getDatabaseCollation(), logger); + super(connection, "", connection.getDatabaseCollation(), logger, null); } SQLServerClob(BaseInputStream stream, TypeInfo typeInfo) throws SQLServerException, UnsupportedEncodingException { - super(null, new String(stream.getBytes(), typeInfo.getCharset()), typeInfo.getSQLCollation(), logger); + super(null, stream, typeInfo.getSQLCollation(), logger , typeInfo); } final JDBCType getJdbcType() { @@ -78,13 +80,15 @@ abstract class SQLServerClobBase implements Serializable { private final SQLCollation sqlCollation; private boolean isClosed = false; + + private final TypeInfo typeInfo; // Active streams which must be closed when the Clob/NClob is closed // // Initial size of the array is based on an assumption that a Clob/NClob object is // typically used either for input or output, and then only once. The array size // grows automatically if multiple streams are used. - private ArrayList activeStreams = new ArrayList(1); + private ArrayList activeStreams = new ArrayList<>(1); transient SQLServerConnection con; private static Logger logger; @@ -94,7 +98,7 @@ final public String toString() { return traceID; } - static private final AtomicInteger baseID = new AtomicInteger(0); // Unique id generator for each instance (used for logging). + static private final AtomicInteger baseID = new AtomicInteger(0); // Unique id generator for each instance (used for logging). // Returns unique id for each instance. private static int nextInstanceID() { @@ -118,14 +122,24 @@ private String getDisplayClassName() { * @param collation * the data collation * @param logger + * logger information + * @param typeInfo + * the column TYPE_INFO */ SQLServerClobBase(SQLServerConnection connection, - String data, + Object data, SQLCollation collation, - Logger logger) { + Logger logger, + TypeInfo typeInfo) { this.con = connection; - this.value = data; + if (data instanceof BaseInputStream) { + activeStreams.add((Closeable) data); + } + else { + this.value = (String) data; + } this.sqlCollation = collation; + this.typeInfo = typeInfo; SQLServerClobBase.logger = logger; if (logger.isLoggable(Level.FINE)) { @@ -144,8 +158,6 @@ private String getDisplayClassName() { * when an error occurs */ public void free() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - if (!isClosed) { // Close active streams, ignoring any errors, since nothing can be done with them after that point anyway. if (null != activeStreams) { @@ -191,9 +203,23 @@ public InputStream getAsciiStream() throws SQLException { DataTypes.throwConversionError(getDisplayClassName(), "AsciiStream"); // Need to use a BufferedInputStream since the stream returned by this method is assumed to support mark/reset - InputStream getterStream = new BufferedInputStream(new ReaderInputStream(new StringReader(value), US_ASCII, value.length())); + InputStream getterStream; + if (null == value && !activeStreams.isEmpty()) { + InputStream inputStream = (InputStream) activeStreams.get(0); + try { + inputStream.reset(); + getterStream = new BufferedInputStream( + new ReaderInputStream(new InputStreamReader(inputStream), US_ASCII, inputStream.available())); + } + catch (IOException e) { + throw new SQLServerException(e.getMessage(), null, 0, e); + } + } + else { + getStringFromStream(); + getterStream = new BufferedInputStream(new ReaderInputStream(new StringReader(value), US_ASCII, value.length())); - activeStreams.add(getterStream); + } return getterStream; } @@ -207,6 +233,7 @@ public InputStream getAsciiStream() throws SQLException { public Reader getCharacterStream() throws SQLException { checkClosed(); + getStringFromStream(); Reader getterStream = new StringReader(value); activeStreams.add(getterStream); return getterStream; @@ -225,8 +252,6 @@ public Reader getCharacterStream() throws SQLException { */ public Reader getCharacterStream(long pos, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - // Not implemented throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); } @@ -247,15 +272,16 @@ public String getSubString(long pos, int length) throws SQLException { checkClosed(); + getStringFromStream(); if (pos < 1) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPositionIndex")); - Object[] msgArgs = {new Long(pos)}; + Object[] msgArgs = {pos}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } if (length < 0) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLength")); - Object[] msgArgs = {new Integer(length)}; + Object[] msgArgs = {length}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } @@ -285,8 +311,26 @@ public String getSubString(long pos, public long length() throws SQLException { checkClosed(); + getStringFromStream(); return value.length(); } + + /** + * Converts the stream to String + * @throws SQLServerException + */ + private void getStringFromStream() throws SQLServerException { + if (null == value && !activeStreams.isEmpty()) { + BaseInputStream stream = (BaseInputStream) activeStreams.get(0); + try { + stream.reset(); + } + catch (IOException e) { + throw new SQLServerException(e.getMessage(), null, 0, e); + } + value = new String((stream).getBytes(), typeInfo.getCharset()); + } + } /** * Retrieves the character position at which the specified Clob object searchstr appears in this Clob object. The search begins at position start. @@ -303,9 +347,10 @@ public long position(Clob searchstr, long start) throws SQLException { checkClosed(); + getStringFromStream(); if (start < 1) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPositionIndex")); - Object[] msgArgs = {new Long(start)}; + Object[] msgArgs = {start}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } @@ -331,9 +376,10 @@ public long position(String searchstr, long start) throws SQLException { checkClosed(); + getStringFromStream(); if (start < 1) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPositionIndex")); - Object[] msgArgs = {new Long(start)}; + Object[] msgArgs = {start}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } @@ -344,7 +390,7 @@ public long position(String searchstr, int pos = value.indexOf(searchstr, (int) (start - 1)); if (-1 != pos) - return pos + 1; + return pos + 1L; return -1; } @@ -362,9 +408,10 @@ public long position(String searchstr, public void truncate(long len) throws SQLException { checkClosed(); + getStringFromStream(); if (len < 0) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLength")); - Object[] msgArgs = {new Long(len)}; + Object[] msgArgs = {len}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } @@ -386,7 +433,7 @@ public java.io.OutputStream setAsciiStream(long pos) throws SQLException { if (pos < 1) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPositionIndex")); - Object[] msgArgs = {new Long(pos)}; + Object[] msgArgs = {pos}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } @@ -407,7 +454,7 @@ public java.io.Writer setCharacterStream(long pos) throws SQLException { if (pos < 1) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPositionIndex")); - Object[] msgArgs = {new Long(pos)}; + Object[] msgArgs = {pos}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } @@ -460,20 +507,21 @@ public int setString(long pos, int len) throws SQLException { checkClosed(); + getStringFromStream(); if (null == str) SQLServerException.makeFromDriverError(con, null, SQLServerException.getErrString("R_cantSetNull"), null, true); // Offset must be within incoming string str boundary. if (offset < 0 || offset > str.length()) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidOffset")); - Object[] msgArgs = {new Integer(offset)}; + Object[] msgArgs = {offset}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } // len must be within incoming string str boundary. if (len < 0 || len > str.length() - offset) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLength")); - Object[] msgArgs = {new Integer(len)}; + Object[] msgArgs = {len}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } @@ -482,7 +530,7 @@ public int setString(long pos, // past the end of data to request "append" mode. if (pos < 1 || pos > value.length() + 1) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPositionIndex")); - Object[] msgArgs = {new Long(pos)}; + Object[] msgArgs = {pos}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java index 191729b59..c6e168a85 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java @@ -17,15 +17,13 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.MessageFormat; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; - -import org.apache.http.impl.client.HttpClientBuilder; +import java.util.Locale; import com.microsoft.azure.keyvault.KeyVaultClient; -import com.microsoft.azure.keyvault.KeyVaultClientImpl; import com.microsoft.azure.keyvault.models.KeyBundle; import com.microsoft.azure.keyvault.models.KeyOperationResult; +import com.microsoft.azure.keyvault.models.KeyVerifyResult; +import com.microsoft.azure.keyvault.webkey.JsonWebKeyEncryptionAlgorithm; import com.microsoft.azure.keyvault.webkey.JsonWebKeySignatureAlgorithm; /** @@ -66,26 +64,20 @@ public String getName() { } /** - * Constructor that takes a callback function to authenticate to AAD. This is used by KeyVaultClient at runtime to authenticate to Azure Key + * Constructor that authenticates to AAD. This is used by KeyVaultClient at runtime to authenticate to Azure Key * Vault. * - * @param authenticationCallback - * - Callback function used for authenticating to AAD. - * @param executorService - * - The ExecutorService used to create the keyVaultClient + * @param clientId + * Identifier of the client requesting the token. + * @param clientKey + * Key of the client requesting the token. * @throws SQLServerException * when an error occurs */ - public SQLServerColumnEncryptionAzureKeyVaultProvider(SQLServerKeyVaultAuthenticationCallback authenticationCallback, - ExecutorService executorService) throws SQLServerException { - if (null == authenticationCallback) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_NullValue")); - Object[] msgArgs1 = {"SQLServerKeyVaultAuthenticationCallback"}; - throw new SQLServerException(form.format(msgArgs1), null); - } - credential = new KeyVaultCredential(authenticationCallback); - HttpClientBuilder builder = HttpClientBuilder.create(); - keyVaultClient = new KeyVaultClientImpl(builder, executorService, credential); + public SQLServerColumnEncryptionAzureKeyVaultProvider(String clientId, + String clientKey) throws SQLServerException { + credential = new KeyVaultCredential(clientId, clientKey); + keyVaultClient = new KeyVaultClient(credential); } /** @@ -184,7 +176,7 @@ public byte[] decryptColumnEncryptionKey(String masterKeyPath, md = MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { - throw new SQLServerException(SQLServerException.getErrString("R_NoSHA256Algorithm"), null); + throw new SQLServerException(SQLServerException.getErrString("R_NoSHA256Algorithm"), e); } md.update(hash); byte dataToVerify[] = md.digest(); @@ -209,7 +201,7 @@ public byte[] decryptColumnEncryptionKey(String masterKeyPath, private short convertTwoBytesToShort(byte[] input, int index) throws SQLServerException { - short shortVal = -1; + short shortVal; if (index + 1 >= input.length) { throw new SQLServerException(null, SQLServerException.getErrString("R_ByteToShortConversion"), null, 0, false); } @@ -263,7 +255,7 @@ public byte[] encryptColumnEncryptionKey(String masterKeyPath, byte[] version = new byte[] {firstVersion[0]}; // Get the Unicode encoded bytes of cultureinvariant lower case masterKeyPath - byte[] masterKeyPathBytes = masterKeyPath.toLowerCase().getBytes(UTF_16LE); + byte[] masterKeyPathBytes = masterKeyPath.toLowerCase(Locale.ENGLISH).getBytes(UTF_16LE); byte[] keyPathLength = new byte[2]; keyPathLength[0] = (byte) (((short) masterKeyPathBytes.length) & 0xff); @@ -302,14 +294,13 @@ public byte[] encryptColumnEncryptionKey(String masterKeyPath, md = MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { - throw new SQLServerException(SQLServerException.getErrString("R_NoSHA256Algorithm"), null); + throw new SQLServerException(SQLServerException.getErrString("R_NoSHA256Algorithm"), e); } md.update(dataToHash); byte dataToSign[] = md.digest(); // Sign the hash - byte[] signedHash = null; - signedHash = AzureKeyVaultSignHashedData(dataToSign, masterKeyPath); + byte[] signedHash = AzureKeyVaultSignHashedData(dataToSign, masterKeyPath); if (signedHash.length != keySizeInBytes) { throw new SQLServerException(SQLServerException.getErrString("R_SignedHashLengthError"), null); @@ -367,7 +358,7 @@ private String validateEncryptionAlgorithm(String encryptionAlgorithm) throws SQ } // Transform to standard format (dash instead of underscore) to support both "RSA_OAEP" and "RSA-OAEP" - if (encryptionAlgorithm.equalsIgnoreCase("RSA_OAEP")) { + if ("RSA_OAEP".equalsIgnoreCase(encryptionAlgorithm)) { encryptionAlgorithm = "RSA-OAEP"; } @@ -401,12 +392,12 @@ private void ValidateNonEmptyAKVPath(String masterKeyPath) throws SQLServerExcep catch (URISyntaxException e) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_AKVURLInvalid")); Object[] msgArgs = {masterKeyPath}; - throw new SQLServerException(null, form.format(msgArgs), null, 0, false); + throw new SQLServerException(form.format(msgArgs), null, 0, e); } // A valid URI. // Check if it is pointing to AKV. - if (!parsedUri.getHost().toLowerCase().endsWith(azureKeyVaultDomainName)) { + if (!parsedUri.getHost().toLowerCase(Locale.ENGLISH).endsWith(azureKeyVaultDomainName)) { // Return an error indicating that the AKV url is invalid. MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_AKVMasterKeyPathInvalid")); Object[] msgArgs = {masterKeyPath}; @@ -434,14 +425,10 @@ private byte[] AzureKeyVaultWrap(String masterKeyPath, throw new SQLServerException(SQLServerException.getErrString("R_CEKNull"), null); } - KeyOperationResult wrappedKey = null; - try { - wrappedKey = keyVaultClient.wrapKeyAsync(masterKeyPath, encryptionAlgorithm, columnEncryptionKey).get(); - } - catch (InterruptedException | ExecutionException e) { - throw new SQLServerException(SQLServerException.getErrString("R_EncryptCEKError"), null); - } - return wrappedKey.getResult(); + JsonWebKeyEncryptionAlgorithm jsonEncryptionAlgorithm = new JsonWebKeyEncryptionAlgorithm(encryptionAlgorithm); + KeyOperationResult wrappedKey = keyVaultClient.wrapKey(masterKeyPath, jsonEncryptionAlgorithm, columnEncryptionKey); + + return wrappedKey.result(); } /** @@ -467,14 +454,10 @@ private byte[] AzureKeyVaultUnWrap(String masterKeyPath, throw new SQLServerException(SQLServerException.getErrString("R_EmptyEncryptedCEK"), null); } - KeyOperationResult unwrappedKey; - try { - unwrappedKey = keyVaultClient.unwrapKeyAsync(masterKeyPath, encryptionAlgorithm, encryptedColumnEncryptionKey).get(); - } - catch (InterruptedException | ExecutionException e) { - throw new SQLServerException(SQLServerException.getErrString("R_DecryptCEKError"), null); - } - return unwrappedKey.getResult(); + JsonWebKeyEncryptionAlgorithm jsonEncryptionAlgorithm = new JsonWebKeyEncryptionAlgorithm(encryptionAlgorithm); + KeyOperationResult unwrappedKey = keyVaultClient.unwrapKey(masterKeyPath, jsonEncryptionAlgorithm, encryptedColumnEncryptionKey); + + return unwrappedKey.result(); } /** @@ -491,14 +474,9 @@ private byte[] AzureKeyVaultSignHashedData(byte[] dataToSign, String masterKeyPath) throws SQLServerException { assert ((null != dataToSign) && (0 != dataToSign.length)); - KeyOperationResult signedData = null; - try { - signedData = keyVaultClient.signAsync(masterKeyPath, JsonWebKeySignatureAlgorithm.RS256, dataToSign).get(); - } - catch (InterruptedException | ExecutionException e) { - throw new SQLServerException(SQLServerException.getErrString("R_GenerateSignature"), null); - } - return signedData.getResult(); + KeyOperationResult signedData = keyVaultClient.sign(masterKeyPath, JsonWebKeySignatureAlgorithm.RS256, dataToSign); + + return signedData.result(); } /** @@ -517,15 +495,9 @@ private boolean AzureKeyVaultVerifySignature(byte[] dataToVerify, assert ((null != dataToVerify) && (0 != dataToVerify.length)); assert ((null != signature) && (0 != signature.length)); - boolean valid = false; - try { - valid = keyVaultClient.verifyAsync(masterKeyPath, JsonWebKeySignatureAlgorithm.RS256, dataToVerify, signature).get(); - } - catch (InterruptedException | ExecutionException e) { - throw new SQLServerException(SQLServerException.getErrString("R_VerifySignature"), null); - } + KeyVerifyResult valid = keyVaultClient.verify(masterKeyPath, JsonWebKeySignatureAlgorithm.RS256, dataToVerify, signature); - return valid; + return valid.value(); } /** @@ -538,21 +510,22 @@ private boolean AzureKeyVaultVerifySignature(byte[] dataToVerify, * when an error occurs */ private int getAKVKeySize(String masterKeyPath) throws SQLServerException { + KeyBundle retrievedKey = keyVaultClient.getKey(masterKeyPath); - KeyBundle retrievedKey = null; - try { - retrievedKey = keyVaultClient.getKeyAsync(masterKeyPath).get(); - } - catch (InterruptedException | ExecutionException e) { - throw new SQLServerException(SQLServerException.getErrString("R_GetAKVKeySize"), null); + if (null == retrievedKey) { + String[] keyTokens = masterKeyPath.split("/"); + + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_AKVKeyNotFound")); + Object[] msgArgs = {keyTokens[keyTokens.length - 1]}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, false); } - if (!retrievedKey.getKey().getKty().equalsIgnoreCase("RSA") && !retrievedKey.getKey().getKty().equalsIgnoreCase("RSA-HSM")) { + if (!"RSA".equalsIgnoreCase(retrievedKey.key().kty().toString()) && !"RSA-HSM".equalsIgnoreCase(retrievedKey.key().kty().toString())) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_NonRSAKey")); - Object[] msgArgs = {retrievedKey.getKey().getKty()}; + Object[] msgArgs = {retrievedKey.key().kty().toString()}; throw new SQLServerException(null, form.format(msgArgs), null, 0, false); } - return retrievedKey.getKey().getN().length; + return retrievedKey.key().n().length; } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionCertificateStoreProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionCertificateStoreProvider.java index a2b13a443..91a473b73 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionCertificateStoreProvider.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionCertificateStoreProvider.java @@ -146,7 +146,7 @@ private String getThumbPrint(X509Certificate cert) throws NoSuchAlgorithmExcepti private CertificateDetails getCertificateByThumbprint(String storeLocation, String thumbprint, String masterKeyPath) throws SQLServerException { - FileInputStream fis = null; + FileInputStream fis; if ((null == keyStoreDirectoryPath)) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_AEKeyPathEmptyOrReserved")); @@ -169,7 +169,7 @@ private CertificateDetails getCertificateByThumbprint(String storeLocation, File keyStoreDirectory = keyStoreFullPath.toFile(); File[] listOfFiles = keyStoreDirectory.listFiles(); - if ((null == listOfFiles) || ((null != listOfFiles) && (0 == listOfFiles.length))) { + if ((null == listOfFiles) || (0 == listOfFiles.length)) { throw new SQLServerException(SQLServerException.getErrString("R_KeyStoreNotFound"), null); } @@ -232,7 +232,7 @@ public byte[] decryptColumnEncryptionKey(String masterKeyPath, byte[] encryptedColumnEncryptionKey) throws SQLServerException { windowsCertificateStoreLogger.entering(SQLServerColumnEncryptionCertificateStoreProvider.class.getName(), "decryptColumnEncryptionKey", "Decrypting Column Encryption Key."); - byte[] plainCek = null; + byte[] plainCek; if (isWindows) { plainCek = decryptColumnEncryptionKeyWindows(masterKeyPath, encryptionAlgorithm, encryptedColumnEncryptionKey); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index f3b4c9c13..fe087862a 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -44,6 +44,7 @@ import java.util.Map; import java.util.Properties; import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -51,11 +52,14 @@ import javax.security.auth.Subject; import javax.sql.XAConnection; -import javax.xml.bind.DatatypeConverter; import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSException; +import mssql.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap; +import mssql.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap.Builder; +import mssql.googlecode.concurrentlinkedhashmap.EvictionListener; + /** * SQLServerConnection implements a JDBC connection to SQL Server. SQLServerConnections support JDBC connection pooling and may be either physical * JDBC connections or logical JDBC connections. @@ -89,13 +93,221 @@ public class SQLServerConnection implements ISQLServerConnection { * may not need more info */ - FeatureAckOptFedAuth fedAuth = new FeatureAckOptFedAuth(); + // Threasholds related to when prepared statement handles are cleaned-up. 1 == immediately. + /** + * The default for the prepared statement clean-up action threshold (i.e. when sp_unprepare is called). + */ + static final int DEFAULT_SERVER_PREPARED_STATEMENT_DISCARD_THRESHOLD = 10; // Used to set the initial default, can be changed later. + private int serverPreparedStatementDiscardThreshold = -1; // Current limit for this particular connection. + + /** + * The default for if prepared statements should execute sp_executesql before following the prepare, unprepare pattern. + */ + static final boolean DEFAULT_ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT_CALL = false; // Used to set the initial default, can be changed later. false == use sp_executesql -> sp_prepexec -> sp_execute -> batched -> sp_unprepare pattern, true == skip sp_executesql part of pattern. + private Boolean enablePrepareOnFirstPreparedStatementCall = null; // Current limit for this particular connection. + + // Handle the actual queue of discarded prepared statements. + private ConcurrentLinkedQueue discardedPreparedStatementHandles = new ConcurrentLinkedQueue<>(); + private AtomicInteger discardedPreparedStatementHandleCount = new AtomicInteger(0); + + private boolean fedAuthRequiredByUser = false; + private boolean fedAuthRequiredPreLoginResponse = false; + private boolean federatedAuthenticationAcknowledged = false; + private boolean federatedAuthenticationRequested = false; + private boolean federatedAuthenticationInfoRequested = false; // Keep this distinct from _federatedAuthenticationRequested, since some fedauth + // library types may not need more info + // private FederatedAuthenticationFeatureExtensionData fedAuthFeatureExtensionData = null; + FeatureAckOptFedAuth fedAuth = new FeatureAckOptFedAuth(); + private String authenticationString = null; private byte[] accessTokenInByte = null; private SqlFedAuthToken fedAuthToken = null; + static class Sha1HashKey { + private byte[] bytes; + + Sha1HashKey(String sql, String parametersDefinition) { + this(String.format("%s%s", sql, parametersDefinition)); + } + + Sha1HashKey(String s) { + bytes = getSha1Digest().digest(s.getBytes()); + } + + public boolean equals(Object obj) { + if (!(obj instanceof Sha1HashKey)) + return false; + + return java.util.Arrays.equals(bytes, ((Sha1HashKey)obj).bytes); + } + + public int hashCode() { + return java.util.Arrays.hashCode(bytes); + } + + private java.security.MessageDigest getSha1Digest() { + try { + return java.security.MessageDigest.getInstance("SHA-1"); + } + catch (final java.security.NoSuchAlgorithmException e) { + // This is not theoretically possible, but we're forced to catch it anyway + throw new RuntimeException(e); + } + } + } + + /** + * Used to keep track of an individual prepared statement handle. + */ + class PreparedStatementHandle { + private int handle = 0; + private final AtomicInteger handleRefCount = new AtomicInteger(); + private boolean isDirectSql; + private volatile boolean evictedFromCache; + private volatile boolean explicitlyDiscarded; + private Sha1HashKey key; + + PreparedStatementHandle(Sha1HashKey key, int handle, boolean isDirectSql, boolean isEvictedFromCache) { + this.key = key; + this.handle = handle; + this.isDirectSql = isDirectSql; + this.setIsEvictedFromCache(isEvictedFromCache); + handleRefCount.set(1); + } + + /** Has the statement been evicted from the statement handle cache. */ + private boolean isEvictedFromCache() { + return evictedFromCache; + } + + /** Specify whether the statement been evicted from the statement handle cache. */ + private void setIsEvictedFromCache(boolean isEvictedFromCache) { + this.evictedFromCache = isEvictedFromCache; + } + + /** Specify that this statement has been explicitly discarded from being used by the cache. */ + void setIsExplicitlyDiscarded() { + this.explicitlyDiscarded = true; + + evictCachedPreparedStatementHandle(this); + } + + /** Has the statement been explicitly discarded. */ + private boolean isExplicitlyDiscarded() { + return explicitlyDiscarded; + } + + /** Get the actual handle. */ + int getHandle() { + return handle; + } + + /** Get the cache key. */ + Sha1HashKey getKey() { + return key; + } + + boolean isDirectSql() { + return isDirectSql; + } + + /** Make sure handle cannot be re-used. + * + * @return + * false: Handle could not be discarded, it is in use. + * true: Handle was successfully put on path for discarding. + */ + private boolean tryDiscardHandle() { + return handleRefCount.compareAndSet(0, -999); + } + + /** Returns whether this statement has been discarded and can no longer be re-used. */ + private boolean isDiscarded() { + return 0 > handleRefCount.intValue(); + } + + /** Adds a new reference to this handle, i.e. re-using it. + * + * @return + * false: Reference could not be added, statement has been discarded or does not have a handle associated with it. + * true: Reference was successfully added. + */ + boolean tryAddReference() { + if (isDiscarded() || isExplicitlyDiscarded()) + return false; + else { + int refCount = handleRefCount.incrementAndGet(); + return refCount > 0; + } + } + + /** Remove a reference from this handle*/ + void removeReference() { + handleRefCount.decrementAndGet(); + } + } + + /** Size of the parsed SQL-text metadata cache */ + static final private int PARSED_SQL_CACHE_SIZE = 100; + + /** Cache of parsed SQL meta data */ + static private ConcurrentLinkedHashMap parsedSQLCache; + + static { + parsedSQLCache = new Builder() + .maximumWeightedCapacity(PARSED_SQL_CACHE_SIZE) + .build(); + } + + /** Get prepared statement cache entry if exists, if not parse and create a new one */ + static ParsedSQLCacheItem getCachedParsedSQL(Sha1HashKey key) { + return parsedSQLCache.get(key); + } + + /** Parse and create a information about parsed SQL text */ + static ParsedSQLCacheItem parseAndCacheSQL(Sha1HashKey key, String sql) throws SQLServerException { + JDBCSyntaxTranslator translator = new JDBCSyntaxTranslator(); + + String parsedSql = translator.translate(sql); + String procName = translator.getProcedureName(); // may return null + boolean returnValueSyntax = translator.hasReturnValueSyntax(); + int paramCount = countParams(parsedSql); + + ParsedSQLCacheItem cacheItem = new ParsedSQLCacheItem (parsedSql, paramCount, procName, returnValueSyntax); + parsedSQLCache.putIfAbsent(key, cacheItem); + return cacheItem; + } + + /** Size of the prepared statement handle cache */ + private int statementPoolingCacheSize = 10; + + /** Default size for prepared statement caches */ + static final int DEFAULT_STATEMENT_POOLING_CACHE_SIZE = 10; + /** Cache of prepared statement handles */ + private ConcurrentLinkedHashMap preparedStatementHandleCache; + /** Cache of prepared statement parameter metadata */ + private ConcurrentLinkedHashMap parameterMetadataCache; + + /** + * Find statement parameters. + * + * @param sql + * SQL text to parse for number of parameters to intialize. + */ + private static int countParams(String sql) { + int nParams = 0; + + // Figure out the expected number of parameters by counting the + // parameter placeholders in the SQL string. + int offset = -1; + while ((offset = ParameterUtils.scanSQLForChar('?', sql, ++offset)) < sql.length()) + ++nParams; + + return nParams; + } + SqlFedAuthToken getAuthenticationResult() { return fedAuthToken; } @@ -112,6 +324,7 @@ private enum State { } private final static float TIMEOUTSTEP = 0.08F; // fraction of timeout to use for fast failover connections + private final static float TIMEOUTSTEP_TNIR = 0.125F; final static int TnirFirstAttemptTimeoutMs = 500; // fraction of timeout to use for fast failover connections /* @@ -132,8 +345,9 @@ ServerPortPlaceHolder getRoutingInfo() { } // Permission targets - // currently only callAbort is implemented private static final String callAbortPerm = "callAbort"; + + private static final String SET_NETWORK_TIMEOUT_PERM = "setNetworkTimeout"; private boolean sendStringParametersAsUnicode = SQLServerDriverBooleanProperty.SEND_STRING_PARAMETERS_AS_UNICODE.getDefaultValue(); // see // connection @@ -143,6 +357,8 @@ ServerPortPlaceHolder getRoutingInfo() { // is // false). + private static String hostName = null; + boolean sendStringParametersAsUnicode() { return sendStringParametersAsUnicode; } @@ -203,6 +419,8 @@ final int getQueryTimeoutSeconds() { final int getSocketTimeoutMilliseconds() { return socketTimeoutMilliseconds; } + + boolean userSetTNIR = true; private boolean sendTimeAsDatetime = SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.getDefaultValue(); @@ -239,6 +457,20 @@ final byte getNegotiatedEncryptionLevel() { return negotiatedEncryptionLevel; } + private String trustManagerClass = null; + + final String getTrustManagerClass() { + assert TDS.ENCRYPT_INVALID != requestedEncryptionLevel; + return trustManagerClass; + } + + private String trustManagerConstructorArg = null; + + final String getTrustManagerConstructorArg() { + assert TDS.ENCRYPT_INVALID != requestedEncryptionLevel; + return trustManagerConstructorArg; + } + static final String RESERVED_PROVIDER_NAME_PREFIX = "MSSQL_"; String columnEncryptionSetting = null; @@ -257,7 +489,7 @@ boolean getServerSupportsColumnEncryption() { } static boolean isWindows; - static Map globalSystemColumnEncryptionKeyStoreProviders = new HashMap(); + static Map globalSystemColumnEncryptionKeyStoreProviders = new HashMap<>(); static { if (System.getProperty("os.name").toLowerCase(Locale.ENGLISH).startsWith("windows")) { isWindows = true; @@ -270,7 +502,7 @@ boolean getServerSupportsColumnEncryption() { } static Map globalCustomColumnEncryptionKeyStoreProviders = null; // This is a per-connection store provider. It can be JKS or AKV. - Map systemColumnEncryptionKeyStoreProvider = new HashMap(); + Map systemColumnEncryptionKeyStoreProvider = new HashMap<>(); /** * Registers key store providers in the globalCustomColumnEncryptionKeyStoreProviders. @@ -293,7 +525,7 @@ public static synchronized void registerColumnEncryptionKeyStoreProviders( throw new SQLServerException(null, SQLServerException.getErrString("R_CustomKeyStoreProviderSetOnce"), null, 0, false); } - globalCustomColumnEncryptionKeyStoreProviders = new HashMap(); + globalCustomColumnEncryptionKeyStoreProviders = new HashMap<>(); for (Map.Entry entry : clientKeyStoreProviders.entrySet()) { String providerName = entry.getKey(); @@ -357,7 +589,7 @@ synchronized SQLServerColumnEncryptionKeyStoreProvider getSystemColumnEncryption } private String trustedServerNameAE = null; - private static Map> columnEncryptionTrustedMasterKeyPaths = new HashMap>(); + private static Map> columnEncryptionTrustedMasterKeyPaths = new HashMap<>(); /** * Sets Trusted Master Key Paths in the columnEncryptionTrustedMasterKeyPaths. @@ -423,7 +655,7 @@ public static synchronized void removeColumnEncryptionTrustedMasterKeyPaths(Stri public static synchronized Map> getColumnEncryptionTrustedMasterKeyPaths() { loggerExternal.entering(SQLServerConnection.class.getName(), "getColumnEncryptionTrustedMasterKeyPaths", "Getting Trusted Master Key Paths"); - Map> masterKeyPathCopy = new HashMap>(); + Map> masterKeyPathCopy = new HashMap<>(); for (Map.Entry> entry : columnEncryptionTrustedMasterKeyPaths.entrySet()) { masterKeyPathCopy.put(entry.getKey(), entry.getValue()); @@ -683,13 +915,25 @@ final boolean attachConnId() { initResettableValues(false); // JDBC 3 driver only works with 1.5 JRE - if (3 == DriverJDBCVersion.major && !Util.SYSTEM_SPEC_VERSION.equals("1.5")) { + if (3 == DriverJDBCVersion.major && !"1.5".equals(Util.SYSTEM_SPEC_VERSION)) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unsupportedJREVersion")); Object[] msgArgs = {Util.SYSTEM_SPEC_VERSION}; String message = form.format(msgArgs); connectionlogger.severe(message); throw new UnsupportedOperationException(message); } + + // Caching turned on? + if (0 < this.getStatementPoolingCacheSize()) { + preparedStatementHandleCache = new Builder() + .maximumWeightedCapacity(getStatementPoolingCacheSize()) + .listener(new PreparedStatementCacheEvictionListener()) + .build(); + + parameterMetadataCache = new Builder() + .maximumWeightedCapacity(getStatementPoolingCacheSize()) + .build(); + } } void setFailoverPartnerServerProvided(String partner) { @@ -791,9 +1035,9 @@ public String toString() { return false; String lcpropValue = propValue.toLowerCase(Locale.US); - if (lcpropValue.equals("true")) + if ("true".equals(lcpropValue)) return true; - if (lcpropValue.equals("false")) + if ("false".equals(lcpropValue)) return false; MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidBooleanValue")); Object[] msgArgs = {propName}; @@ -851,7 +1095,10 @@ Connection connect(Properties propsIn, // timeout, default is 15 per spec String sPropValue = propsIn.getProperty(SQLServerDriverIntProperty.LOGIN_TIMEOUT.toString()); if (null != sPropValue && sPropValue.length() > 0) { - loginTimeoutSeconds = Integer.parseInt(sPropValue); + int sPropValueInt = Integer.parseInt(sPropValue); + if (0 != sPropValueInt) { // Use the default timeout in case of a zero value + loginTimeoutSeconds = sPropValueInt; + } } } @@ -942,9 +1189,9 @@ Connection connectInternal(Properties propsIn, pooledConnectionParent = pooledConnection; - String sPropKey = null; - String sPropValue = null; - + String sPropKey; + String sPropValue; + sPropKey = SQLServerDriverStringProperty.USER.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); if (sPropValue == null) { @@ -1006,9 +1253,8 @@ Connection connectInternal(Properties propsIn, String sPropKeyPort = SQLServerDriverIntProperty.PORT_NUMBER.toString(); String sPropValuePort = activeConnectionProperties.getProperty(sPropKeyPort); - int px = sPropValue.indexOf('\\'); + int px = sPropValue.indexOf('\\'); - String instancePort = null; String instanceNameProperty = SQLServerDriverStringProperty.INSTANCE_NAME.toString(); // found the instance name with the severname @@ -1086,16 +1332,16 @@ Connection connectInternal(Properties propsIn, if (null != sPropValue) { keyStoreLocation = sPropValue; } - + registerKeyStoreProviderOnConnection(keyStoreAuthentication, keyStoreSecret, keyStoreLocation); - sPropKey = SQLServerDriverBooleanProperty.MULTI_SUBNET_FAILOVER.toString(); - sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { - sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.MULTI_SUBNET_FAILOVER.getDefaultValue()); - activeConnectionProperties.setProperty(sPropKey, sPropValue); - } - multiSubnetFailover = booleanPropertyOn(sPropKey, sPropValue); + sPropKey = SQLServerDriverBooleanProperty.MULTI_SUBNET_FAILOVER.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (sPropValue == null) { + sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.MULTI_SUBNET_FAILOVER.getDefaultValue()); + activeConnectionProperties.setProperty(sPropKey, sPropValue); + } + multiSubnetFailover = booleanPropertyOn(sPropKey, sPropValue); sPropKey = SQLServerDriverBooleanProperty.TRANSPARENT_NETWORK_IP_RESOLUTION.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); @@ -1115,40 +1361,43 @@ Connection connectInternal(Properties propsIn, // Set requestedEncryptionLevel according to the value of the encrypt connection property requestedEncryptionLevel = booleanPropertyOn(sPropKey, sPropValue) ? TDS.ENCRYPT_ON : TDS.ENCRYPT_OFF; - sPropKey = SQLServerDriverBooleanProperty.TRUST_SERVER_CERTIFICATE.toString(); - sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { - sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.TRUST_SERVER_CERTIFICATE.getDefaultValue()); - activeConnectionProperties.setProperty(sPropKey, sPropValue); - } + sPropKey = SQLServerDriverBooleanProperty.TRUST_SERVER_CERTIFICATE.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (sPropValue == null) { + sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.TRUST_SERVER_CERTIFICATE.getDefaultValue()); + activeConnectionProperties.setProperty(sPropKey, sPropValue); + } trustServerCertificate = booleanPropertyOn(sPropKey, sPropValue); - sPropKey = SQLServerDriverStringProperty.SELECT_METHOD.toString(); - sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) - sPropValue = SQLServerDriverStringProperty.SELECT_METHOD.getDefaultValue(); - if (sPropValue.equalsIgnoreCase("cursor") || sPropValue.equalsIgnoreCase("direct")) { - activeConnectionProperties.setProperty(sPropKey, sPropValue.toLowerCase()); - } - else { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidselectMethod")); - Object[] msgArgs = {sPropValue}; - SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false); - } + trustManagerClass = activeConnectionProperties.getProperty(SQLServerDriverStringProperty.TRUST_MANAGER_CLASS.toString()); + trustManagerConstructorArg = activeConnectionProperties.getProperty(SQLServerDriverStringProperty.TRUST_MANAGER_CONSTRUCTOR_ARG.toString()); - sPropKey = SQLServerDriverStringProperty.RESPONSE_BUFFERING.toString(); - sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) - sPropValue = SQLServerDriverStringProperty.RESPONSE_BUFFERING.getDefaultValue(); - if (sPropValue.equalsIgnoreCase("full") || sPropValue.equalsIgnoreCase("adaptive")) { - activeConnectionProperties.setProperty(sPropKey, sPropValue.toLowerCase()); - } - else { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidresponseBuffering")); - Object[] msgArgs = {sPropValue}; - SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false); - } + sPropKey = SQLServerDriverStringProperty.SELECT_METHOD.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (sPropValue == null) + sPropValue = SQLServerDriverStringProperty.SELECT_METHOD.getDefaultValue(); + if ("cursor".equalsIgnoreCase(sPropValue) || "direct".equalsIgnoreCase(sPropValue)) { + activeConnectionProperties.setProperty(sPropKey, sPropValue.toLowerCase(Locale.ENGLISH)); + } + else { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidselectMethod")); + Object[] msgArgs = {sPropValue}; + SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false); + } + + sPropKey = SQLServerDriverStringProperty.RESPONSE_BUFFERING.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (sPropValue == null) + sPropValue = SQLServerDriverStringProperty.RESPONSE_BUFFERING.getDefaultValue(); + if ("full".equalsIgnoreCase(sPropValue) || "adaptive".equalsIgnoreCase(sPropValue)) { + activeConnectionProperties.setProperty(sPropKey, sPropValue.toLowerCase(Locale.ENGLISH)); + } + else { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidresponseBuffering")); + Object[] msgArgs = {sPropValue}; + SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false); + } sPropKey = SQLServerDriverStringProperty.APPLICATION_INTENT.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); @@ -1157,29 +1406,43 @@ Connection connectInternal(Properties propsIn, applicationIntent = ApplicationIntent.valueOfString(sPropValue); activeConnectionProperties.setProperty(sPropKey, applicationIntent.toString()); - sPropKey = SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.toString(); - sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { - sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.getDefaultValue()); - activeConnectionProperties.setProperty(sPropKey, sPropValue); - } - - sendTimeAsDatetime = booleanPropertyOn(sPropKey, sPropValue); + sPropKey = SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (sPropValue == null) { + sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.getDefaultValue()); + activeConnectionProperties.setProperty(sPropKey, sPropValue); + } - sPropKey = SQLServerDriverBooleanProperty.DISABLE_STATEMENT_POOLING.toString(); - sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue != null) // if the user does not set it, it is ok but if set the value can only be true - if (false == booleanPropertyOn(sPropKey, sPropValue)) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invaliddisableStatementPooling")); - Object[] msgArgs = {new String(sPropValue)}; - SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false); - } + sendTimeAsDatetime = booleanPropertyOn(sPropKey, sPropValue); - sPropKey = SQLServerDriverBooleanProperty.INTEGRATED_SECURITY.toString(); - sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue != null) { - integratedSecurity = booleanPropertyOn(sPropKey, sPropValue); + // Must be set before DISABLE_STATEMENT_POOLING + sPropKey = SQLServerDriverIntProperty.STATEMENT_POOLING_CACHE_SIZE.toString(); + if (activeConnectionProperties.getProperty(sPropKey) != null && activeConnectionProperties.getProperty(sPropKey).length() > 0) { + try { + int n = new Integer(activeConnectionProperties.getProperty(sPropKey)); + this.setStatementPoolingCacheSize(n); } + catch (NumberFormatException e) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_statementPoolingCacheSize")); + Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)}; + SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false); + } + } + + // Must be set after STATEMENT_POOLING_CACHE_SIZE + sPropKey = SQLServerDriverBooleanProperty.DISABLE_STATEMENT_POOLING.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (null != sPropValue) { + // If disabled set cache size to 0 if disabled. + if(booleanPropertyOn(sPropKey, sPropValue)) + this.setStatementPoolingCacheSize(0); + } + + sPropKey = SQLServerDriverBooleanProperty.INTEGRATED_SECURITY.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (sPropValue != null) { + integratedSecurity = booleanPropertyOn(sPropKey, sPropValue); + } // Ignore authenticationScheme setting if integrated authentication not specified if (integratedSecurity) { @@ -1190,87 +1453,109 @@ Connection connectInternal(Properties propsIn, } } - if(intAuthScheme == AuthenticationScheme.javaKerberos){ - sPropKey = SQLServerDriverObjectProperty.GSS_CREDENTIAL.toString(); - if(activeConnectionProperties.containsKey(sPropKey)) - ImpersonatedUserCred = (GSSCredential) activeConnectionProperties.get(sPropKey); - } - - sPropKey = SQLServerDriverStringProperty.AUTHENTICATION.toString(); - sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { - sPropValue = SQLServerDriverStringProperty.AUTHENTICATION.getDefaultValue(); - } - authenticationString = SqlAuthentication.valueOfString(sPropValue).toString(); - - if ((true == integratedSecurity) && (!authenticationString.equalsIgnoreCase(SqlAuthentication.NotSpecified.toString()))) { - connectionlogger.severe(toString() + " " + SQLServerException.getErrString("R_SetAuthenticationWhenIntegratedSecurityTrue")); - throw new SQLServerException(SQLServerException.getErrString("R_SetAuthenticationWhenIntegratedSecurityTrue"), null); - } - - if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString()) - && ((!activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString()).isEmpty()) - || (!activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString()).isEmpty()))) { - connectionlogger.severe(toString() + " " + SQLServerException.getErrString("R_IntegratedAuthenticationWithUserPassword")); - throw new SQLServerException(SQLServerException.getErrString("R_IntegratedAuthenticationWithUserPassword"), null); - } - - if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryPassword.toString()) - && ((activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString()).isEmpty()) - || (activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString()).isEmpty()))) { - connectionlogger.severe(toString() + " " + SQLServerException.getErrString("R_NoUserPasswordForActivePassword")); - throw new SQLServerException(SQLServerException.getErrString("R_NoUserPasswordForActivePassword"), null); - } - - if (authenticationString.equalsIgnoreCase(SqlAuthentication.SqlPassword.toString()) - && ((activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString()).isEmpty()) - || (activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString()).isEmpty()))) { - connectionlogger.severe(toString() + " " + SQLServerException.getErrString("R_NoUserPasswordForSqlPassword")); - throw new SQLServerException(SQLServerException.getErrString("R_NoUserPasswordForSqlPassword"), null); - } - - sPropKey = SQLServerDriverStringProperty.ACCESS_TOKEN.toString(); - sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (null != sPropValue) { - accessTokenInByte = sPropValue.getBytes(UTF_16LE); - } - - if ((null != accessTokenInByte) && 0 == accessTokenInByte.length) { - connectionlogger.severe(toString() + " " + SQLServerException.getErrString("R_AccessTokenCannotBeEmpty")); - throw new SQLServerException(SQLServerException.getErrString("R_AccessTokenCannotBeEmpty"), null); - } - - if ((true == integratedSecurity) && (null != accessTokenInByte)) { - connectionlogger.severe(toString() + " " + SQLServerException.getErrString("R_SetAccesstokenWhenIntegratedSecurityTrue")); - throw new SQLServerException(SQLServerException.getErrString("R_SetAccesstokenWhenIntegratedSecurityTrue"), null); - } - - if ((!authenticationString.equalsIgnoreCase(SqlAuthentication.NotSpecified.toString())) && (null != accessTokenInByte)) { - connectionlogger.severe(toString() + " " + SQLServerException.getErrString("R_SetBothAuthenticationAndAccessToken")); - throw new SQLServerException(SQLServerException.getErrString("R_SetBothAuthenticationAndAccessToken"), null); - } - - if ((null != accessTokenInByte) && ((!activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString()).isEmpty()) - || (!activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString()).isEmpty()))) { - connectionlogger.severe(toString() + " " + SQLServerException.getErrString("R_AccessTokenWithUserPassword")); - throw new SQLServerException(SQLServerException.getErrString("R_AccessTokenWithUserPassword"), null); - } - - if ((!System.getProperty("os.name").toLowerCase().startsWith("windows")) - && (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString()))) { - throw new SQLServerException(SQLServerException.getErrString("R_AADIntegratedOnNonWindows"), null); - } - + if(intAuthScheme == AuthenticationScheme.javaKerberos){ + sPropKey = SQLServerDriverObjectProperty.GSS_CREDENTIAL.toString(); + if(activeConnectionProperties.containsKey(sPropKey)) + ImpersonatedUserCred = (GSSCredential) activeConnectionProperties.get(sPropKey); + } + + sPropKey = SQLServerDriverStringProperty.AUTHENTICATION.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (sPropValue == null) { + sPropValue = SQLServerDriverStringProperty.AUTHENTICATION.getDefaultValue(); + } + authenticationString = SqlAuthentication.valueOfString(sPropValue).toString(); + + if ((true == integratedSecurity) && (!authenticationString.equalsIgnoreCase(SqlAuthentication.NotSpecified.toString()))) { + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + " " + SQLServerException.getErrString("R_SetAuthenticationWhenIntegratedSecurityTrue")); + } + throw new SQLServerException(SQLServerException.getErrString("R_SetAuthenticationWhenIntegratedSecurityTrue"), null); + } + + if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString()) + && ((!activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString()).isEmpty()) + || (!activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString()).isEmpty()))) { + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + " " + SQLServerException.getErrString("R_IntegratedAuthenticationWithUserPassword")); + } + throw new SQLServerException(SQLServerException.getErrString("R_IntegratedAuthenticationWithUserPassword"), null); + } + + if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryPassword.toString()) + && ((activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString()).isEmpty()) + || (activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString()).isEmpty()))) { + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + " " + SQLServerException.getErrString("R_NoUserPasswordForActivePassword")); + } + throw new SQLServerException(SQLServerException.getErrString("R_NoUserPasswordForActivePassword"), null); + } + + if (authenticationString.equalsIgnoreCase(SqlAuthentication.SqlPassword.toString()) + && ((activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString()).isEmpty()) + || (activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString()).isEmpty()))) { + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + " " + SQLServerException.getErrString("R_NoUserPasswordForSqlPassword")); + } + throw new SQLServerException(SQLServerException.getErrString("R_NoUserPasswordForSqlPassword"), null); + } + + sPropKey = SQLServerDriverStringProperty.ACCESS_TOKEN.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (null != sPropValue) { + accessTokenInByte = sPropValue.getBytes(UTF_16LE); + } + + if ((null != accessTokenInByte) && 0 == accessTokenInByte.length) { + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + " " + SQLServerException.getErrString("R_AccessTokenCannotBeEmpty")); + } + throw new SQLServerException(SQLServerException.getErrString("R_AccessTokenCannotBeEmpty"), null); + } + + if ((true == integratedSecurity) && (null != accessTokenInByte)) { + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + " " + SQLServerException.getErrString("R_SetAccesstokenWhenIntegratedSecurityTrue")); + } + throw new SQLServerException(SQLServerException.getErrString("R_SetAccesstokenWhenIntegratedSecurityTrue"), null); + } + + if ((!authenticationString.equalsIgnoreCase(SqlAuthentication.NotSpecified.toString())) && (null != accessTokenInByte)) { + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + " " + SQLServerException.getErrString("R_SetBothAuthenticationAndAccessToken")); + } + throw new SQLServerException(SQLServerException.getErrString("R_SetBothAuthenticationAndAccessToken"), null); + } + + if ((null != accessTokenInByte) && ((!activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString()).isEmpty()) + || (!activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString()).isEmpty()))) { + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + " " + SQLServerException.getErrString("R_AccessTokenWithUserPassword")); + } + throw new SQLServerException(SQLServerException.getErrString("R_AccessTokenWithUserPassword"), null); + } + + if ((!System.getProperty("os.name").toLowerCase(Locale.ENGLISH).startsWith("windows")) + && (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString()))) { + throw new SQLServerException(SQLServerException.getErrString("R_AADIntegratedOnNonWindows"), null); + } + + // Turn off TNIR for FedAuth if user does not set TNIR explicitly + if (!userSetTNIR) { + if ((!authenticationString.equalsIgnoreCase(SqlAuthentication.NotSpecified.toString())) || (null != accessTokenInByte)) { + transparentNetworkIPResolution = false; + } + } sPropKey = SQLServerDriverStringProperty.WORKSTATION_ID.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); ValidateMaxSQLLoginName(sPropKey, sPropValue); - sPropKey = SQLServerDriverIntProperty.PORT_NUMBER.toString(); - try { - String strPort = activeConnectionProperties.getProperty(sPropKey); - if (null != strPort) { - nPort = (new Integer(strPort)).intValue(); - + int nPort = 0; + sPropKey = SQLServerDriverIntProperty.PORT_NUMBER.toString(); + try { + String strPort = activeConnectionProperties.getProperty(sPropKey); + if (null != strPort) { + nPort = new Integer(strPort); if ((nPort < 0) || (nPort > 65535)) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPortNumber")); Object[] msgArgs = {Integer.toString(nPort)}; @@ -1344,71 +1629,144 @@ else if (0 == requestedPacketSize) responseBuffering = activeConnectionProperties.getProperty(sPropKey); } - sPropKey = SQLServerDriverIntProperty.LOCK_TIMEOUT.toString(); - int defaultLockTimeOut = SQLServerDriverIntProperty.LOCK_TIMEOUT.getDefaultValue(); - nLockTimeout = defaultLockTimeOut; // Wait forever - if (activeConnectionProperties.getProperty(sPropKey) != null && activeConnectionProperties.getProperty(sPropKey).length() > 0) { - try { - int n = (new Integer(activeConnectionProperties.getProperty(sPropKey))).intValue(); - if (n >= defaultLockTimeOut) - nLockTimeout = n; - else { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLockTimeOut")); - Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)}; - SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false); - } - } - catch (NumberFormatException e) { + sPropKey = SQLServerDriverIntProperty.LOCK_TIMEOUT.toString(); + int defaultLockTimeOut = SQLServerDriverIntProperty.LOCK_TIMEOUT.getDefaultValue(); + nLockTimeout = defaultLockTimeOut; // Wait forever + if (activeConnectionProperties.getProperty(sPropKey) != null && activeConnectionProperties.getProperty(sPropKey).length() > 0) { + try { + int n = new Integer(activeConnectionProperties.getProperty(sPropKey)); + if (n >= defaultLockTimeOut) + nLockTimeout = n; + else { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLockTimeOut")); Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)}; SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false); } } + catch (NumberFormatException e) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLockTimeOut")); + Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)}; + SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false); + } + } - sPropKey = SQLServerDriverIntProperty.QUERY_TIMEOUT.toString(); - int defaultQueryTimeout = SQLServerDriverIntProperty.QUERY_TIMEOUT.getDefaultValue(); - queryTimeoutSeconds = defaultQueryTimeout; // Wait forever - if (activeConnectionProperties.getProperty(sPropKey) != null && activeConnectionProperties.getProperty(sPropKey).length() > 0) { - try { - int n = (new Integer(activeConnectionProperties.getProperty(sPropKey))).intValue(); - if (n >= defaultQueryTimeout) { - queryTimeoutSeconds = n; - } - else { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidQueryTimeout")); - Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)}; - SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false); - } + sPropKey = SQLServerDriverIntProperty.QUERY_TIMEOUT.toString(); + int defaultQueryTimeout = SQLServerDriverIntProperty.QUERY_TIMEOUT.getDefaultValue(); + queryTimeoutSeconds = defaultQueryTimeout; // Wait forever + if (activeConnectionProperties.getProperty(sPropKey) != null && activeConnectionProperties.getProperty(sPropKey).length() > 0) { + try { + int n = new Integer(activeConnectionProperties.getProperty(sPropKey)); + if (n >= defaultQueryTimeout) { + queryTimeoutSeconds = n; } - catch (NumberFormatException e) { + else { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidQueryTimeout")); Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)}; SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false); } } + catch (NumberFormatException e) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidQueryTimeout")); + Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)}; + SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false); + } + } - sPropKey = SQLServerDriverIntProperty.SOCKET_TIMEOUT.toString(); - int defaultSocketTimeout = SQLServerDriverIntProperty.SOCKET_TIMEOUT.getDefaultValue(); - socketTimeoutMilliseconds = defaultSocketTimeout; // Wait forever - if (activeConnectionProperties.getProperty(sPropKey) != null && activeConnectionProperties.getProperty(sPropKey).length() > 0) { - try { - int n = (new Integer(activeConnectionProperties.getProperty(sPropKey))).intValue(); - if (n >= defaultSocketTimeout) { - socketTimeoutMilliseconds = n; - } - else { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidSocketTimeout")); - Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)}; - SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false); - } + sPropKey = SQLServerDriverIntProperty.SOCKET_TIMEOUT.toString(); + int defaultSocketTimeout = SQLServerDriverIntProperty.SOCKET_TIMEOUT.getDefaultValue(); + socketTimeoutMilliseconds = defaultSocketTimeout; // Wait forever + if (activeConnectionProperties.getProperty(sPropKey) != null && activeConnectionProperties.getProperty(sPropKey).length() > 0) { + try { + int n = new Integer(activeConnectionProperties.getProperty(sPropKey)); + if (n >= defaultSocketTimeout) { + socketTimeoutMilliseconds = n; } - catch (NumberFormatException e) { + else { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidSocketTimeout")); Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)}; SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false); } } + catch (NumberFormatException e) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidSocketTimeout")); + Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)}; + SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false); + } + } + + sPropKey = SQLServerDriverIntProperty.SERVER_PREPARED_STATEMENT_DISCARD_THRESHOLD.toString(); + if (activeConnectionProperties.getProperty(sPropKey) != null && activeConnectionProperties.getProperty(sPropKey).length() > 0) { + try { + int n = new Integer(activeConnectionProperties.getProperty(sPropKey)); + setServerPreparedStatementDiscardThreshold(n); + } + catch (NumberFormatException e) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_serverPreparedStatementDiscardThreshold")); + Object[] msgArgs = {activeConnectionProperties.getProperty(sPropKey)}; + SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false); + } + } + + sPropKey = SQLServerDriverBooleanProperty.ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (null != sPropValue) { + setEnablePrepareOnFirstPreparedStatementCall(booleanPropertyOn(sPropKey, sPropValue)); + } + sPropKey = SQLServerDriverStringProperty.SSL_PROTOCOL.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (null == sPropValue) { + sPropValue = SQLServerDriverStringProperty.SSL_PROTOCOL.getDefaultValue(); + activeConnectionProperties.setProperty(sPropKey, sPropValue); + } + else { + activeConnectionProperties.setProperty(sPropKey, SSLProtocol.valueOfString(sPropValue).toString()); + } + + String databaseNameProperty = SQLServerDriverStringProperty.DATABASE_NAME.toString(); + String serverNameProperty = SQLServerDriverStringProperty.SERVER_NAME.toString(); + String failOverPartnerProperty = SQLServerDriverStringProperty.FAILOVER_PARTNER.toString(); + String failOverPartnerPropertyValue = activeConnectionProperties.getProperty(failOverPartnerProperty); + + // failoverPartner and multiSubnetFailover=true cannot be used together + if (multiSubnetFailover && failOverPartnerPropertyValue != null) { + SQLServerException.makeFromDriverError(this, this, SQLServerException.getErrString("R_dbMirroringWithMultiSubnetFailover"), null, + false); + } + + // transparentNetworkIPResolution is ignored if multiSubnetFailover or DBMirroring is true and user does not set TNIR explicitly + if (multiSubnetFailover || (null != failOverPartnerPropertyValue)) { + if (!userSetTNIR) { + transparentNetworkIPResolution = false; + } + } + + // failoverPartner and applicationIntent=ReadOnly cannot be used together + if ((applicationIntent != null) && applicationIntent.equals(ApplicationIntent.READ_ONLY) && failOverPartnerPropertyValue != null) { + SQLServerException.makeFromDriverError(this, this, SQLServerException.getErrString("R_dbMirroringWithReadOnlyIntent"), null, false); + } + + // check to see failover specified without DB error here if not. + if (null != activeConnectionProperties.getProperty(databaseNameProperty)) { + // look to see if there exists a failover + failoverInfo = FailoverMapSingleton.getFailoverInfo(this, activeConnectionProperties.getProperty(serverNameProperty), + activeConnectionProperties.getProperty(instanceNameProperty), + activeConnectionProperties.getProperty(databaseNameProperty)); + } + else { + // it is an error to specify failover without db. + if (null != failOverPartnerPropertyValue) + SQLServerException.makeFromDriverError(this, this, SQLServerException.getErrString("R_failoverPartnerWithoutDB"), null, true); + } + + String mirror = null; + if (null == failoverInfo) + mirror = failOverPartnerPropertyValue; + + long startTime = System.currentTimeMillis(); + login(activeConnectionProperties.getProperty(serverNameProperty), instanceValue, nPort, mirror, failoverInfo, loginTimeoutSeconds, + startTime); + connectRetryCount = SQLServerDriverIntProperty.CONNECT_RETRY_COUNT.getDefaultValue(); // if the user does not specify a default // timeout, default is 1 sPropValue = activeConnectionProperties.getProperty(SQLServerDriverIntProperty.CONNECT_RETRY_COUNT.toString()); @@ -1426,6 +1784,7 @@ else if (0 == requestedPacketSize) connectionPropertyExceptionHelper("R_invalidConnectRetryCount", sPropValue); } } + connectRetryInterval = SQLServerDriverIntProperty.CONNECT_RETRY_INTERVAL.getDefaultValue(); // if the user does not specify a default // timeout, default is 10 seconds sPropValue = activeConnectionProperties.getProperty(SQLServerDriverIntProperty.CONNECT_RETRY_INTERVAL.toString()); @@ -1441,49 +1800,6 @@ else if (0 == requestedPacketSize) connectionPropertyExceptionHelper("R_invalidConnectRetryInterval", sPropValue); } } - - String databaseNameProperty = SQLServerDriverStringProperty.DATABASE_NAME.toString(); - String serverNameProperty = SQLServerDriverStringProperty.SERVER_NAME.toString(); - String failOverPartnerProperty = SQLServerDriverStringProperty.FAILOVER_PARTNER.toString(); - String failOverPartnerPropertyValue = activeConnectionProperties.getProperty(failOverPartnerProperty); - - // failoverPartner and multiSubnetFailover=true cannot be used together - if (multiSubnetFailover && failOverPartnerPropertyValue != null) { - SQLServerException.makeFromDriverError(this, this, SQLServerException.getErrString("R_dbMirroringWithMultiSubnetFailover"), null, - false); - } - - // transparentNetworkIPResolution is ignored if multiSubnetFailover or DBMirroring is true. - if (multiSubnetFailover || (null != failOverPartnerPropertyValue)) { - transparentNetworkIPResolution = false; - } - - // failoverPartner and applicationIntent=ReadOnly cannot be used together - if ((applicationIntent != null) && applicationIntent.equals(ApplicationIntent.READ_ONLY) && failOverPartnerPropertyValue != null) { - SQLServerException.makeFromDriverError(this, this, SQLServerException.getErrString("R_dbMirroringWithReadOnlyIntent"), null, - false); - } - - // check to see failover specified without DB error here if not. - if (null != activeConnectionProperties.getProperty(databaseNameProperty)) { - // look to see if there exists a failover - failoverInfo = FailoverMapSingleton.getFailoverInfo(this, activeConnectionProperties.getProperty(serverNameProperty), - activeConnectionProperties.getProperty(instanceNameProperty), - activeConnectionProperties.getProperty(databaseNameProperty)); - } - else { - // it is an error to specify failover without db. - if (null != failOverPartnerPropertyValue) - SQLServerException.makeFromDriverError(this, this, SQLServerException.getErrString("R_failoverPartnerWithoutDB"), null, true); - } - - String mirror = null; - if (null == failoverInfo) - mirror = failOverPartnerPropertyValue; - - long startTime = System.currentTimeMillis(); - login(activeConnectionProperties.getProperty(serverNameProperty), instanceValue, nPort, mirror, failoverInfo, loginTimeoutSeconds, - startTime); } else { // reconnecting @@ -1499,8 +1815,10 @@ else if (0 == requestedPacketSize) int sslRecordSize = Util.isIBM() ? 8192 : 16384; if (tdsPacketSize > sslRecordSize) { - connectionlogger.finer(toString() + " Negotiated tdsPacketSize " + tdsPacketSize + " is too large for SSL with JRE " - + Util.SYSTEM_JRE + " (max size is " + sslRecordSize + ")"); + if (connectionlogger.isLoggable(Level.FINER)) { + connectionlogger.finer(toString() + " Negotiated tdsPacketSize " + tdsPacketSize + " is too large for SSL with JRE " + + Util.SYSTEM_JRE + " (max size is " + sslRecordSize + ")"); + } MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_packetSizeTooBigForSSL")); Object[] msgArgs = {Integer.toString(sslRecordSize)}; terminate(SQLServerException.DRIVER_ERROR_UNSUPPORTED_CONFIG, form.format(msgArgs)); @@ -1549,7 +1867,8 @@ private void login(String primary, long timerStart) throws SQLServerException { // standardLogin would be false only for db mirroring scenarios. It would be true // for all other cases, including multiSubnetFailover - final boolean isDBMirroring = (null == mirror && null == foActual) ? false : true; + final boolean isDBMirroring = null != mirror || null != foActual; + long timerExpire; int sleepInterval = 100; // milliseconds to sleep (back off) between attempts. long timeoutUnitInterval; @@ -1582,13 +1901,16 @@ private void login(String primary, if (0 == timeout) { timeout = SQLServerDriverIntProperty.LOGIN_TIMEOUT.getDefaultValue(); } - long timerTimeout = timeout * 1000; // ConnectTimeout is in seconds, we need timer millis + long timerTimeout = timeout * 1000L; // ConnectTimeout is in seconds, we need timer millis timerExpire = timerStart + timerTimeout; // For non-dbmirroring, non-tnir and non-multisubnetfailover scenarios, full time out would be used as time slice. - if (isDBMirroring || useParallel || useTnir) { + if (isDBMirroring || useParallel) { timeoutUnitInterval = (long) (TIMEOUTSTEP * timerTimeout); } + else if (useTnir) { + timeoutUnitInterval = (long) (TIMEOUTSTEP_TNIR * timerTimeout); + } else { timeoutUnitInterval = timerTimeout; } @@ -1759,7 +2081,8 @@ else if (null == currentPrimaryPlaceHolder) { Thread.sleep(sleepInterval); } catch (InterruptedException e) { - // continue if the thread is interrupted. This really should not happen. + // re-interrupt the current thread, in order to restore the thread's interrupt status. + Thread.currentThread().interrupt(); } sleepInterval = (sleepInterval < 500) ? sleepInterval * 2 : 1000; } @@ -1767,12 +2090,22 @@ else if (null == currentPrimaryPlaceHolder) { // Update timeout interval (but no more than the point where we're supposed to fail: timerExpire) attemptNumber++; - if (useParallel || useTnir) { + if (useParallel) { intervalExpire = System.currentTimeMillis() + (timeoutUnitInterval * (attemptNumber + 1)); } else if (isDBMirroring) { intervalExpire = System.currentTimeMillis() + (timeoutUnitInterval * ((attemptNumber / 2) + 1)); } + else if (useTnir) { + long timeSlice = timeoutUnitInterval * (1 << attemptNumber); + + // In case the timeout for the first slice is less than 500 ms then bump it up to 500 ms + if ((1 == attemptNumber) && (500 > timeSlice)) { + timeSlice = 500; + } + + intervalExpire = System.currentTimeMillis() + timeSlice; + } else intervalExpire = timerExpire; // Due to the below condition and the timerHasExpired check in catch block, @@ -1868,7 +2201,7 @@ ServerPortPlaceHolder primaryPermissionCheck(String primary, connectionlogger.fine(toString() + " SQL Server port returned by SQL Browser: " + instancePort); try { if (null != instancePort) { - primaryPortNumber = (new Integer(instancePort)).intValue(); + primaryPortNumber = new Integer(instancePort); if ((primaryPortNumber < 0) || (primaryPortNumber > 65535)) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPortNumber")); @@ -1943,6 +2276,14 @@ private void connectHelper(ServerPortPlaceHolder serverInfo, connectionlogger.fine(toString() + " Connecting with server: " + serverInfo.getServerName() + " port: " + serverInfo.getPortNumber() + " Timeout slice: " + timeOutsliceInMillis + " Timeout Full: " + timeOutFullInSeconds); } + + // Before opening the TDSChannel, calculate local hostname + // as the InetAddress.getLocalHost() takes more than usual time in certain OS and JVM combination, it avoids connection loss + hostName = activeConnectionProperties.getProperty(SQLServerDriverStringProperty.WORKSTATION_ID.toString()); + if (StringUtils.isEmpty(hostName)) { + hostName = Util.lookupHostName(); + } + // if the timeout is infinite slices are infinite too. tdsChannel = new TDSChannel(this); if (0 == timeOutFullInSeconds) @@ -2143,8 +2484,10 @@ void Prelogin(String serverName, // then maybe we are just trying to talk to an older server that doesn't support prelogin // (and that we don't support with this driver). if (-1 == bytesRead) { - connectionlogger.warning( - toString() + preloginErrorLogString + " Unexpected end of prelogin response after " + responseBytesRead + " bytes read"); + if (connectionlogger.isLoggable(Level.WARNING)) { + connectionlogger.warning( + toString() + preloginErrorLogString + " Unexpected end of prelogin response after " + responseBytesRead + " bytes read"); + } MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_tcpipConnectionFailed")); Object[] msgArgs = {serverName, Integer.toString(portNumber), SQLServerException.getErrString("R_notSQLServer")}; terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, form.format(msgArgs)); @@ -2164,7 +2507,9 @@ void Prelogin(String serverName, if (!processedResponseHeader && responseBytesRead >= TDS.PACKET_HEADER_SIZE) { // Verify that the response is actually a response... if (TDS.PKT_REPLY != preloginResponse[0]) { - connectionlogger.warning(toString() + preloginErrorLogString + " Unexpected response type:" + preloginResponse[0]); + if (connectionlogger.isLoggable(Level.WARNING)) { + connectionlogger.warning(toString() + preloginErrorLogString + " Unexpected response type:" + preloginResponse[0]); + } MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_tcpipConnectionFailed")); Object[] msgArgs = {serverName, Integer.toString(portNumber), SQLServerException.getErrString("R_notSQLServer")}; terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, form.format(msgArgs)); @@ -2174,7 +2519,9 @@ void Prelogin(String serverName, // In theory, it can be longer, but in current practice it isn't, as all of the // prelogin response items easily fit into a single 4K packet. if (TDS.STATUS_BIT_EOM != (TDS.STATUS_BIT_EOM & preloginResponse[1])) { - connectionlogger.warning(toString() + preloginErrorLogString + " Unexpected response status:" + preloginResponse[1]); + if (connectionlogger.isLoggable(Level.WARNING)) { + connectionlogger.warning(toString() + preloginErrorLogString + " Unexpected response status:" + preloginResponse[1]); + } MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_tcpipConnectionFailed")); Object[] msgArgs = {serverName, Integer.toString(portNumber), SQLServerException.getErrString("R_notSQLServer")}; terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, form.format(msgArgs)); @@ -2185,8 +2532,10 @@ void Prelogin(String serverName, assert responseLength >= 0; if (responseLength >= preloginResponse.length) { - connectionlogger.warning(toString() + preloginErrorLogString + " Response length:" + responseLength - + " is greater than allowed length:" + preloginResponse.length); + if (connectionlogger.isLoggable(Level.WARNING)) { + connectionlogger.warning(toString() + preloginErrorLogString + " Response length:" + responseLength + + " is greater than allowed length:" + preloginResponse.length); + } MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_tcpipConnectionFailed")); Object[] msgArgs = {serverName, Integer.toString(portNumber), SQLServerException.getErrString("R_notSQLServer")}; terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, form.format(msgArgs)); @@ -2205,7 +2554,9 @@ void Prelogin(String serverName, while (true) { // Get the option token if (responseIndex >= responseLength) { - connectionlogger.warning(toString() + " Option token not found"); + if (connectionlogger.isLoggable(Level.WARNING)) { + connectionlogger.warning(toString() + " Option token not found"); + } throwInvalidTDS(); } byte optionToken = preloginResponse[responseIndex++]; @@ -2216,7 +2567,9 @@ void Prelogin(String serverName, // Get the offset and length that follows the option token if (responseIndex + 4 >= responseLength) { - connectionlogger.warning(toString() + " Offset/Length not found for option:" + optionToken); + if (connectionlogger.isLoggable(Level.WARNING)) { + connectionlogger.warning(toString() + " Offset/Length not found for option:" + optionToken); + } throwInvalidTDS(); } @@ -2229,26 +2582,35 @@ void Prelogin(String serverName, assert optionLength >= 0; if (optionOffset + optionLength > responseLength) { - connectionlogger.warning( - toString() + " Offset:" + optionOffset + " and length:" + optionLength + " exceed response length:" + responseLength); + if (connectionlogger.isLoggable(Level.WARNING)) { + connectionlogger.warning( + toString() + " Offset:" + optionOffset + " and length:" + optionLength + " exceed response length:" + responseLength); + } throwInvalidTDS(); } switch (optionToken) { case TDS.B_PRELOGIN_OPTION_VERSION: if (receivedVersionOption) { - connectionlogger.warning(toString() + " Version option already received"); + if (connectionlogger.isLoggable(Level.WARNING)) { + connectionlogger.warning(toString() + " Version option already received"); + } throwInvalidTDS(); } if (6 != optionLength) { - connectionlogger.warning(toString() + " Version option length:" + optionLength + " is incorrect. Correct value is 6."); + if (connectionlogger.isLoggable(Level.WARNING)) { + connectionlogger.warning(toString() + " Version option length:" + optionLength + " is incorrect. Correct value is 6."); + } throwInvalidTDS(); } serverMajorVersion = preloginResponse[optionOffset]; if (serverMajorVersion < 9) { - connectionlogger.warning(toString() + " Server major version:" + serverMajorVersion + " is not supported by this driver."); + if (connectionlogger.isLoggable(Level.WARNING)) { + connectionlogger + .warning(toString() + " Server major version:" + serverMajorVersion + " is not supported by this driver."); + } MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unsupportedServerVersion")); Object[] msgArgs = {Integer.toString(preloginResponse[optionOffset])}; terminate(SQLServerException.DRIVER_ERROR_UNSUPPORTED_CONFIG, form.format(msgArgs)); @@ -2262,12 +2624,17 @@ void Prelogin(String serverName, case TDS.B_PRELOGIN_OPTION_ENCRYPTION: if (TDS.ENCRYPT_INVALID != negotiatedEncryptionLevel) { - connectionlogger.warning(toString() + " Encryption option already received"); + if (connectionlogger.isLoggable(Level.WARNING)) { + connectionlogger.warning(toString() + " Encryption option already received"); + } throwInvalidTDS(); } if (1 != optionLength) { - connectionlogger.warning(toString() + " Encryption option length:" + optionLength + " is incorrect. Correct value is 1."); + if (connectionlogger.isLoggable(Level.WARNING)) { + connectionlogger + .warning(toString() + " Encryption option length:" + optionLength + " is incorrect. Correct value is 1."); + } throwInvalidTDS(); } @@ -2276,7 +2643,9 @@ void Prelogin(String serverName, // If the server did not return a valid encryption level, terminate the connection. if (TDS.ENCRYPT_OFF != negotiatedEncryptionLevel && TDS.ENCRYPT_ON != negotiatedEncryptionLevel && TDS.ENCRYPT_REQ != negotiatedEncryptionLevel && TDS.ENCRYPT_NOT_SUP != negotiatedEncryptionLevel) { - connectionlogger.warning(toString() + " Server returned " + TDS.getEncryptionLevel(negotiatedEncryptionLevel)); + if (connectionlogger.isLoggable(Level.WARNING)) { + connectionlogger.warning(toString() + " Server returned " + TDS.getEncryptionLevel(negotiatedEncryptionLevel)); + } throwInvalidTDS(); } @@ -2296,9 +2665,11 @@ void Prelogin(String serverName, if (TDS.ENCRYPT_REQ == negotiatedEncryptionLevel) terminate(SQLServerException.DRIVER_ERROR_SSL_FAILED, SQLServerException.getErrString("R_sslRequiredByServer")); - connectionlogger - .warning(toString() + " Client requested encryption level: " + TDS.getEncryptionLevel(requestedEncryptionLevel) - + " Server returned unexpected encryption level: " + TDS.getEncryptionLevel(negotiatedEncryptionLevel)); + if (connectionlogger.isLoggable(Level.WARNING)) { + connectionlogger + .warning(toString() + " Client requested encryption level: " + TDS.getEncryptionLevel(requestedEncryptionLevel) + + " Server returned unexpected encryption level: " + TDS.getEncryptionLevel(negotiatedEncryptionLevel)); + } throwInvalidTDS(); } break; @@ -2306,8 +2677,10 @@ void Prelogin(String serverName, case TDS.B_PRELOGIN_OPTION_FEDAUTHREQUIRED: // Only 0x00 and 0x01 are accepted values from the server. if (0 != preloginResponse[optionOffset] && 1 != preloginResponse[optionOffset]) { - connectionlogger.severe(toString() + " Server sent an unexpected value for FedAuthRequired PreLogin Option. Value was " - + preloginResponse[optionOffset]); + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + " Server sent an unexpected value for FedAuthRequired PreLogin Option. Value was " + + preloginResponse[optionOffset]); + } MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_FedAuthRequiredPreLoginResponseInvalidValue")); throw new SQLServerException(form.format(new Object[] {preloginResponse[optionOffset]}), null); } @@ -2317,7 +2690,7 @@ void Prelogin(String serverName, // Or AccessToken is not null, mean token based authentication is used. if (((null != authenticationString) && (!authenticationString.equalsIgnoreCase(SqlAuthentication.NotSpecified.toString()))) || (null != accessTokenInByte)) { - fedAuth.fedAuthRequiredPreLoginResponse = (preloginResponse[optionOffset] == 1 ? true : false); + fedAuthRequiredPreLoginResponse = (preloginResponse[optionOffset] == 1); } break; @@ -2329,7 +2702,9 @@ void Prelogin(String serverName, } if (!receivedVersionOption || TDS.ENCRYPT_INVALID == negotiatedEncryptionLevel) { - connectionlogger.warning(toString() + " Prelogin response is missing version and/or encryption option."); + if (connectionlogger.isLoggable(Level.WARNING)) { + connectionlogger.warning(toString() + " Prelogin response is missing version and/or encryption option."); + } throwInvalidTDS(); } } @@ -2823,7 +3198,7 @@ static String sqlStatementToSetCommit(boolean autoCommit) { public void setAutoCommit(boolean newAutoCommitMode) throws SQLServerException { if (loggerExternal.isLoggable(Level.FINER)) { - loggerExternal.entering(getClassNameLogging(), "setAutoCommit", Boolean.valueOf(newAutoCommitMode)); + loggerExternal.entering(getClassNameLogging(), "setAutoCommit", newAutoCommitMode); if (Util.IsActivityTraceOn()) loggerExternal.finer(toString() + " ActivityId: " + ActivityCorrelator.getNext().toString()); } @@ -2853,7 +3228,7 @@ public void setAutoCommit(boolean newAutoCommitMode) throws SQLServerException { checkClosed(); boolean res = !inXATransaction && databaseAutoCommitMode; if (loggerExternal.isLoggable(Level.FINER)) - loggerExternal.exiting(getClassNameLogging(), "getAutoCommit", Boolean.valueOf(res)); + loggerExternal.exiting(getClassNameLogging(), "getAutoCommit", res); return res; } @@ -2903,8 +3278,6 @@ public void rollback() throws SQLServerException { public void abort(Executor executor) throws SQLException { loggerExternal.entering(getClassNameLogging(), "abort", executor); - DriverJDBCVersion.checkSupportsJDBC41(); - // nop if connection is closed if (isClosed()) return; @@ -2978,6 +3351,20 @@ private void closeInternal() throws SQLServerException { if (null != tdsChannel) { tdsChannel.close(); } + + // Invalidate statement caches. + if (null != preparedStatementHandleCache) + preparedStatementHandleCache.clear(); + + if (null != parameterMetadataCache) + parameterMetadataCache.clear(); + + // Clean-up queue etc. related to batching of prepared statement discard actions (sp_unprepare). + cleanupPreparedStatementDiscardActions(); + + ActivityCorrelator.cleanupActivityId(); + + loggerExternal.exiting(getClassNameLogging(), "close"); } // This function is used by the proxy for notifying the pool manager that this connection proxy is closed @@ -2996,6 +3383,7 @@ final void poolCloseEventNotify() throws SQLServerException { connectionCommand("IF @@TRANCOUNT > 0 ROLLBACK TRAN" /* +close connection */, "close connection"); } notifyPooledConnection(null); + ActivityCorrelator.cleanupActivityId(); if (connectionlogger.isLoggable(Level.FINER)) { connectionlogger.finer(toString() + " Connection closed and returned to connection pool"); } @@ -3005,7 +3393,7 @@ final void poolCloseEventNotify() throws SQLServerException { /* L0 */ public boolean isClosed() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "isClosed"); - loggerExternal.exiting(getClassNameLogging(), "isClosed", Boolean.valueOf(isSessionUnAvailable())); + loggerExternal.exiting(getClassNameLogging(), "isClosed", isSessionUnAvailable()); return isSessionUnAvailable(); } @@ -3021,7 +3409,7 @@ final void poolCloseEventNotify() throws SQLServerException { /* L0 */ public void setReadOnly(boolean readOnly) throws SQLServerException { if (loggerExternal.isLoggable(Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "setReadOnly", Boolean.valueOf(readOnly)); + loggerExternal.entering(getClassNameLogging(), "setReadOnly", readOnly); checkClosed(); // do nothing per spec loggerExternal.exiting(getClassNameLogging(), "setReadOnly"); @@ -3031,7 +3419,7 @@ final void poolCloseEventNotify() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "isReadOnly"); checkClosed(); if (loggerExternal.isLoggable(Level.FINER)) - loggerExternal.exiting(getClassNameLogging(), "isReadOnly", Boolean.valueOf(false)); + loggerExternal.exiting(getClassNameLogging(), "isReadOnly", Boolean.FALSE); return false; } @@ -3057,7 +3445,7 @@ final void poolCloseEventNotify() throws SQLServerException { /* L0 */ public void setTransactionIsolation(int level) throws SQLServerException { if (loggerExternal.isLoggable(Level.FINER)) { - loggerExternal.entering(getClassNameLogging(), "setTransactionIsolation", new Integer(level)); + loggerExternal.entering(getClassNameLogging(), "setTransactionIsolation", level); if (Util.IsActivityTraceOn()) { loggerExternal.finer(toString() + " ActivityId: " + ActivityCorrelator.getNext().toString()); } @@ -3077,7 +3465,7 @@ final void poolCloseEventNotify() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getTransactionIsolation"); checkClosed(); if (loggerExternal.isLoggable(Level.FINER)) - loggerExternal.exiting(getClassNameLogging(), "getTransactionIsolation", new Integer(transactionIsolationLevel)); + loggerExternal.exiting(getClassNameLogging(), "getTransactionIsolation", transactionIsolationLevel); return transactionIsolationLevel; } @@ -3121,7 +3509,7 @@ public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLServerException { if (loggerExternal.isLoggable(Level.FINER)) loggerExternal.entering(getClassNameLogging(), "createStatement", - new Object[] {new Integer(resultSetType), new Integer(resultSetConcurrency)}); + new Object[] {resultSetType, resultSetConcurrency}); checkClosed(); Statement st = new SQLServerStatement(this, resultSetType, resultSetConcurrency, SQLServerStatementColumnEncryptionSetting.UseConnectionSetting); @@ -3134,10 +3522,10 @@ public PreparedStatement prepareStatement(String sql, int resultSetConcurrency) throws SQLServerException { if (loggerExternal.isLoggable(Level.FINER)) loggerExternal.entering(getClassNameLogging(), "prepareStatement", - new Object[] {sql, new Integer(resultSetType), new Integer(resultSetConcurrency)}); + new Object[] {sql, resultSetType, resultSetConcurrency}); checkClosed(); - PreparedStatement st = null; + PreparedStatement st; if (Util.use42Wrapper()) { st = new SQLServerPreparedStatement42(this, sql, resultSetType, resultSetConcurrency, @@ -3158,10 +3546,10 @@ private PreparedStatement prepareStatement(String sql, SQLServerStatementColumnEncryptionSetting stmtColEncSetting) throws SQLServerException { if (loggerExternal.isLoggable(Level.FINER)) loggerExternal.entering(getClassNameLogging(), "prepareStatement", - new Object[] {sql, new Integer(resultSetType), new Integer(resultSetConcurrency), stmtColEncSetting}); + new Object[] {sql, resultSetType, resultSetConcurrency, stmtColEncSetting}); checkClosed(); - PreparedStatement st = null; + PreparedStatement st; if (Util.use42Wrapper()) { st = new SQLServerPreparedStatement42(this, sql, resultSetType, resultSetConcurrency, stmtColEncSetting); @@ -3179,10 +3567,10 @@ public CallableStatement prepareCall(String sql, int resultSetConcurrency) throws SQLServerException { if (loggerExternal.isLoggable(Level.FINER)) loggerExternal.entering(getClassNameLogging(), "prepareCall", - new Object[] {sql, new Integer(resultSetType), new Integer(resultSetConcurrency)}); + new Object[] {sql, resultSetType, resultSetConcurrency}); checkClosed(); - CallableStatement st = null; + CallableStatement st; if (Util.use42Wrapper()) { st = new SQLServerCallableStatement42(this, sql, resultSetType, resultSetConcurrency, @@ -3214,7 +3602,7 @@ public CallableStatement prepareCall(String sql, public java.util.Map> getTypeMap() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getTypeMap"); checkClosed(); - java.util.Map> mp = new java.util.HashMap>(); + java.util.Map> mp = new java.util.HashMap<>(); loggerExternal.exiting(getClassNameLogging(), "getTypeMap", mp); return mp; } @@ -3246,7 +3634,6 @@ int writeFedAuthFeatureRequest(boolean write, || fedAuthFeatureExtensionData.libraryType == TDS.TDS_FEDAUTH_LIBRARY_SECURITYTOKEN); int dataLen = 0; - int totalLen = 0; // set dataLen and totalLen switch (fedAuthFeatureExtensionData.libraryType) { @@ -3263,7 +3650,7 @@ int writeFedAuthFeatureRequest(boolean write, break; } - totalLen = dataLen + 5; // length of feature id (1 byte), data length field (4 bytes), and feature data (dataLen) + int totalLen = dataLen + 5; // length of feature id (1 byte), data length field (4 bytes), and feature data (dataLen) // write feature id if (write) { @@ -3751,7 +4138,9 @@ final void processEnvChange(TDSReader tdsReader) throws SQLServerException { // Error on unrecognized, unused ENVCHANGES default: - connectionlogger.warning(toString() + " Unknown environment change: " + envchange); + if (connectionlogger.isLoggable(Level.WARNING)) { + connectionlogger.warning(toString() + " Unknown environment change: " + envchange); + } throwInvalidTDS(); break; } @@ -3855,7 +4244,9 @@ final void processFedAuthInfo(TDSReader tdsReader, if (tokenLen < 4) { // the token must at least contain a DWORD(length is 4 bytes) indicating the number of info IDs - connectionlogger.severe(toString() + "FEDAUTHINFO token stream length too short for CountOfInfoIDs."); + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + "FEDAUTHINFO token stream length too short for CountOfInfoIDs."); + } throw new SQLServerException(SQLServerException.getErrString("R_FedAuthInfoLengthTooShortForCountOfInfoIds"), null); } @@ -3918,7 +4309,9 @@ final void processFedAuthInfo(TDSReader tdsReader, // if dataOffset points to a region within FedAuthInfoOpt or after the end of the token, throw if (dataOffset < totalOptionsSize || dataOffset >= tokenLen) { - connectionlogger.severe(toString() + "FedAuthInfoDataOffset points to an invalid location."); + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + "FedAuthInfoDataOffset points to an invalid location."); + } MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_FedAuthInfoInvalidOffset")); throw new SQLServerException(form.format(new Object[] {dataOffset}), null); } @@ -3932,7 +4325,7 @@ final void processFedAuthInfo(TDSReader tdsReader, } catch (Exception e) { connectionlogger.severe(toString() + "Failed to read FedAuthInfoData."); - throw new SQLServerException(SQLServerException.getErrString("R_FedAuthInfoFailedToReadData"), null); + throw new SQLServerException(SQLServerException.getErrString("R_FedAuthInfoFailedToReadData"), e); } if (connectionlogger.isLoggable(Level.FINER)) { @@ -3956,7 +4349,9 @@ final void processFedAuthInfo(TDSReader tdsReader, } } else { - connectionlogger.severe(toString() + "FEDAUTHINFO token stream is not long enough to contain the data it claims to."); + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + "FEDAUTHINFO token stream is not long enough to contain the data it claims to."); + } MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_FedAuthInfoLengthTooShortForData")); throw new SQLServerException(form.format(new Object[] {tokenLen}), null); } @@ -3964,7 +4359,9 @@ final void processFedAuthInfo(TDSReader tdsReader, if (null == sqlFedAuthInfo.spn || null == sqlFedAuthInfo.stsurl || sqlFedAuthInfo.spn.trim().isEmpty() || sqlFedAuthInfo.stsurl.trim().isEmpty()) { // We should be receiving both stsurl and spn - connectionlogger.severe(toString() + "FEDAUTHINFO token stream does not contain both STSURL and SPN."); + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + "FEDAUTHINFO token stream does not contain both STSURL and SPN."); + } throw new SQLServerException(SQLServerException.getErrString("R_FedAuthInfoDoesNotContainStsurlAndSpn"), null); } @@ -4093,7 +4490,8 @@ else if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDi Thread.sleep(sleepInterval); } catch (InterruptedException e1) { - // continue if the thread is interrupted. This really should not happen. + // re-interrupt the current thread, in order to restore the thread's interrupt status. + Thread.currentThread().interrupt(); } sleepInterval = sleepInterval * 2; } @@ -4171,9 +4569,10 @@ private void onFeatureExtAck(int featureId, if (connectionlogger.isLoggable(Level.FINER)) { connectionlogger.fine(toString() + " Received feature extension acknowledgement for federated authentication."); } - - if (!fedAuth.federatedAuthenticationRequested) { - connectionlogger.severe(toString() + " Did not request federated authentication."); + if (!federatedAuthenticationRequested) { + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + " Did not request federated authentication."); + } MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_UnrequestedFeatureAckReceived")); Object[] msgArgs = {featureId}; throw new SQLServerException(form.format(msgArgs), null); @@ -4187,15 +4586,19 @@ private void onFeatureExtAck(int featureId, case TDS.TDS_FEDAUTH_LIBRARY_SECURITYTOKEN: // The server shouldn't have sent any additional data with the ack (like a nonce) if (0 != data.length) { - connectionlogger.severe( - toString() + " Federated authentication feature extension ack for ADAL and Security Token includes extra data."); + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + + " Federated authentication feature extension ack for ADAL and Security Token includes extra data."); + } throw new SQLServerException(SQLServerException.getErrString("R_FedAuthFeatureAckContainsExtraData"), null); } break; default: assert false; // Unknown _fedAuthLibrary type - connectionlogger.severe(toString() + " Attempting to use unknown federated authentication library."); + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + " Attempting to use unknown federated authentication library."); + } MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_FedAuthFeatureAckUnknownLibraryType")); Object[] msgArgs = {fedAuth.fedAuthFeatureExtensionData.libraryType}; throw new SQLServerException(form.format(msgArgs), null); @@ -4210,13 +4613,17 @@ private void onFeatureExtAck(int featureId, } if (1 > data.length) { - connectionlogger.severe(toString() + " Unknown version number for AE."); + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + " Unknown version number for AE."); + } throw new SQLServerException(SQLServerException.getErrString("R_InvalidAEVersionNumber"), null); } byte supportedTceVersion = data[0]; if (0 == supportedTceVersion || supportedTceVersion > TDS.MAX_SUPPORTED_TCE_VERSION) { - connectionlogger.severe(toString() + " Invalid version number for AE."); + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + " Invalid version number for AE."); + } throw new SQLServerException(SQLServerException.getErrString("R_InvalidAEVersionNumber"), null); } @@ -4227,7 +4634,9 @@ private void onFeatureExtAck(int featureId, default: { // Unknown feature ack - connectionlogger.severe(toString() + " Unknown feature ack."); + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe(toString() + " Unknown feature ack."); + } throw new SQLServerException(SQLServerException.getErrString("R_UnknownFeatureAck"), null); } } @@ -4497,14 +4906,12 @@ final boolean complete(LogonCommand logonCommand, // Fed Auth feature requested without specifying fedAuthFeatureExtensionData. assert (null != fedAuthFeatureExtensionData || !(fedAuth.federatedAuthenticationInfoRequested || fedAuth.federatedAuthenticationRequested)); - String hostName = activeConnectionProperties.getProperty(SQLServerDriverStringProperty.WORKSTATION_ID.toString()); String sUser = activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString()); String sPwd = activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString()); String appName = activeConnectionProperties.getProperty(SQLServerDriverStringProperty.APPLICATION_NAME.toString()); String interfaceLibName = "Microsoft JDBC Driver " + SQLJdbcVersion.major + "." + SQLJdbcVersion.minor; - String interfaceLibVersion = generateInterfaceLibVersion(); String databaseName = activeConnectionProperties.getProperty(SQLServerDriverStringProperty.DATABASE_NAME.toString()); - String serverName = null; + String serverName; // currentConnectPlaceHolder should not be null here. Still doing the check for extra security. if (null != currentConnectPlaceHolder) { serverName = currentConnectPlaceHolder.getServerName(); @@ -4516,10 +4923,6 @@ final boolean complete(LogonCommand logonCommand, if (serverName != null && serverName.length() > 128) serverName = serverName.substring(0, 128); - if (hostName == null || hostName.length() == 0) { - hostName = Util.lookupHostName(); - } - byte[] secBlob = new byte[0]; boolean[] done = {false}; if (null != authentication) { @@ -4539,10 +4942,10 @@ final boolean complete(LogonCommand logonCommand, byte appNameBytes[] = toUCS16(appName); byte serverNameBytes[] = toUCS16(serverName); byte interfaceLibNameBytes[] = toUCS16(interfaceLibName); - byte interfaceLibVersionBytes[] = DatatypeConverter.parseHexBinary(interfaceLibVersion); + byte interfaceLibVersionBytes[] = {(byte) SQLJdbcVersion.build, (byte) SQLJdbcVersion.patch, (byte) SQLJdbcVersion.minor, + (byte) SQLJdbcVersion.major}; byte databaseNameBytes[] = toUCS16(databaseName); byte netAddress[] = new byte[6]; - int len2 = 0; int dataLen = 0; final int TDS_LOGIN_REQUEST_BASE_LEN = 94; @@ -4575,7 +4978,7 @@ else if (serverMajorVersion >= 9) // Yukon (9.0) --> TDS 7.2 // Prelogin disconn TDSWriter tdsWriter = logonCommand.startRequest(TDS.PKT_LOGON70); - len2 = (int) (TDS_LOGIN_REQUEST_BASE_LEN + hostnameBytes.length + appNameBytes.length + serverNameBytes.length + interfaceLibNameBytes.length + int len2 = (int) (TDS_LOGIN_REQUEST_BASE_LEN + hostnameBytes.length + appNameBytes.length + serverNameBytes.length + interfaceLibNameBytes.length + databaseNameBytes.length + secBlob.length /* + featureExtLength */ + IB_FEATURE_EXT_LENGTH /* +4 */);// AE is always on; // only add lengths of password and username if not using SSPI or requesting federated authentication info @@ -4627,7 +5030,7 @@ else if (serverMajorVersion >= 9) // Yukon (9.0) --> TDS 7.2 // Prelogin disconn ? TDS.LOGIN_READ_ONLY_INTENT : TDS.LOGIN_READ_WRITE_INTENT))); // OptionFlags3 - byte colEncSetting = 0x00; + byte colEncSetting; // AE is always ON { colEncSetting = TDS.LOGIN_OPTION3_FEATURE_EXTENSION; @@ -4786,6 +5189,7 @@ else if (serverMajorVersion >= 9) // Yukon (9.0) --> TDS 7.2 // Prelogin disconn LogonProcessor logonProcessor = new LogonProcessor(authentication, fedAuth); TDSReader tdsReader = null; + do { tdsReader = logonCommand.startResponse(); connectionRecoveryPossible = false; // This is set to false to verify that sessionRecoveryFeatureExtension is received while reconnecting. @@ -4810,52 +5214,6 @@ else if (serverMajorVersion >= 9) // Yukon (9.0) --> TDS 7.2 // Prelogin disconn } } - private String generateInterfaceLibVersion() { - - StringBuffer outputInterfaceLibVersion = new StringBuffer(); - - String interfaceLibMajor = Integer.toHexString(SQLJdbcVersion.major); - String interfaceLibMinor = Integer.toHexString(SQLJdbcVersion.minor); - String interfaceLibPatch = Integer.toHexString(SQLJdbcVersion.patch); - String interfaceLibBuild = Integer.toHexString(SQLJdbcVersion.build); - - // build the interface lib name - // 2 characters reserved for build - // 2 characters reserved for patch - // 2 characters reserved for minor - // 2 characters reserved for major - if (2 == interfaceLibBuild.length()) { - outputInterfaceLibVersion.append(interfaceLibBuild); - } - else { - outputInterfaceLibVersion.append("0"); - outputInterfaceLibVersion.append(interfaceLibBuild); - } - if (2 == interfaceLibPatch.length()) { - outputInterfaceLibVersion.append(interfaceLibPatch); - } - else { - outputInterfaceLibVersion.append("0"); - outputInterfaceLibVersion.append(interfaceLibPatch); - } - if (2 == interfaceLibMinor.length()) { - outputInterfaceLibVersion.append(interfaceLibMinor); - } - else { - outputInterfaceLibVersion.append("0"); - outputInterfaceLibVersion.append(interfaceLibMinor); - } - if (2 == interfaceLibMajor.length()) { - outputInterfaceLibVersion.append(interfaceLibMajor); - } - else { - outputInterfaceLibVersion.append("0"); - outputInterfaceLibVersion.append(interfaceLibMajor); - } - - return outputInterfaceLibVersion.toString(); - } - /* --------------- JDBC 3.0 ------------- */ /** @@ -4886,7 +5244,7 @@ public Statement createStatement(int nType, int nConcur, int resultSetHoldability) throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "createStatement", - new Object[] {new Integer(nType), new Integer(nConcur), resultSetHoldability}); + new Object[] {nType, nConcur, resultSetHoldability}); Statement st = createStatement(nType, nConcur, resultSetHoldability, SQLServerStatementColumnEncryptionSetting.UseConnectionSetting); loggerExternal.exiting(getClassNameLogging(), "createStatement", st); return st; @@ -4897,7 +5255,7 @@ public Statement createStatement(int nType, int resultSetHoldability, SQLServerStatementColumnEncryptionSetting stmtColEncSetting) throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "createStatement", - new Object[] {new Integer(nType), new Integer(nConcur), resultSetHoldability, stmtColEncSetting}); + new Object[] {nType, nConcur, resultSetHoldability, stmtColEncSetting}); checkClosed(); checkValidHoldability(resultSetHoldability); checkMatchesCurrentHoldability(resultSetHoldability); @@ -4911,7 +5269,7 @@ public Statement createStatement(int nType, int nConcur, int resultSetHoldability) throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "prepareStatement", - new Object[] {new Integer(nType), new Integer(nConcur), resultSetHoldability}); + new Object[] {nType, nConcur, resultSetHoldability}); PreparedStatement st = prepareStatement(sql, nType, nConcur, resultSetHoldability, SQLServerStatementColumnEncryptionSetting.UseConnectionSetting); loggerExternal.exiting(getClassNameLogging(), "prepareStatement", st); @@ -4950,12 +5308,12 @@ public PreparedStatement prepareStatement(java.lang.String sql, int resultSetHoldability, SQLServerStatementColumnEncryptionSetting stmtColEncSetting) throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "prepareStatement", - new Object[] {new Integer(nType), new Integer(nConcur), resultSetHoldability, stmtColEncSetting}); + new Object[] {nType, nConcur, resultSetHoldability, stmtColEncSetting}); checkClosed(); checkValidHoldability(resultSetHoldability); checkMatchesCurrentHoldability(resultSetHoldability); - PreparedStatement st = null; + PreparedStatement st; if (Util.use42Wrapper()) { st = new SQLServerPreparedStatement42(this, sql, nType, nConcur, stmtColEncSetting); @@ -4973,7 +5331,7 @@ public PreparedStatement prepareStatement(java.lang.String sql, int nConcur, int resultSetHoldability) throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "prepareStatement", - new Object[] {new Integer(nType), new Integer(nConcur), resultSetHoldability}); + new Object[] {nType, nConcur, resultSetHoldability}); CallableStatement st = prepareCall(sql, nType, nConcur, resultSetHoldability, SQLServerStatementColumnEncryptionSetting.UseConnectionSetting); loggerExternal.exiting(getClassNameLogging(), "prepareCall", st); return st; @@ -4985,12 +5343,12 @@ public CallableStatement prepareCall(String sql, int resultSetHoldability, SQLServerStatementColumnEncryptionSetting stmtColEncSetiing) throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "prepareStatement", - new Object[] {new Integer(nType), new Integer(nConcur), resultSetHoldability, stmtColEncSetiing}); + new Object[] {nType, nConcur, resultSetHoldability, stmtColEncSetiing}); checkClosed(); checkValidHoldability(resultSetHoldability); checkMatchesCurrentHoldability(resultSetHoldability); - CallableStatement st = null; + CallableStatement st; if (Util.use42Wrapper()) { st = new SQLServerCallableStatement42(this, sql, nType, nConcur, stmtColEncSetiing); @@ -5007,7 +5365,7 @@ public CallableStatement prepareCall(String sql, /* L3 */ public PreparedStatement prepareStatement(String sql, int flag) throws SQLServerException { - loggerExternal.entering(getClassNameLogging(), "prepareStatement", new Object[] {sql, new Integer(flag)}); + loggerExternal.entering(getClassNameLogging(), "prepareStatement", new Object[] {sql, flag}); SQLServerPreparedStatement ps = (SQLServerPreparedStatement) prepareStatement(sql, flag, SQLServerStatementColumnEncryptionSetting.UseConnectionSetting); @@ -5046,7 +5404,7 @@ public CallableStatement prepareCall(String sql, public PreparedStatement prepareStatement(String sql, int flag, SQLServerStatementColumnEncryptionSetting stmtColEncSetting) throws SQLServerException { - loggerExternal.entering(getClassNameLogging(), "prepareStatement", new Object[] {sql, new Integer(flag), stmtColEncSetting}); + loggerExternal.entering(getClassNameLogging(), "prepareStatement", new Object[] {sql, flag, stmtColEncSetting}); checkClosed(); SQLServerPreparedStatement ps = (SQLServerPreparedStatement) prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, stmtColEncSetting); @@ -5257,25 +5615,61 @@ public void setHoldability(int holdability) throws SQLServerException { } public int getNetworkTimeout() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC41(); + loggerExternal.entering(getClassNameLogging(), "getNetworkTimeout"); - // this operation is not supported - throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); + checkClosed(); + + int timeout = 0; + try { + timeout = tdsChannel.getNetworkTimeout(); + } + catch (IOException ioe) { + terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, ioe.getMessage(), ioe); + } + + loggerExternal.exiting(getClassNameLogging(), "getNetworkTimeout"); + return timeout; } public void setNetworkTimeout(Executor executor, int timeout) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC41(); + loggerExternal.entering(getClassNameLogging(), "setNetworkTimeout", timeout); - // this operation is not supported - throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); + if (timeout < 0) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidSocketTimeout")); + Object[] msgArgs = {timeout}; + SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, false); + } + + checkClosed(); + + // check for setNetworkTimeout permission + SecurityManager secMgr = System.getSecurityManager(); + if (secMgr != null) { + try { + SQLPermission perm = new SQLPermission(SET_NETWORK_TIMEOUT_PERM); + secMgr.checkPermission(perm); + } + catch (SecurityException ex) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_permissionDenied")); + Object[] msgArgs = {SET_NETWORK_TIMEOUT_PERM}; + SQLServerException.makeFromDriverError(this, this, form.format(msgArgs), null, true); + } + } + + try { + tdsChannel.setNetworkTimeout(timeout); + } + catch (IOException ioe) { + terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, ioe.getMessage(), ioe); + } + + loggerExternal.exiting(getClassNameLogging(), "setNetworkTimeout"); } public String getSchema() throws SQLException { loggerExternal.entering(getClassNameLogging(), "getSchema"); - DriverJDBCVersion.checkSupportsJDBC41(); - checkClosed(); SQLServerStatement stmt = null; @@ -5314,8 +5708,6 @@ public String getSchema() throws SQLException { public void setSchema(String schema) throws SQLException { loggerExternal.entering(getClassNameLogging(), "setSchema", schema); - DriverJDBCVersion.checkSupportsJDBC41(); - checkClosed(); addWarning(SQLServerException.getErrString("R_setSchemaWarning")); @@ -5339,35 +5731,28 @@ public synchronized void setSendTimeAsDatetime(boolean sendTimeAsDateTimeValue) public java.sql.Array createArrayOf(String typeName, Object[] elements) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - // Not implemented throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); } public Blob createBlob() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); return new SQLServerBlob(this); } public Clob createClob() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); return new SQLServerClob(this); } public NClob createNClob() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); return new SQLServerNClob(this); } public SQLXML createSQLXML() throws SQLException { loggerExternal.entering(getClassNameLogging(), "createSQLXML"); - DriverJDBCVersion.checkSupportsJDBC4(); - SQLXML sqlxml = null; - sqlxml = new SQLServerSQLXML(this); + SQLXML sqlxml = new SQLServerSQLXML(this); if (loggerExternal.isLoggable(Level.FINER)) loggerExternal.exiting(getClassNameLogging(), "createSQLXML", sqlxml); @@ -5376,8 +5761,6 @@ public SQLXML createSQLXML() throws SQLException { public Struct createStruct(String typeName, Object[] attributes) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - // Not implemented throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); } @@ -5387,7 +5770,6 @@ String getTrustedServerNameAE() throws SQLServerException { } public Properties getClientInfo() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "getClientInfo"); checkClosed(); Properties p = new Properties(); @@ -5396,7 +5778,6 @@ public Properties getClientInfo() throws SQLException { } public String getClientInfo(String name) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "getClientInfo", name); checkClosed(); loggerExternal.exiting(getClassNameLogging(), "getClientInfo", null); @@ -5404,7 +5785,6 @@ public String getClientInfo(String name) throws SQLException { } public void setClientInfo(Properties properties) throws SQLClientInfoException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "setClientInfo", properties); // This function is only marked as throwing only SQLClientInfoException so the conversion is necessary try { @@ -5429,7 +5809,6 @@ public void setClientInfo(Properties properties) throws SQLClientInfoException { public void setClientInfo(String name, String value) throws SQLClientInfoException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "setClientInfo", new Object[] {name, value}); // This function is only marked as throwing only SQLClientInfoException so the conversion is necessary try { @@ -5469,8 +5848,6 @@ public boolean isValid(int timeout) throws SQLException { loggerExternal.entering(getClassNameLogging(), "isValid", timeout); - DriverJDBCVersion.checkSupportsJDBC4(); - // Throw an exception if the timeout is invalid if (timeout < 0) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidQueryTimeOutValue")); @@ -5511,15 +5888,13 @@ public boolean isValid(int timeout) throws SQLException { public boolean isWrapperFor(Class iface) throws SQLException { loggerExternal.entering(getClassNameLogging(), "isWrapperFor", iface); - DriverJDBCVersion.checkSupportsJDBC4(); boolean f = iface.isInstance(this); - loggerExternal.exiting(getClassNameLogging(), "isWrapperFor", Boolean.valueOf(f)); + loggerExternal.exiting(getClassNameLogging(), "isWrapperFor", f); return f; } public T unwrap(Class iface) throws SQLException { loggerExternal.entering(getClassNameLogging(), "unwrap", iface); - DriverJDBCVersion.checkSupportsJDBC4(); T t; try { t = iface.cast(this); @@ -5660,17 +6035,15 @@ String getInstancePort(String server, lastErrorMessage = "Failed to determine instance for the : " + server + " instance:" + instanceName; // First we create a datagram socket - if (null == datagramSocket) { - try { - datagramSocket = new DatagramSocket(); - datagramSocket.setSoTimeout(1000); - } - catch (SocketException socketException) { - // Errors creating a local socket - // Log the error and bail. - lastErrorMessage = "Unable to create local datagram socket"; - throw socketException; - } + try { + datagramSocket = new DatagramSocket(); + datagramSocket.setSoTimeout(1000); + } + catch (SocketException socketException) { + // Errors creating a local socket + // Log the error and bail. + lastErrorMessage = "Unable to create local datagram socket"; + throw socketException; } // Second, we need to get the IP address of the server to which we'll send the UDP request. @@ -5729,7 +6102,7 @@ String getInstancePort(String server, browserResult = new String(receiveBuffer, 3, receiveBuffer.length - 3); if (connectionlogger.isLoggable(Level.FINER)) connectionlogger.fine( - toString() + " Received SSRP UDP response from IP address: " + udpResponse.getAddress().getHostAddress().toString()); + toString() + " Received SSRP UDP response from IP address: " + udpResponse.getAddress().getHostAddress()); } catch (IOException ioException) { // Warn and retry @@ -5804,7 +6177,275 @@ public static synchronized void setColumnEncryptionKeyCacheTtl(int columnEncrypt static synchronized long getColumnEncryptionKeyCacheTtl() { return columnEncryptionKeyCacheTtl; } + + + /** + * Enqueue a discarded prepared statement handle to be clean-up on the server. + * + * @param statementHandle + * The prepared statement handle that should be scheduled for unprepare. + */ + final void enqueueUnprepareStatementHandle(PreparedStatementHandle statementHandle) { + if(null == statementHandle) + return; + + if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) + loggerExternal.finer(this + ": Adding PreparedHandle to queue for un-prepare:" + statementHandle.getHandle()); + + // Add the new handle to the discarding queue and find out current # enqueued. + this.discardedPreparedStatementHandles.add(statementHandle); + this.discardedPreparedStatementHandleCount.incrementAndGet(); + } + + + /** + * Returns the number of currently outstanding prepared statement un-prepare actions. + * + * @return Returns the current value per the description. + */ + public int getDiscardedServerPreparedStatementCount() { + return this.discardedPreparedStatementHandleCount.get(); + } + + /** + * Forces the un-prepare requests for any outstanding discarded prepared statements to be executed. + */ + public void closeUnreferencedPreparedStatementHandles() { + this.unprepareUnreferencedPreparedStatementHandles(true); + } + + /** + * Remove references to outstanding un-prepare requests. Should be run when connection is closed. + */ + private final void cleanupPreparedStatementDiscardActions() { + discardedPreparedStatementHandles.clear(); + discardedPreparedStatementHandleCount.set(0); + } + + /** + * Returns the behavior for a specific connection instance. If false the first execution will call sp_executesql and not prepare + * a statement, once the second execution happens it will call sp_prepexec and actually setup a prepared statement handle. Following + * executions will call sp_execute. This relieves the need for sp_unprepare on prepared statement close if the statement is only + * executed once. The default for this option can be changed by calling setDefaultEnablePrepareOnFirstPreparedStatementCall(). + * + * @return Returns the current setting per the description. + */ + public boolean getEnablePrepareOnFirstPreparedStatementCall() { + if(null == this.enablePrepareOnFirstPreparedStatementCall) + return DEFAULT_ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT_CALL; + else + return this.enablePrepareOnFirstPreparedStatementCall; + } + + /** + * Specifies the behavior for a specific connection instance. If value is false the first execution will call sp_executesql and not prepare + * a statement, once the second execution happens it will call sp_prepexec and actually setup a prepared statement handle. Following + * executions will call sp_execute. This relieves the need for sp_unprepare on prepared statement close if the statement is only + * executed once. + * + * @param value + * Changes the setting per the description. + */ + public void setEnablePrepareOnFirstPreparedStatementCall(boolean value) { + this.enablePrepareOnFirstPreparedStatementCall = value; + } + + /** + * Returns the behavior for a specific connection instance. This setting controls how many outstanding prepared statement discard actions + * (sp_unprepare) can be outstanding per connection before a call to clean-up the outstanding handles on the server is executed. If the setting is + * {@literal <=} 1, unprepare actions will be executed immedietely on prepared statement close. If it is set to {@literal >} 1, these calls + * will be batched together to avoid overhead of calling sp_unprepare too often. The default for this option can be changed by calling + * getDefaultServerPreparedStatementDiscardThreshold(). + * + * @return Returns the current setting per the description. + */ + public int getServerPreparedStatementDiscardThreshold() { + if (0 > this.serverPreparedStatementDiscardThreshold) + return DEFAULT_SERVER_PREPARED_STATEMENT_DISCARD_THRESHOLD; + else + return this.serverPreparedStatementDiscardThreshold; + } + + /** + * Specifies the behavior for a specific connection instance. This setting controls how many outstanding prepared statement discard actions + * (sp_unprepare) can be outstanding per connection before a call to clean-up the outstanding handles on the server is executed. If the setting is + * {@literal <=} 1 unprepare actions will be executed immedietely on prepared statement close. If it is set to {@literal >} 1 these calls will be + * batched together to avoid overhead of calling sp_unprepare too often. + * + * @param value + * Changes the setting per the description. + */ + public void setServerPreparedStatementDiscardThreshold(int value) { + this.serverPreparedStatementDiscardThreshold = Math.max(0, value); + } + + final boolean isPreparedStatementUnprepareBatchingEnabled() { + return 1 < getServerPreparedStatementDiscardThreshold(); + } + /** + * Cleans-up discarded prepared statement handles on the server using batched un-prepare actions if the batching threshold has been reached. + * + * @param force + * When force is set to true we ignore the current threshold for if the discard actions should run and run them anyway. + */ + final void unprepareUnreferencedPreparedStatementHandles(boolean force) { + // Skip out if session is unavailable to adhere to previous non-batched behavior. + if (isSessionUnAvailable()) + return; + + final int threshold = getServerPreparedStatementDiscardThreshold(); + + // Met threshold to clean-up? + if (force || threshold < getDiscardedServerPreparedStatementCount()) { + + // Create batch of sp_unprepare statements. + StringBuilder sql = new StringBuilder(threshold * 32/*EXEC sp_cursorunprepare++;*/); + + // Build the string containing no more than the # of handles to remove. + // Note that sp_unprepare can fail if the statement is already removed. + // However, the server will only abort that statement and continue with + // the remaining clean-up. + int handlesRemoved = 0; + PreparedStatementHandle statementHandle = null; + + while (null != (statementHandle = discardedPreparedStatementHandles.poll())){ + ++handlesRemoved; + + sql.append(statementHandle.isDirectSql() ? "EXEC sp_unprepare " : "EXEC sp_cursorunprepare ") + .append(statementHandle.getHandle()) + .append(';'); + } + + try { + // Execute the batched set. + try(Statement stmt = this.createStatement()) { + stmt.execute(sql.toString()); + } + + if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) + loggerExternal.finer(this + ": Finished un-preparing handle count:" + handlesRemoved); + } + catch(SQLException e) { + if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) + loggerExternal.log(Level.FINER, this + ": Error batch-closing at least one prepared handle", e); + } + + // Decrement threshold counter + discardedPreparedStatementHandleCount.addAndGet(-handlesRemoved); + } + } + + + /** + * Returns the size of the prepared statement cache for this connection. A value less than 1 means no cache. + * @return Returns the current setting per the description. + */ + public int getStatementPoolingCacheSize() { + return statementPoolingCacheSize; + } + + /** + * Returns the current number of pooled prepared statement handles. + * @return Returns the current setting per the description. + */ + public int getStatementHandleCacheEntryCount() { + if(!isStatementPoolingEnabled()) + return 0; + else + return this.preparedStatementHandleCache.size(); + } + + /** + * Whether statement pooling is enabled or not for this connection. + * @return Returns the current setting per the description. + */ + public boolean isStatementPoolingEnabled() { + return null != preparedStatementHandleCache && 0 < this.getStatementPoolingCacheSize(); + } + + /** + * Specifies the size of the prepared statement cache for this conection. A value less than 1 means no cache. + * @param value The new cache size. + * + */ + public void setStatementPoolingCacheSize(int value) { + if (value != this.statementPoolingCacheSize) { + value = Math.max(0, value); + statementPoolingCacheSize = value; + + if (null != preparedStatementHandleCache) + preparedStatementHandleCache.setCapacity(value); + + if (null != parameterMetadataCache) + parameterMetadataCache.setCapacity(value); + } + } + + /** Get a parameter metadata cache entry if statement pooling is enabled */ + final SQLServerParameterMetaData getCachedParameterMetadata(Sha1HashKey key) { + if(!isStatementPoolingEnabled()) + return null; + + return parameterMetadataCache.get(key); + } + + /** Register a parameter metadata cache entry if statement pooling is enabled */ + final void registerCachedParameterMetadata(Sha1HashKey key, SQLServerParameterMetaData pmd) { + if(!isStatementPoolingEnabled() || null == pmd) + return; + + parameterMetadataCache.put(key, pmd); + } + + /** Get or create prepared statement handle cache entry if statement pooling is enabled */ + final PreparedStatementHandle getCachedPreparedStatementHandle(Sha1HashKey key) { + if(!isStatementPoolingEnabled()) + return null; + + return preparedStatementHandleCache.get(key); + } + + /** Get or create prepared statement handle cache entry if statement pooling is enabled */ + final PreparedStatementHandle registerCachedPreparedStatementHandle(Sha1HashKey key, int handle, boolean isDirectSql) { + if(!isStatementPoolingEnabled() || null == key) + return null; + + PreparedStatementHandle cacheItem = new PreparedStatementHandle(key, handle, isDirectSql, false); + preparedStatementHandleCache.putIfAbsent(key, cacheItem); + return cacheItem; + } + + /** Return prepared statement handle cache entry so it can be un-prepared. */ + final void returnCachedPreparedStatementHandle(PreparedStatementHandle handle) { + handle.removeReference(); + + if (handle.isEvictedFromCache() && handle.tryDiscardHandle()) + enqueueUnprepareStatementHandle(handle); + } + + /** Force eviction of prepared statement handle cache entry. */ + final void evictCachedPreparedStatementHandle(PreparedStatementHandle handle) { + if(null == handle || null == handle.getKey()) + return; + + preparedStatementHandleCache.remove(handle.getKey()); + } + + // Handle closing handles when removed from cache. + final class PreparedStatementCacheEvictionListener implements EvictionListener { + public void onEviction(Sha1HashKey key, PreparedStatementHandle handle) { + if(null != handle) { + handle.setIsEvictedFromCache(true); // Mark as evicted from cache. + + // Only discard if not referenced. + if(handle.tryDiscardHandle()) { + enqueueUnprepareStatementHandle(handle); + // Do not run discard actions here! Can interfere with executing statement. + } + } + } + } } /** diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java index 895d84c77..b180a11c9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java @@ -32,7 +32,7 @@ /** * SQLServerConnectionPoolProxy is a wrapper around SQLServerConnection object. When returning a connection object from PooledConnection.getConnection - * we returnt this proxy per SPEC. + * we return this proxy per SPEC. *

    * This class's public functions need to be kept identical to the SQLServerConnection's. *

    @@ -40,8 +40,7 @@ * details. */ -class SQLServerConnectionPoolProxy implements ISQLServerConnection, java.io.Serializable { - private static final long serialVersionUID = -6412542417798843534L; +class SQLServerConnectionPoolProxy implements ISQLServerConnection { private SQLServerConnection wrappedConnection; private boolean bIsOpen; static private final AtomicInteger baseConnectionID = new AtomicInteger(0); // connection id dispenser @@ -114,7 +113,7 @@ public void commit() throws SQLServerException { } /** - * Rollback a transcation. + * Rollback a transaction. * * @throws SQLServerException * if no transaction exists or if the connection is in auto-commit mode. @@ -125,8 +124,6 @@ public void rollback() throws SQLServerException { } public void abort(Executor executor) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC41(); - if (!bIsOpen || (null == wrappedConnection)) return; @@ -557,117 +554,86 @@ public PreparedStatement prepareStatement(String sql, } public int getNetworkTimeout() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC41(); - - // The driver currently does not implement the optional JDBC APIs - throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); + checkClosed(); + return wrappedConnection.getNetworkTimeout(); } public void setNetworkTimeout(Executor executor, int timeout) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC41(); - - // The driver currently does not implement the optional JDBC APIs - throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); + checkClosed(); + wrappedConnection.setNetworkTimeout(executor, timeout); } public String getSchema() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC41(); - checkClosed(); return wrappedConnection.getSchema(); } public void setSchema(String schema) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC41(); - checkClosed(); wrappedConnection.setSchema(schema); } public java.sql.Array createArrayOf(String typeName, Object[] elements) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - checkClosed(); return wrappedConnection.createArrayOf(typeName, elements); } public Blob createBlob() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - checkClosed(); return wrappedConnection.createBlob(); } public Clob createClob() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - checkClosed(); return wrappedConnection.createClob(); } public NClob createNClob() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - checkClosed(); return wrappedConnection.createNClob(); } public SQLXML createSQLXML() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - checkClosed(); return wrappedConnection.createSQLXML(); } public Struct createStruct(String typeName, Object[] attributes) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - checkClosed(); return wrappedConnection.createStruct(typeName, attributes); } public Properties getClientInfo() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - checkClosed(); return wrappedConnection.getClientInfo(); } public String getClientInfo(String name) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - checkClosed(); return wrappedConnection.getClientInfo(name); } public void setClientInfo(Properties properties) throws SQLClientInfoException { - DriverJDBCVersion.checkSupportsJDBC4(); - // No checkClosed() call since we can only throw SQLClientInfoException from here wrappedConnection.setClientInfo(properties); } public void setClientInfo(String name, String value) throws SQLClientInfoException { - DriverJDBCVersion.checkSupportsJDBC4(); - // No checkClosed() call since we can only throw SQLClientInfoException from here wrappedConnection.setClientInfo(name, value); } public boolean isValid(int timeout) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - checkClosed(); return wrappedConnection.isValid(timeout); } public boolean isWrapperFor(Class iface) throws SQLException { wrappedConnection.getConnectionLogger().entering(toString(), "isWrapperFor", iface); - DriverJDBCVersion.checkSupportsJDBC4(); boolean f = iface.isInstance(this); wrappedConnection.getConnectionLogger().exiting(toString(), "isWrapperFor", f); return f; @@ -675,8 +641,6 @@ public boolean isWrapperFor(Class iface) throws SQLException { public T unwrap(Class iface) throws SQLException { wrappedConnection.getConnectionLogger().entering(toString(), "unwrap", iface); - DriverJDBCVersion.checkSupportsJDBC4(); - T t; try { t = iface.cast(this); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataColumn.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataColumn.java index 74cffe6d0..7877cf3ab 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataColumn.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataColumn.java @@ -16,6 +16,7 @@ public final class SQLServerDataColumn { int javaSqlType; int precision = 0; int scale = 0; + int numberOfDigitsIntegerPart = 0; /** * Initializes a new instance of SQLServerDataColumn with the column name and type. diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java index 3b50364a6..84b2c9cec 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java @@ -111,8 +111,6 @@ public PrintWriter getLogWriter() { } public Logger getParentLogger() throws SQLFeatureNotSupportedException { - DriverJDBCVersion.checkSupportsJDBC41(); - return parentLogger; } @@ -180,7 +178,7 @@ public String getAuthentication() { /** * sets GSSCredential * - * @param userCredential + * @param userCredential the credential */ public void setGSSCredentials(GSSCredential userCredential){ setObjectProperty(connectionProps,SQLServerDriverObjectProperty.GSS_CREDENTIAL.toString(), userCredential); @@ -587,13 +585,32 @@ public boolean getFIPS() { return getBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.FIPS.toString(), SQLServerDriverBooleanProperty.FIPS.getDefaultValue()); } + + public void setSSLProtocol(String sslProtocol) { + setStringProperty(connectionProps, SQLServerDriverStringProperty.SSL_PROTOCOL.toString(), sslProtocol); + } + + public String getSSLProtocol() { + return getStringProperty(connectionProps, SQLServerDriverStringProperty.SSL_PROTOCOL.toString(), + SQLServerDriverStringProperty.SSL_PROTOCOL.getDefaultValue()); + } + + public void setTrustManagerClass(String trustManagerClass) { + setStringProperty(connectionProps, SQLServerDriverStringProperty.TRUST_MANAGER_CLASS.toString(), trustManagerClass); + } + + public String getTrustManagerClass() { + return getStringProperty(connectionProps, SQLServerDriverStringProperty.TRUST_MANAGER_CLASS.toString(), + SQLServerDriverStringProperty.TRUST_MANAGER_CLASS.getDefaultValue()); + } - public void setFIPSProvider(String fipsProvider) { - setStringProperty(connectionProps, SQLServerDriverStringProperty.FIPS_PROVIDER.toString(), fipsProvider); + public void setTrustManagerConstructorArg(String trustManagerClass) { + setStringProperty(connectionProps, SQLServerDriverStringProperty.TRUST_MANAGER_CONSTRUCTOR_ARG.toString(), trustManagerClass); } - public String getFIPSProvider() { - return getStringProperty(connectionProps, SQLServerDriverStringProperty.FIPS_PROVIDER.toString(), null); + public String getTrustManagerConstructorArg() { + return getStringProperty(connectionProps, SQLServerDriverStringProperty.TRUST_MANAGER_CONSTRUCTOR_ARG.toString(), + SQLServerDriverStringProperty.TRUST_MANAGER_CONSTRUCTOR_ARG.getDefaultValue()); } // The URL property is exposed for backwards compatibility reasons. Also, several @@ -674,24 +691,140 @@ public int getConnectRetryInterval() { SQLServerDriverIntProperty.CONNECT_RETRY_INTERVAL.getDefaultValue()); } + /** + * Setting the query timeout + * + * @param queryTimeout + * The number of seconds to wait before a timeout has occurred on a query. The default value is 0, which means infinite timeout. + */ public void setQueryTimeout(int queryTimeout) { setIntProperty(connectionProps, SQLServerDriverIntProperty.QUERY_TIMEOUT.toString(), queryTimeout); } + /** + * Getting the query timeout + * + * @return The number of seconds to wait before a timeout has occurred on a query. + */ public int getQueryTimeout() { return getIntProperty(connectionProps, SQLServerDriverIntProperty.QUERY_TIMEOUT.toString(), SQLServerDriverIntProperty.QUERY_TIMEOUT.getDefaultValue()); } + /** + * If this configuration is false the first execution of a prepared statement will call sp_executesql and not prepare + * a statement, once the second execution happens it will call sp_prepexec and actually setup a prepared statement handle. Following + * executions will call sp_execute. This relieves the need for sp_unprepare on prepared statement close if the statement is only + * executed once. + * + * @param enablePrepareOnFirstPreparedStatementCall + * Changes the setting per the description. + */ + public void setEnablePrepareOnFirstPreparedStatementCall(boolean enablePrepareOnFirstPreparedStatementCall) { + setBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT.toString(), enablePrepareOnFirstPreparedStatementCall); + } + + /** + * If this configuration returns false the first execution of a prepared statement will call sp_executesql and not prepare a statement, once the + * second execution happens it will call sp_prepexec and actually setup a prepared statement handle. Following executions will call sp_execute. + * This relieves the need for sp_unprepare on prepared statement close if the statement is only executed once. + * + * @return Returns the current setting per the description. + */ + public boolean getEnablePrepareOnFirstPreparedStatementCall() { + boolean defaultValue = SQLServerDriverBooleanProperty.ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT.getDefaultValue(); + return getBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT.toString(), + defaultValue); + } + + /** + * This setting controls how many outstanding prepared statement discard actions (sp_unprepare) can be outstanding per connection before a call to + * clean-up the outstanding handles on the server is executed. If the setting is {@literal <=} 1 unprepare actions will be executed immedietely on + * prepared statement close. If it is set to {@literal >} 1 these calls will be batched together to avoid overhead of calling sp_unprepare too + * often. + * + * @param serverPreparedStatementDiscardThreshold + * Changes the setting per the description. + */ + public void setServerPreparedStatementDiscardThreshold(int serverPreparedStatementDiscardThreshold) { + setIntProperty(connectionProps, SQLServerDriverIntProperty.SERVER_PREPARED_STATEMENT_DISCARD_THRESHOLD.toString(), serverPreparedStatementDiscardThreshold); + } + + /** + * This setting controls how many outstanding prepared statement discard actions (sp_unprepare) can be outstanding per connection before a call to + * clean-up the outstanding handles on the server is executed. If the setting is {@literal <=} 1 unprepare actions will be executed immedietely on + * prepared statement close. If it is set to {@literal >} 1 these calls will be batched together to avoid overhead of calling sp_unprepare too + * often. + * + * @return Returns the current setting per the description. + */ + public int getServerPreparedStatementDiscardThreshold() { + int defaultSize = SQLServerDriverIntProperty.SERVER_PREPARED_STATEMENT_DISCARD_THRESHOLD.getDefaultValue(); + return getIntProperty(connectionProps, SQLServerDriverIntProperty.SERVER_PREPARED_STATEMENT_DISCARD_THRESHOLD.toString(), defaultSize); + } + + /** + * Specifies the size of the prepared statement cache for this conection. A value less than 1 means no cache. + * + * @param statementPoolingCacheSize + * Changes the setting per the description. + */ + public void setStatementPoolingCacheSize(int statementPoolingCacheSize) { + setIntProperty(connectionProps, SQLServerDriverIntProperty.STATEMENT_POOLING_CACHE_SIZE.toString(), statementPoolingCacheSize); + } + + /** + * Returns the size of the prepared statement cache for this conection. A value less than 1 means no cache. + * + * @return Returns the current setting per the description. + */ + public int getStatementPoolingCacheSize() { + int defaultSize = SQLServerDriverIntProperty.STATEMENT_POOLING_CACHE_SIZE.getDefaultValue(); + return getIntProperty(connectionProps, SQLServerDriverIntProperty.STATEMENT_POOLING_CACHE_SIZE.toString(), defaultSize); + } + + /** + * Setting the socket timeout + * + * @param socketTimeout + * The number of milliseconds to wait before a timeout is occurred on a socket read or accept. The default value is 0, which means + * infinite timeout. + */ public void setSocketTimeout(int socketTimeout) { setIntProperty(connectionProps, SQLServerDriverIntProperty.SOCKET_TIMEOUT.toString(), socketTimeout); } + /** + * Getting the socket timeout + * + * @return The number of milliseconds to wait before a timeout is occurred on a socket read or accept. + */ public int getSocketTimeout() { int defaultTimeOut = SQLServerDriverIntProperty.SOCKET_TIMEOUT.getDefaultValue(); return getIntProperty(connectionProps, SQLServerDriverIntProperty.SOCKET_TIMEOUT.toString(), defaultTimeOut); } + /** + * Sets the login configuration file for Kerberos authentication. This + * overrides the default configuration SQLJDBCDriver + * + * @param configurationName the configuration name + */ + public void setJASSConfigurationName(String configurationName) { + setStringProperty(connectionProps, SQLServerDriverStringProperty.JAAS_CONFIG_NAME.toString(), + configurationName); + } + + /** + * Retrieves the login configuration file for Kerberos authentication. + * + * @return login configuration file name + */ + public String getJASSConfigurationName() { + return getStringProperty(connectionProps, SQLServerDriverStringProperty.JAAS_CONFIG_NAME.toString(), + SQLServerDriverStringProperty.JAAS_CONFIG_NAME.getDefaultValue()); + } + // responseBuffering controls the driver's buffering of responses from SQL Server. // Possible values are: // @@ -750,7 +883,7 @@ private void setIntProperty(Properties props, String propKey, int propValue) { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "set" + propKey, new Integer(propValue)); + loggerExternal.entering(getClassNameLogging(), "set" + propKey, propValue); props.setProperty(propKey, new Integer(propValue).toString()); loggerExternal.exiting(getClassNameLogging(), "set" + propKey); } @@ -776,7 +909,7 @@ private int getIntProperty(Properties props, } } if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.exiting(getClassNameLogging(), "get" + propKey, new Integer(value)); + loggerExternal.exiting(getClassNameLogging(), "get" + propKey, value); return value; } @@ -786,7 +919,7 @@ private void setBooleanProperty(Properties props, String propKey, boolean propValue) { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "set" + propKey, Boolean.valueOf(propValue)); + loggerExternal.entering(getClassNameLogging(), "set" + propKey, propValue); props.setProperty(propKey, (propValue) ? "true" : "false"); loggerExternal.exiting(getClassNameLogging(), "set" + propKey); } @@ -810,7 +943,7 @@ private boolean getBooleanProperty(Properties props, value = Boolean.valueOf(propValue); } loggerExternal.exiting(getClassNameLogging(), "get" + propKey, value); - return value.booleanValue(); + return value; } private void setObjectProperty(Properties props, @@ -848,8 +981,8 @@ private Object getObjectProperty(Properties props, SQLServerConnection getConnectionInternal(String username, String password, SQLServerPooledConnection pooledConnection) throws SQLServerException { - Properties userSuppliedProps = null; - Properties mergedProps = null; + Properties userSuppliedProps; + Properties mergedProps; // Trust store password stripped and this object got created via Objectfactory referencing. if (trustStorePasswordStripped) SQLServerException.makeFromDriverError(null, null, SQLServerException.getErrString("R_referencingFailedTSP"), null, true); @@ -955,17 +1088,17 @@ void initializeFromReference(javax.naming.Reference ref) { String propertyValue = (String) addr.getContent(); // Special case dataSourceURL and dataSourceDescription. - if (propertyName.equals("dataSourceURL")) { + if ("dataSourceURL".equals(propertyName)) { dataSourceURL = propertyValue; } - else if (propertyName.equals("dataSourceDescription")) { + else if ("dataSourceDescription".equals(propertyName)) { dataSourceDescription = propertyValue; } - else if (propertyName.equals("trustStorePasswordStripped")) { + else if ("trustStorePasswordStripped".equals(propertyName)) { trustStorePasswordStripped = true; } // Just skip "class" StringRefAddr, it does not go into connectionProps - else if (false == propertyName.equals("class")) { + else if (!"class".equals(propertyName)) { connectionProps.setProperty(propertyName, propertyValue); } @@ -974,16 +1107,13 @@ else if (false == propertyName.equals("class")) { public boolean isWrapperFor(Class iface) throws SQLException { loggerExternal.entering(getClassNameLogging(), "isWrapperFor", iface); - DriverJDBCVersion.checkSupportsJDBC4(); boolean f = iface.isInstance(this); - loggerExternal.exiting(getClassNameLogging(), "isWrapperFor", Boolean.valueOf(f)); + loggerExternal.exiting(getClassNameLogging(), "isWrapperFor", f); return f; } public T unwrap(Class iface) throws SQLException { loggerExternal.entering(getClassNameLogging(), "unwrap", iface); - DriverJDBCVersion.checkSupportsJDBC4(); - T t; try { t = iface.cast(this); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataTable.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataTable.java index a0fc760f1..60188841a 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataTable.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataTable.java @@ -13,10 +13,12 @@ import java.time.OffsetDateTime; import java.time.OffsetTime; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import java.util.UUID; public final class SQLServerDataTable { @@ -24,6 +26,7 @@ public final class SQLServerDataTable { int rowCount = 0; int columnCount = 0; Map columnMetadata = null; + Set columnNames = null; Map rows = null; private String tvpName = null; @@ -36,8 +39,9 @@ public final class SQLServerDataTable { */ // Name used in CREATE TYPE public SQLServerDataTable() throws SQLServerException { - columnMetadata = new LinkedHashMap(); - rows = new HashMap(); + columnMetadata = new LinkedHashMap<>(); + columnNames = new HashSet<>(); + rows = new HashMap<>(); } /** @@ -75,7 +79,7 @@ public synchronized Iterator> getIterator() { public synchronized void addColumnMetadata(String columnName, int sqlType) throws SQLServerException { // column names must be unique - Util.checkDuplicateColumnName(columnName, columnMetadata); + Util.checkDuplicateColumnName(columnName, columnNames); columnMetadata.put(columnCount++, new SQLServerDataColumn(columnName, sqlType)); } @@ -89,10 +93,11 @@ public synchronized void addColumnMetadata(String columnName, */ public synchronized void addColumnMetadata(SQLServerDataColumn column) throws SQLServerException { // column names must be unique - Util.checkDuplicateColumnName(column.columnName, columnMetadata); + Util.checkDuplicateColumnName(column.columnName, columnNames); columnMetadata.put(columnCount++, column); } + /** * Adds one row of data to the data table. * @@ -116,125 +121,13 @@ public synchronized void addRow(Object... values) throws SQLServerException { int currentColumn = 0; while (columnsIterator.hasNext()) { Object val = null; - boolean bValueNull; - int nValueLen; if ((null != values) && (currentColumn < values.length) && (null != values[currentColumn])) - val = (null == values[currentColumn]) ? null : values[currentColumn]; + val = values[currentColumn]; currentColumn++; Map.Entry pair = columnsIterator.next(); - SQLServerDataColumn currentColumnMetadata = pair.getValue(); JDBCType jdbcType = JDBCType.of(pair.getValue().javaSqlType); - - boolean isColumnMetadataUpdated = false; - switch (jdbcType) { - case BIGINT: - rowValues[pair.getKey()] = (null == val) ? null : Long.parseLong(val.toString()); - break; - - case BIT: - rowValues[pair.getKey()] = (null == val) ? null : Boolean.parseBoolean(val.toString()); - break; - - case INTEGER: - rowValues[pair.getKey()] = (null == val) ? null : Integer.parseInt(val.toString()); - break; - - case SMALLINT: - case TINYINT: - rowValues[pair.getKey()] = (null == val) ? null : Short.parseShort(val.toString()); - break; - - case DECIMAL: - case NUMERIC: - BigDecimal bd = null; - if (null != val) { - bd = new BigDecimal(val.toString()); - // BigDecimal#precision returns number of digits in the unscaled value. - // Say, for value 0.01, it returns 1 but the precision should be 3 for SQLServer - int precision = Util.getValueLengthBaseOnJavaType(bd, JavaType.of(bd), null, null, jdbcType); - if (bd.scale() > currentColumnMetadata.scale) { - currentColumnMetadata.scale = bd.scale(); - isColumnMetadataUpdated = true; - } - if (precision > currentColumnMetadata.precision) { - currentColumnMetadata.precision = precision; - isColumnMetadataUpdated = true; - } - if (isColumnMetadataUpdated) - columnMetadata.put(pair.getKey(), currentColumnMetadata); - } - rowValues[pair.getKey()] = bd; - break; - - case DOUBLE: - rowValues[pair.getKey()] = (null == val) ? null : Double.parseDouble(val.toString()); - break; - - case FLOAT: - case REAL: - rowValues[pair.getKey()] = (null == val) ? null : Float.parseFloat(val.toString()); - break; - - case TIMESTAMP_WITH_TIMEZONE: - case TIME_WITH_TIMEZONE: - DriverJDBCVersion.checkSupportsJDBC42(); - case DATE: - case TIME: - case TIMESTAMP: - case DATETIMEOFFSET: - // Sending temporal types as string. Error from database is thrown if parsing fails - // no need to send precision for temporal types, string literal will never exceed DataTypes.SHORT_VARTYPE_MAX_BYTES - - if (null == val) - rowValues[pair.getKey()] = null; - // java.sql.Date, java.sql.Time and java.sql.Timestamp are subclass of java.util.Date - else if (val instanceof java.util.Date) - rowValues[pair.getKey()] = val.toString(); - else if (val instanceof microsoft.sql.DateTimeOffset) - rowValues[pair.getKey()] = val.toString(); - else if (val instanceof OffsetDateTime) - rowValues[pair.getKey()] = val.toString(); - else if (val instanceof OffsetTime) - rowValues[pair.getKey()] = val.toString(); - else - rowValues[pair.getKey()] = (null == val) ? null : (String) val; - break; - - case BINARY: - case VARBINARY: - bValueNull = (null == val); - nValueLen = bValueNull ? 0 : ((byte[]) val).length; - - if (nValueLen > currentColumnMetadata.precision) { - currentColumnMetadata.precision = nValueLen; - columnMetadata.put(pair.getKey(), currentColumnMetadata); - } - rowValues[pair.getKey()] = (bValueNull) ? null : (byte[]) val; - - break; - - case CHAR: - if (val instanceof UUID && (val != null)) - val = val.toString(); - case VARCHAR: - case NCHAR: - case NVARCHAR: - bValueNull = (null == val); - nValueLen = bValueNull ? 0 : (2 * ((String) val).length()); - - if (nValueLen > currentColumnMetadata.precision) { - currentColumnMetadata.precision = nValueLen; - columnMetadata.put(pair.getKey(), currentColumnMetadata); - } - rowValues[pair.getKey()] = (bValueNull) ? null : (String) val; - break; - - default: - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unsupportedDataTypeTVP")); - Object[] msgArgs = {jdbcType}; - throw new SQLServerException(null, form.format(msgArgs), null, 0, false); - } + internalAddrow(jdbcType, val, rowValues, pair); } rows.put(rowCount++, rowValues); } @@ -246,11 +139,162 @@ else if (val instanceof OffsetTime) } } - + + /** + * Adding rows one row of data to data table. + * @param jdbcType The jdbcType + * @param val The data value + * @param rowValues Row of data + * @param pair pair to be added to data table + * @throws SQLServerException + */ + private void internalAddrow(JDBCType jdbcType, + Object val, + Object[] rowValues, + Map.Entry pair) throws SQLServerException { + + SQLServerDataColumn currentColumnMetadata = pair.getValue(); + boolean isColumnMetadataUpdated = false; + boolean bValueNull; + int nValueLen; + switch (jdbcType) { + case BIGINT: + rowValues[pair.getKey()] = (null == val) ? null : Long.parseLong(val.toString()); + break; + + case BIT: + rowValues[pair.getKey()] = (null == val) ? null : Boolean.parseBoolean(val.toString()); + break; + + case INTEGER: + rowValues[pair.getKey()] = (null == val) ? null : Integer.parseInt(val.toString()); + break; + + case SMALLINT: + case TINYINT: + rowValues[pair.getKey()] = (null == val) ? null : Short.parseShort(val.toString()); + break; + + case DECIMAL: + case NUMERIC: + BigDecimal bd = null; + if (null != val) { + bd = new BigDecimal(val.toString()); + // BigDecimal#precision returns number of digits in the unscaled value. + // Say, for value 0.01, it returns 1 but the precision should be 3 for SQLServer + int precision = Util.getValueLengthBaseOnJavaType(bd, JavaType.of(bd), null, null, jdbcType); + if (bd.scale() > currentColumnMetadata.scale) { + currentColumnMetadata.scale = bd.scale(); + isColumnMetadataUpdated = true; + } + if (precision > currentColumnMetadata.precision) { + currentColumnMetadata.precision = precision; + isColumnMetadataUpdated = true; + } + + // precision equal: the maximum number of digits in integer part + the maximum scale + int numberOfDigitsIntegerPart = precision - bd.scale(); + if (numberOfDigitsIntegerPart > currentColumnMetadata.numberOfDigitsIntegerPart) { + currentColumnMetadata.numberOfDigitsIntegerPart = numberOfDigitsIntegerPart; + isColumnMetadataUpdated = true; + } + + if (isColumnMetadataUpdated) { + currentColumnMetadata.precision = currentColumnMetadata.scale + currentColumnMetadata.numberOfDigitsIntegerPart; + columnMetadata.put(pair.getKey(), currentColumnMetadata); + } + } + rowValues[pair.getKey()] = bd; + break; + + case DOUBLE: + rowValues[pair.getKey()] = (null == val) ? null : Double.parseDouble(val.toString()); + break; + + case FLOAT: + case REAL: + rowValues[pair.getKey()] = (null == val) ? null : Float.parseFloat(val.toString()); + break; + + case TIMESTAMP_WITH_TIMEZONE: + case TIME_WITH_TIMEZONE: + DriverJDBCVersion.checkSupportsJDBC42(); + case DATE: + case TIME: + case TIMESTAMP: + case DATETIMEOFFSET: + case DATETIME: + case SMALLDATETIME: + // Sending temporal types as string. Error from database is thrown if parsing fails + // no need to send precision for temporal types, string literal will never exceed DataTypes.SHORT_VARTYPE_MAX_BYTES + + if (null == val) + rowValues[pair.getKey()] = null; + // java.sql.Date, java.sql.Time and java.sql.Timestamp are subclass of java.util.Date + else if (val instanceof java.util.Date) + rowValues[pair.getKey()] = val.toString(); + else if (val instanceof microsoft.sql.DateTimeOffset) + rowValues[pair.getKey()] = val.toString(); + else if (val instanceof OffsetDateTime) + rowValues[pair.getKey()] = val.toString(); + else if (val instanceof OffsetTime) + rowValues[pair.getKey()] = val.toString(); + else + rowValues[pair.getKey()] = (null == val) ? null : (String) val; + break; + + case BINARY: + case VARBINARY: + case LONGVARBINARY: + bValueNull = (null == val); + nValueLen = bValueNull ? 0 : ((byte[]) val).length; + + if (nValueLen > currentColumnMetadata.precision) { + currentColumnMetadata.precision = nValueLen; + columnMetadata.put(pair.getKey(), currentColumnMetadata); + } + rowValues[pair.getKey()] = (bValueNull) ? null : (byte[]) val; + + break; + + case CHAR: + if (val instanceof UUID && (val != null)) + val = val.toString(); + case VARCHAR: + case NCHAR: + case NVARCHAR: + case LONGVARCHAR: + case LONGNVARCHAR: + case SQLXML: + bValueNull = (null == val); + nValueLen = bValueNull ? 0 : (2 * ((String) val).length()); + + if (nValueLen > currentColumnMetadata.precision) { + currentColumnMetadata.precision = nValueLen; + columnMetadata.put(pair.getKey(), currentColumnMetadata); + } + rowValues[pair.getKey()] = (bValueNull) ? null : (String) val; + break; + case SQL_VARIANT: + JDBCType internalJDBCType; + if (null == val) { // TODO:Check this later + throw new SQLServerException(SQLServerException.getErrString("R_invalidValueForTVPWithSQLVariant"), null); + } + JavaType javaType = JavaType.of(val); + internalJDBCType = javaType.getJDBCType(SSType.UNKNOWN, jdbcType); + internalAddrow(internalJDBCType, val, rowValues, pair); + break; + default: + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unsupportedDataTypeTVP")); + Object[] msgArgs = {jdbcType}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, false); + } + } + public synchronized Map getColumnMetadata() { return columnMetadata; } - + public String getTvpName() { return tvpName; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java index 956294c85..516a4d590 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java @@ -8,6 +8,7 @@ package com.microsoft.sqlserver.jdbc; +import java.sql.BatchUpdateException; import java.sql.CallableStatement; import java.sql.Connection; import java.sql.DriverPropertyInfo; @@ -17,6 +18,7 @@ import java.text.MessageFormat; import java.util.EnumMap; import java.util.Properties; +import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; @@ -89,7 +91,7 @@ final void close() throws SQLServerException { } } - EnumMap handleMap = new EnumMap(CallableHandles.class); + EnumMap handleMap = new EnumMap<>(CallableHandles.class); // Returns unique id for each instance. private static int nextInstanceID() { @@ -120,13 +122,11 @@ final public String toString() { } public boolean isWrapperFor(Class iface) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); boolean f = iface.isInstance(this); return f; } public T unwrap(Class iface) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); T t; try { t = iface.cast(this); @@ -246,7 +246,6 @@ private SQLServerResultSet getResultSetFromInternalQueries(String catalog, SQLServerResultSet rs = null; try { rs = ((SQLServerStatement) connection.createStatement()).executeQueryInternal(query); - } finally { if (null != orgCat) { @@ -358,7 +357,6 @@ private SQLServerResultSet getResultSetWithProvidedColumnNames(String catalog, } public boolean autoCommitFailureClosesAllResultSets() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); return false; } @@ -379,7 +377,6 @@ public boolean autoCommitFailureClosesAllResultSets() throws SQLException { } public boolean generatedKeyAlwaysReturned() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC41(); checkClosed(); // driver supports retrieving generated keys @@ -617,7 +614,6 @@ private static String EscapeIDName(String inID) throws SQLServerException { public java.sql.ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); /* @@ -647,7 +643,6 @@ public java.sql.ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern, String columnNamePattern) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); /* * sp_sproc_columns [[@procedure_name =] 'name'] [,[@procedure_owner =] 'owner'] [,[@procedure_qualifier =] 'qualifier'] [,[@column_name =] @@ -688,7 +683,6 @@ public java.sql.ResultSet getFunctionColumns(String catalog, } public java.sql.ResultSet getClientInfoProperties() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); return getResultSetFromInternalQueries(null, "SELECT" + /* 1 */ " cast(NULL as char(1)) as NAME," + @@ -751,6 +745,7 @@ public java.sql.ResultSet getClientInfoProperties() throws SQLException { loggerExternal.finer(toString() + " ActivityId: " + ActivityCorrelator.getNext().toString()); } checkClosed(); + /* * sp_fkeys [ @pktable_name = ] 'pktable_name' [ , [ @pktable_owner = ] 'pktable_owner' ] [ , [ @pktable_qualifier = ] 'pktable_qualifier' ] { * , [ @fktable_name = ] 'fktable_name' } [ , [ @fktable_owner = ] 'fktable_owner' ] [ , [ @fktable_qualifier = ] 'fktable_qualifier' ] @@ -763,7 +758,9 @@ public java.sql.ResultSet getClientInfoProperties() throws SQLException { arguments[4] = schem2; arguments[5] = cat2; - return getResultSetWithProvidedColumnNames(null, CallableHandles.SP_FKEYS, arguments, pkfkColumnNames); + SQLServerResultSet fkeysRS = getResultSetWithProvidedColumnNames(null, CallableHandles.SP_FKEYS, arguments, pkfkColumnNames); + + return getResultSetForForeignKeyInformation(fkeysRS, null); } /* L0 */ public String getDatabaseProductName() throws SQLServerException { @@ -814,6 +811,7 @@ public java.sql.ResultSet getClientInfoProperties() throws SQLException { loggerExternal.finer(toString() + " ActivityId: " + ActivityCorrelator.getNext().toString()); } checkClosed(); + /* * sp_fkeys [ @pktable_name = ] 'pktable_name' [ , [ @pktable_owner = ] 'pktable_owner' ] [ , [ @pktable_qualifier = ] 'pktable_qualifier' ] { * , [ @fktable_name = ] 'fktable_name' } [ , [ @fktable_owner = ] 'fktable_owner' ] [ , [ @fktable_qualifier = ] 'fktable_qualifier' ] @@ -825,7 +823,10 @@ public java.sql.ResultSet getClientInfoProperties() throws SQLException { arguments[3] = null; // fktable_name arguments[4] = null; arguments[5] = null; - return getResultSetWithProvidedColumnNames(cat, CallableHandles.SP_FKEYS, arguments, pkfkColumnNames); + + SQLServerResultSet fkeysRS = getResultSetWithProvidedColumnNames(cat, CallableHandles.SP_FKEYS, arguments, pkfkColumnNames); + + return getResultSetForForeignKeyInformation(fkeysRS, cat); } /* L0 */ public String getExtraNameCharacters() throws SQLServerException { @@ -845,6 +846,7 @@ public java.sql.ResultSet getClientInfoProperties() throws SQLException { loggerExternal.finer(toString() + " ActivityId: " + ActivityCorrelator.getNext().toString()); } checkClosed(); + /* * sp_fkeys [ @pktable_name = ] 'pktable_name' [ , [ @pktable_owner = ] 'pktable_owner' ] [ , [ @pktable_qualifier = ] 'pktable_qualifier' ] { * , [ @fktable_name = ] 'fktable_name' } [ , [ @fktable_owner = ] 'fktable_owner' ] [ , [ @fktable_qualifier = ] 'fktable_qualifier' ] @@ -856,7 +858,147 @@ public java.sql.ResultSet getClientInfoProperties() throws SQLException { arguments[3] = table; // fktable_name arguments[4] = schema; arguments[5] = cat; - return getResultSetWithProvidedColumnNames(cat, CallableHandles.SP_FKEYS, arguments, pkfkColumnNames); + + SQLServerResultSet fkeysRS = getResultSetWithProvidedColumnNames(cat, CallableHandles.SP_FKEYS, arguments, pkfkColumnNames); + + return getResultSetForForeignKeyInformation(fkeysRS, cat); + } + + /** + * The original sp_fkeys stored procedure does not give the required values from JDBC specification. This method creates 2 temporary tables and + * uses join and other operations on them to give the correct values. + * + * @param sp_fkeys_Query + * @return + * @throws SQLServerException + */ + private ResultSet getResultSetForForeignKeyInformation(SQLServerResultSet fkeysRS, String cat) throws SQLServerException { + UUID uuid = UUID.randomUUID(); + String fkeys_results_tableName = "[#fkeys_results" + uuid + "]"; + String foreign_keys_combined_tableName = "[#foreign_keys_combined_results" + uuid + "]"; + String sys_foreign_keys = "sys.foreign_keys"; + + String fkeys_results_column_definition = "PKTABLE_QUALIFIER sysname, PKTABLE_OWNER sysname, PKTABLE_NAME sysname, PKCOLUMN_NAME sysname, FKTABLE_QUALIFIER sysname, FKTABLE_OWNER sysname, FKTABLE_NAME sysname, FKCOLUMN_NAME sysname, KEY_SEQ smallint, UPDATE_RULE smallint, DELETE_RULE smallint, FK_NAME sysname, PK_NAME sysname, DEFERRABILITY smallint"; + String foreign_keys_combined_column_definition = "name sysname, delete_referential_action_desc nvarchar(60), update_referential_action_desc nvarchar(60)," + + fkeys_results_column_definition; + + // cannot close this statement, otherwise the returned resultset would be closed too. + SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(); + + /** + * create a temp table that has the same definition as the result of sp_fkeys: + * + * create table #fkeys_results ( + * PKTABLE_QUALIFIER sysname, + * PKTABLE_OWNER sysname, + * PKTABLE_NAME sysname, + * PKCOLUMN_NAME sysname, + * FKTABLE_QUALIFIER sysname, + * FKTABLE_OWNER sysname, + * FKTABLE_NAME sysname, + * FKCOLUMN_NAME sysname, + * KEY_SEQ smallint, + * UPDATE_RULE smallint, + * DELETE_RULE smallint, + * FK_NAME sysname, + * PK_NAME sysname, + * DEFERRABILITY smallint + * ); + * + */ + stmt.execute("create table " + fkeys_results_tableName + " (" + fkeys_results_column_definition + ")"); + + /** + * insert the results of sp_fkeys to the temp table #fkeys_results + */ + SQLServerPreparedStatement ps = (SQLServerPreparedStatement) connection + .prepareCall("insert into " + fkeys_results_tableName + "values(?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); + try { + while (fkeysRS.next()) { + ps.setString(1, fkeysRS.getString(1)); + ps.setString(2, fkeysRS.getString(2)); + ps.setString(3, fkeysRS.getString(3)); + ps.setString(4, fkeysRS.getString(4)); + ps.setString(5, fkeysRS.getString(5)); + ps.setString(6, fkeysRS.getString(6)); + ps.setString(7, fkeysRS.getString(7)); + ps.setString(8, fkeysRS.getString(8)); + ps.setInt(9, fkeysRS.getInt(9)); + ps.setInt(10, fkeysRS.getInt(10)); + ps.setInt(11, fkeysRS.getInt(11)); + ps.setString(12, fkeysRS.getString(12)); + ps.setString(13, fkeysRS.getString(13)); + ps.setInt(14, fkeysRS.getInt(14)); + ps.execute(); + } + } + finally { + if (null != ps) { + ps.close(); + } + if (null != fkeysRS) { + fkeysRS.close(); + } + } + + /** + * create another temp table that has 3 columns from sys.foreign_keys and the rest of columns are the same as #fkeys_results: + * + * create table #foreign_keys_combined_results ( + * name sysname, + * delete_referential_action_desc nvarchar(60), + * update_referential_action_desc nvarchar(60), + * ...... + * ...... + * ...... + * ); + * + */ + stmt.addBatch("create table " + foreign_keys_combined_tableName + " (" + foreign_keys_combined_column_definition + ")"); + + /** + * right join the content of sys.foreign_keys and the content of #fkeys_results base on foreign key name and save the result to the new temp + * table #foreign_keys_combined_results + */ + stmt.addBatch("insert into " + foreign_keys_combined_tableName + + " select " + sys_foreign_keys + ".name, " + sys_foreign_keys + ".delete_referential_action_desc, " + sys_foreign_keys + ".update_referential_action_desc," + + fkeys_results_tableName + ".PKTABLE_QUALIFIER," + fkeys_results_tableName + ".PKTABLE_OWNER," + fkeys_results_tableName + ".PKTABLE_NAME," + fkeys_results_tableName + ".PKCOLUMN_NAME," + + fkeys_results_tableName + ".FKTABLE_QUALIFIER," + fkeys_results_tableName + ".FKTABLE_OWNER," + fkeys_results_tableName + ".FKTABLE_NAME," + fkeys_results_tableName + ".FKCOLUMN_NAME," + + fkeys_results_tableName + ".KEY_SEQ," + fkeys_results_tableName + ".UPDATE_RULE," + fkeys_results_tableName + ".DELETE_RULE," + fkeys_results_tableName + ".FK_NAME," + fkeys_results_tableName + ".PK_NAME," + + fkeys_results_tableName + ".DEFERRABILITY from " + sys_foreign_keys + + " right join " + fkeys_results_tableName + " on " + sys_foreign_keys + ".name=" + fkeys_results_tableName + ".FK_NAME"); + + /** + * the DELETE_RULE value and UPDATE_RULE value returned from sp_fkeys are not the same as required by JDBC spec. therefore, we need to update + * those values to JDBC required values base on delete_referential_action_desc and update_referential_action_desc returned from sys.foreign_keys + * No Action: 3 + * Cascade: 0 + * Set Null: 2 + * Set Default: 4 + */ + stmt.addBatch("update " + foreign_keys_combined_tableName + " set DELETE_RULE=3 where delete_referential_action_desc='NO_ACTION';" + + "update " + foreign_keys_combined_tableName + " set DELETE_RULE=0 where delete_referential_action_desc='Cascade';" + + "update " + foreign_keys_combined_tableName + " set DELETE_RULE=2 where delete_referential_action_desc='SET_NULL';" + + "update " + foreign_keys_combined_tableName + " set DELETE_RULE=4 where delete_referential_action_desc='SET_DEFAULT';" + + "update " + foreign_keys_combined_tableName + " set UPDATE_RULE=3 where update_referential_action_desc='NO_ACTION';" + + "update " + foreign_keys_combined_tableName + " set UPDATE_RULE=0 where update_referential_action_desc='Cascade';" + + "update " + foreign_keys_combined_tableName + " set UPDATE_RULE=2 where update_referential_action_desc='SET_NULL';" + + "update " + foreign_keys_combined_tableName + " set UPDATE_RULE=4 where update_referential_action_desc='SET_DEFAULT';"); + + try { + stmt.executeBatch(); + } + catch (BatchUpdateException e) { + throw new SQLServerException(e.getMessage(), e.getSQLState(), e.getErrorCode(), null); + } + + /** + * now, the #foreign_keys_combined_results table has the correct values for DELETE_RULE and UPDATE_RULE. Then we can return the result of + * the table with the same definition of the resultset return by sp_fkeys (same column definition and same order). + */ + return stmt.executeQuery( + "select PKTABLE_QUALIFIER as 'PKTABLE_CAT',PKTABLE_OWNER as 'PKTABLE_SCHEM',PKTABLE_NAME,PKCOLUMN_NAME,FKTABLE_QUALIFIER as 'FKTABLE_CAT',FKTABLE_OWNER as 'FKTABLE_SCHEM',FKTABLE_NAME,FKCOLUMN_NAME,KEY_SEQ,UPDATE_RULE,DELETE_RULE,FK_NAME,PK_NAME,DEFERRABILITY from " + + foreign_keys_combined_tableName + " order by FKTABLE_QUALIFIER, FKTABLE_OWNER, FKTABLE_NAME, KEY_SEQ"); } private final static String[] getIndexInfoColumnNames = {/* 1 */ TABLE_CAT, /* 2 */ TABLE_SCHEM, /* 3 */ TABLE_NAME, /* 4 */ NON_UNIQUE, @@ -1109,8 +1251,6 @@ public ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC41(); - if (loggerExternal.isLoggable(Level.FINER) && Util.IsActivityTraceOn()) { loggerExternal.finer(toString() + " ActivityId: " + ActivityCorrelator.getNext().toString()); } @@ -1217,7 +1357,6 @@ public java.sql.ResultSet getSchemas(String catalog, if (loggerExternal.isLoggable(Level.FINER) && Util.IsActivityTraceOn()) { loggerExternal.finer(toString() + " ActivityId: " + ActivityCorrelator.getNext().toString()); } - DriverJDBCVersion.checkSupportsJDBC4(); return getSchemasInternal(catalog, schemaPattern); } @@ -1927,7 +2066,7 @@ else if (name.equals(SQLServerDriverIntProperty.PORT_NUMBER.toString())) { } // if the value is outside of the valid values throw error. MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidArgument")); - Object[] msgArgs = {new Integer(type)}; + Object[] msgArgs = {type}; throw new SQLServerException(null, form.format(msgArgs), null, 0, true); } @@ -1943,7 +2082,7 @@ else if (name.equals(SQLServerDriverIntProperty.PORT_NUMBER.toString())) { } // if the value is outside of the valid values throw error. MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidArgument")); - Object[] msgArgs = {new Integer(type)}; + Object[] msgArgs = {type}; throw new SQLServerException(null, form.format(msgArgs), null, 0, true); } @@ -1998,7 +2137,7 @@ else if (name.equals(SQLServerDriverIntProperty.PORT_NUMBER.toString())) { if (p > 0) s = s.substring(0, p); try { - return new Integer(s).intValue(); + return new Integer(s); } catch (NumberFormatException e) { return 0; @@ -2013,7 +2152,7 @@ else if (name.equals(SQLServerDriverIntProperty.PORT_NUMBER.toString())) { if (p > 0 && q > 0) s = s.substring(p + 1, q); try { - return new Integer(s).intValue(); + return new Integer(s); } catch (NumberFormatException e) { return 0; @@ -2036,7 +2175,6 @@ else if (name.equals(SQLServerDriverIntProperty.PORT_NUMBER.toString())) { } public RowIdLifetime getRowIdLifetime() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); return RowIdLifetime.ROWID_UNSUPPORTED; } @@ -2049,7 +2187,7 @@ public RowIdLifetime getRowIdLifetime() throws SQLException { // if the value is outside of the valid values throw error. MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidArgument")); - Object[] msgArgs = {new Integer(holdability)}; + Object[] msgArgs = {holdability}; throw new SQLServerException(null, form.format(msgArgs), null, 0, true); } @@ -2141,7 +2279,6 @@ public RowIdLifetime getRowIdLifetime() throws SQLException { } public boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); return true; } @@ -2214,12 +2351,12 @@ final Object apply(Object value, switch (asJDBCType) { case INTEGER: - return new Integer(oneValueToAnother(((Integer) value).intValue())); + return oneValueToAnother((Integer) value); case SMALLINT: // small and tinyint returned as short case TINYINT: - return new Short((short) oneValueToAnother(((Short) value).intValue())); + return (short) oneValueToAnother(((Short) value).intValue()); case BIGINT: - return new Long(oneValueToAnother(((Long) value).intValue())); + return (long) oneValueToAnother(((Long) value).intValue()); case CHAR: case VARCHAR: case LONGVARCHAR: diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java index 4b139b2c4..55c6b5918 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java @@ -120,6 +120,46 @@ else if (value.toLowerCase(Locale.US).equalsIgnoreCase(ColumnEncryptionSetting.D } } +enum SSLProtocol { + TLS("TLS"), + TLS_V10("TLSv1"), + TLS_V11("TLSv1.1"), + TLS_V12("TLSv1.2"),; + + private final String name; + + private SSLProtocol(String name) { + this.name = name; + } + + public String toString() { + return name; + } + + static SSLProtocol valueOfString(String value) throws SQLServerException { + SSLProtocol protocol = null; + + if (value.toLowerCase(Locale.ENGLISH).equalsIgnoreCase(SSLProtocol.TLS.toString())) { + protocol = SSLProtocol.TLS; + } + else if (value.toLowerCase(Locale.ENGLISH).equalsIgnoreCase(SSLProtocol.TLS_V10.toString())) { + protocol = SSLProtocol.TLS_V10; + } + else if (value.toLowerCase(Locale.ENGLISH).equalsIgnoreCase(SSLProtocol.TLS_V11.toString())) { + protocol = SSLProtocol.TLS_V11; + } + else if (value.toLowerCase(Locale.ENGLISH).equalsIgnoreCase(SSLProtocol.TLS_V12.toString())) { + protocol = SSLProtocol.TLS_V12; + } + else { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidSSLProtocol")); + Object[] msgArgs = {value}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, false); + } + return protocol; + } +} + enum KeyStoreAuthentication { JavaKeyStorePassword; @@ -164,7 +204,7 @@ enum ApplicationIntent { READ_ONLY("readonly"); // the value of the enum - private String value; + private final String value; // constructor that sets the string value of the enum private ApplicationIntent(String value) { @@ -199,11 +239,11 @@ else if (value.equalsIgnoreCase(ApplicationIntent.READ_WRITE.toString())) { enum SQLServerDriverObjectProperty { GSS_CREDENTIAL("gsscredential", null); - private String name; - private Object defaultValue; + private final String name; + private final String defaultValue; private SQLServerDriverObjectProperty(String name, - Object defaultValue) { + String defaultValue) { this.name = name; this.defaultValue = defaultValue; } @@ -213,7 +253,7 @@ private SQLServerDriverObjectProperty(String name, * @return */ public String getDefaultValue() { - return null; + return defaultValue; } public String toString() { @@ -221,6 +261,8 @@ public String toString() { } } + + enum SQLServerDriverStringProperty { APPLICATION_INTENT ("applicationIntent", ApplicationIntent.READ_WRITE.toString()), @@ -229,6 +271,7 @@ enum SQLServerDriverStringProperty FAILOVER_PARTNER ("failoverPartner", ""), HOSTNAME_IN_CERTIFICATE ("hostNameInCertificate", ""), INSTANCE_NAME ("instanceName", ""), + JAAS_CONFIG_NAME ("jaasConfigurationName", "SQLJDBCDriver"), PASSWORD ("password", ""), RESPONSE_BUFFERING ("responseBuffering", "adaptive"), SELECT_METHOD ("selectMethod", "direct"), @@ -237,6 +280,8 @@ enum SQLServerDriverStringProperty TRUST_STORE_TYPE ("trustStoreType", "JKS"), TRUST_STORE ("trustStore", ""), TRUST_STORE_PASSWORD ("trustStorePassword", ""), + TRUST_MANAGER_CLASS ("trustManagerClass", ""), + TRUST_MANAGER_CONSTRUCTOR_ARG("trustManagerConstructorArg", ""), USER ("user", ""), WORKSTATION_ID ("workstationID", Util.WSIDNotAvailable), AUTHENTICATION_SCHEME ("authenticationScheme", AuthenticationScheme.nativeAuthentication.toString()), @@ -246,11 +291,11 @@ enum SQLServerDriverStringProperty KEY_STORE_AUTHENTICATION ("keyStoreAuthentication", ""), KEY_STORE_SECRET ("keyStoreSecret", ""), KEY_STORE_LOCATION ("keyStoreLocation", ""), - FIPS_PROVIDER ("fipsProvider", ""), + SSL_PROTOCOL ("sslProtocol", SSLProtocol.TLS.toString()), ; - private String name; - private String defaultValue; + private final String name; + private final String defaultValue; private SQLServerDriverStringProperty(String name, String defaultValue) { @@ -268,17 +313,20 @@ public String toString() { } enum SQLServerDriverIntProperty { - PACKET_SIZE ("packetSize", TDS.DEFAULT_PACKET_SIZE), - LOCK_TIMEOUT ("lockTimeout", -1), - LOGIN_TIMEOUT ("loginTimeout", 15), - QUERY_TIMEOUT ("queryTimeout", -1), - PORT_NUMBER ("portNumber", 1433), - SOCKET_TIMEOUT ("socketTimeout", 0), + PACKET_SIZE ("packetSize", TDS.DEFAULT_PACKET_SIZE), + LOCK_TIMEOUT ("lockTimeout", -1), + LOGIN_TIMEOUT ("loginTimeout", 15), + QUERY_TIMEOUT ("queryTimeout", -1), + PORT_NUMBER ("portNumber", 1433), + SOCKET_TIMEOUT ("socketTimeout", 0), + SERVER_PREPARED_STATEMENT_DISCARD_THRESHOLD("serverPreparedStatementDiscardThreshold", SQLServerConnection.DEFAULT_SERVER_PREPARED_STATEMENT_DISCARD_THRESHOLD), + STATEMENT_POOLING_CACHE_SIZE ("statementPoolingCacheSize", SQLServerConnection.DEFAULT_STATEMENT_POOLING_CACHE_SIZE), CONNECT_RETRY_COUNT("connectRetryCount", 1), CONNECT_RETRY_INTERVAL("connectRetryInterval", 10); - - private String name; - private int defaultValue; + ; + + private final String name; + private final int defaultValue; private SQLServerDriverIntProperty(String name, int defaultValue) { @@ -295,23 +343,24 @@ public String toString() { } } -enum SQLServerDriverBooleanProperty +enum SQLServerDriverBooleanProperty { - DISABLE_STATEMENT_POOLING ("disableStatementPooling", true), - ENCRYPT ("encrypt", false), - INTEGRATED_SECURITY ("integratedSecurity", false), - LAST_UPDATE_COUNT ("lastUpdateCount", true), - MULTI_SUBNET_FAILOVER ("multiSubnetFailover", false), - SERVER_NAME_AS_ACE ("serverNameAsACE", false), - SEND_STRING_PARAMETERS_AS_UNICODE ("sendStringParametersAsUnicode", true), - SEND_TIME_AS_DATETIME ("sendTimeAsDatetime", true), - TRANSPARENT_NETWORK_IP_RESOLUTION ("TransparentNetworkIPResolution", true), - TRUST_SERVER_CERTIFICATE ("trustServerCertificate", false), - XOPEN_STATES ("xopenStates", false), - FIPS ("fips", false); - - private String name; - private boolean defaultValue; + DISABLE_STATEMENT_POOLING ("disableStatementPooling", false), + ENCRYPT ("encrypt", false), + INTEGRATED_SECURITY ("integratedSecurity", false), + LAST_UPDATE_COUNT ("lastUpdateCount", true), + MULTI_SUBNET_FAILOVER ("multiSubnetFailover", false), + SERVER_NAME_AS_ACE ("serverNameAsACE", false), + SEND_STRING_PARAMETERS_AS_UNICODE ("sendStringParametersAsUnicode", true), + SEND_TIME_AS_DATETIME ("sendTimeAsDatetime", true), + TRANSPARENT_NETWORK_IP_RESOLUTION ("TransparentNetworkIPResolution", true), + TRUST_SERVER_CERTIFICATE ("trustServerCertificate", false), + XOPEN_STATES ("xopenStates", false), + FIPS ("fips", false), + ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT("enablePrepareOnFirstPreparedStatementCall", SQLServerConnection.DEFAULT_ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT_CALL); + + private final String name; + private final boolean defaultValue; private SQLServerDriverBooleanProperty(String name, boolean defaultValue) { @@ -339,52 +388,58 @@ public final class SQLServerDriver implements java.sql.Driver { private static final String[] TRUE_FALSE = {"true", "false"}; private static final SQLServerDriverPropertyInfo[] DRIVER_PROPERTIES = { - // default required available choices - // property name value property (if appropriate) - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.APPLICATION_INTENT.toString(), SQLServerDriverStringProperty.APPLICATION_INTENT.getDefaultValue(), false, new String[]{ApplicationIntent.READ_ONLY.toString(), ApplicationIntent.READ_WRITE.toString()}), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.APPLICATION_NAME.toString(), SQLServerDriverStringProperty.APPLICATION_NAME.getDefaultValue(), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.COLUMN_ENCRYPTION.toString(), SQLServerDriverStringProperty.COLUMN_ENCRYPTION.getDefaultValue(), false, new String[] {ColumnEncryptionSetting.Disabled.toString(), ColumnEncryptionSetting.Enabled.toString()}), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.DATABASE_NAME.toString(), SQLServerDriverStringProperty.DATABASE_NAME.getDefaultValue(), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.DISABLE_STATEMENT_POOLING.toString(), Boolean.toString(SQLServerDriverBooleanProperty.DISABLE_STATEMENT_POOLING.getDefaultValue()), false, new String[] {"true"}), - new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.ENCRYPT.toString(), Boolean.toString(SQLServerDriverBooleanProperty.ENCRYPT.getDefaultValue()), false, TRUE_FALSE), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.FAILOVER_PARTNER.toString(), SQLServerDriverStringProperty.FAILOVER_PARTNER.getDefaultValue(), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString(), SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.getDefaultValue(), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.INSTANCE_NAME.toString(), SQLServerDriverStringProperty.INSTANCE_NAME.getDefaultValue(), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.INTEGRATED_SECURITY.toString(), Boolean.toString(SQLServerDriverBooleanProperty.INTEGRATED_SECURITY.getDefaultValue()), false, TRUE_FALSE), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.KEY_STORE_AUTHENTICATION.toString(), SQLServerDriverStringProperty.KEY_STORE_AUTHENTICATION.getDefaultValue(), false, new String[] {KeyStoreAuthentication.JavaKeyStorePassword.toString()}), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.KEY_STORE_SECRET .toString(), SQLServerDriverStringProperty.KEY_STORE_SECRET.getDefaultValue(), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.KEY_STORE_LOCATION .toString(), SQLServerDriverStringProperty.KEY_STORE_LOCATION.getDefaultValue(), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.LAST_UPDATE_COUNT.toString(), Boolean.toString(SQLServerDriverBooleanProperty.LAST_UPDATE_COUNT.getDefaultValue()), false, TRUE_FALSE), - new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.LOCK_TIMEOUT.toString(), Integer.toString(SQLServerDriverIntProperty.LOCK_TIMEOUT.getDefaultValue()), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.LOGIN_TIMEOUT.toString(), Integer.toString(SQLServerDriverIntProperty.LOGIN_TIMEOUT.getDefaultValue()), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.MULTI_SUBNET_FAILOVER.toString(), Boolean.toString(SQLServerDriverBooleanProperty.MULTI_SUBNET_FAILOVER.getDefaultValue()), false, TRUE_FALSE), - new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.PACKET_SIZE.toString(), Integer.toString(SQLServerDriverIntProperty.PACKET_SIZE.getDefaultValue()), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.PASSWORD.toString(), SQLServerDriverStringProperty.PASSWORD.getDefaultValue(), true, null), - new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.PORT_NUMBER.toString(), Integer.toString(SQLServerDriverIntProperty.PORT_NUMBER.getDefaultValue()), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.QUERY_TIMEOUT.toString(), Integer.toString(SQLServerDriverIntProperty.QUERY_TIMEOUT.getDefaultValue()), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.RESPONSE_BUFFERING.toString(), SQLServerDriverStringProperty.RESPONSE_BUFFERING.getDefaultValue(), false, new String[] {"adaptive", "full"}), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SELECT_METHOD.toString(), SQLServerDriverStringProperty.SELECT_METHOD.getDefaultValue(), false, new String[] {"direct", "cursor"}), - new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.SEND_STRING_PARAMETERS_AS_UNICODE.toString(), Boolean.toString(SQLServerDriverBooleanProperty.SEND_STRING_PARAMETERS_AS_UNICODE.getDefaultValue()), false, TRUE_FALSE), - new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.SERVER_NAME_AS_ACE.toString(), Boolean.toString(SQLServerDriverBooleanProperty.SERVER_NAME_AS_ACE.getDefaultValue()), false, TRUE_FALSE), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SERVER_NAME.toString(), SQLServerDriverStringProperty.SERVER_NAME.getDefaultValue(), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SERVER_SPN.toString(), SQLServerDriverStringProperty.SERVER_SPN.getDefaultValue(), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.TRANSPARENT_NETWORK_IP_RESOLUTION.toString(), Boolean.toString(SQLServerDriverBooleanProperty.TRANSPARENT_NETWORK_IP_RESOLUTION.getDefaultValue()), false, TRUE_FALSE), - new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.TRUST_SERVER_CERTIFICATE.toString(), Boolean.toString(SQLServerDriverBooleanProperty.TRUST_SERVER_CERTIFICATE.getDefaultValue()), false, TRUE_FALSE), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.TRUST_STORE_TYPE.toString(), SQLServerDriverStringProperty.TRUST_STORE_TYPE.getDefaultValue(), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.TRUST_STORE.toString(), SQLServerDriverStringProperty.TRUST_STORE.getDefaultValue(), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.TRUST_STORE_PASSWORD.toString(), SQLServerDriverStringProperty.TRUST_STORE_PASSWORD.getDefaultValue(), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.toString(), Boolean.toString(SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.getDefaultValue()), false, TRUE_FALSE), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.USER.toString(), SQLServerDriverStringProperty.USER.getDefaultValue(), true, null), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.WORKSTATION_ID.toString(), SQLServerDriverStringProperty.WORKSTATION_ID.getDefaultValue(), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.XOPEN_STATES.toString(), Boolean.toString(SQLServerDriverBooleanProperty.XOPEN_STATES.getDefaultValue()), false, TRUE_FALSE), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.AUTHENTICATION_SCHEME.toString(), SQLServerDriverStringProperty.AUTHENTICATION_SCHEME.getDefaultValue(), false, new String[] {AuthenticationScheme.javaKerberos.toString(),AuthenticationScheme.nativeAuthentication.toString()}), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.AUTHENTICATION.toString(), SQLServerDriverStringProperty.AUTHENTICATION.getDefaultValue(), false, new String[] {SqlAuthentication.NotSpecified.toString(),SqlAuthentication.SqlPassword.toString(),SqlAuthentication.ActiveDirectoryPassword.toString(),SqlAuthentication.ActiveDirectoryIntegrated.toString()}), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.FIPS_PROVIDER.toString(), SQLServerDriverStringProperty.FIPS_PROVIDER.getDefaultValue(), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.SOCKET_TIMEOUT.toString(), Integer.toString(SQLServerDriverIntProperty.SOCKET_TIMEOUT.getDefaultValue()), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.FIPS.toString(), Boolean.toString(SQLServerDriverBooleanProperty.FIPS.getDefaultValue()), false, TRUE_FALSE), + // default required available choices + // property name value property (if appropriate) + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.APPLICATION_INTENT.toString(), SQLServerDriverStringProperty.APPLICATION_INTENT.getDefaultValue(), false, new String[]{ApplicationIntent.READ_ONLY.toString(), ApplicationIntent.READ_WRITE.toString()}), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.APPLICATION_NAME.toString(), SQLServerDriverStringProperty.APPLICATION_NAME.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.COLUMN_ENCRYPTION.toString(), SQLServerDriverStringProperty.COLUMN_ENCRYPTION.getDefaultValue(), false, new String[] {ColumnEncryptionSetting.Disabled.toString(), ColumnEncryptionSetting.Enabled.toString()}), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.DATABASE_NAME.toString(), SQLServerDriverStringProperty.DATABASE_NAME.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.DISABLE_STATEMENT_POOLING.toString(), Boolean.toString(SQLServerDriverBooleanProperty.DISABLE_STATEMENT_POOLING.getDefaultValue()), false, new String[] {"true"}), + new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.ENCRYPT.toString(), Boolean.toString(SQLServerDriverBooleanProperty.ENCRYPT.getDefaultValue()), false, TRUE_FALSE), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.FAILOVER_PARTNER.toString(), SQLServerDriverStringProperty.FAILOVER_PARTNER.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString(), SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.INSTANCE_NAME.toString(), SQLServerDriverStringProperty.INSTANCE_NAME.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.INTEGRATED_SECURITY.toString(), Boolean.toString(SQLServerDriverBooleanProperty.INTEGRATED_SECURITY.getDefaultValue()), false, TRUE_FALSE), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.KEY_STORE_AUTHENTICATION.toString(), SQLServerDriverStringProperty.KEY_STORE_AUTHENTICATION.getDefaultValue(), false, new String[] {KeyStoreAuthentication.JavaKeyStorePassword.toString()}), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.KEY_STORE_SECRET .toString(), SQLServerDriverStringProperty.KEY_STORE_SECRET.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.KEY_STORE_LOCATION .toString(), SQLServerDriverStringProperty.KEY_STORE_LOCATION.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.LAST_UPDATE_COUNT.toString(), Boolean.toString(SQLServerDriverBooleanProperty.LAST_UPDATE_COUNT.getDefaultValue()), false, TRUE_FALSE), + new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.LOCK_TIMEOUT.toString(), Integer.toString(SQLServerDriverIntProperty.LOCK_TIMEOUT.getDefaultValue()), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.LOGIN_TIMEOUT.toString(), Integer.toString(SQLServerDriverIntProperty.LOGIN_TIMEOUT.getDefaultValue()), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.MULTI_SUBNET_FAILOVER.toString(), Boolean.toString(SQLServerDriverBooleanProperty.MULTI_SUBNET_FAILOVER.getDefaultValue()), false, TRUE_FALSE), + new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.PACKET_SIZE.toString(), Integer.toString(SQLServerDriverIntProperty.PACKET_SIZE.getDefaultValue()), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.PASSWORD.toString(), SQLServerDriverStringProperty.PASSWORD.getDefaultValue(), true, null), + new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.PORT_NUMBER.toString(), Integer.toString(SQLServerDriverIntProperty.PORT_NUMBER.getDefaultValue()), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.QUERY_TIMEOUT.toString(), Integer.toString(SQLServerDriverIntProperty.QUERY_TIMEOUT.getDefaultValue()), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.RESPONSE_BUFFERING.toString(), SQLServerDriverStringProperty.RESPONSE_BUFFERING.getDefaultValue(), false, new String[] {"adaptive", "full"}), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SELECT_METHOD.toString(), SQLServerDriverStringProperty.SELECT_METHOD.getDefaultValue(), false, new String[] {"direct", "cursor"}), + new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.SEND_STRING_PARAMETERS_AS_UNICODE.toString(), Boolean.toString(SQLServerDriverBooleanProperty.SEND_STRING_PARAMETERS_AS_UNICODE.getDefaultValue()), false, TRUE_FALSE), + new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.SERVER_NAME_AS_ACE.toString(), Boolean.toString(SQLServerDriverBooleanProperty.SERVER_NAME_AS_ACE.getDefaultValue()), false, TRUE_FALSE), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SERVER_NAME.toString(), SQLServerDriverStringProperty.SERVER_NAME.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SERVER_SPN.toString(), SQLServerDriverStringProperty.SERVER_SPN.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.TRANSPARENT_NETWORK_IP_RESOLUTION.toString(), Boolean.toString(SQLServerDriverBooleanProperty.TRANSPARENT_NETWORK_IP_RESOLUTION.getDefaultValue()), false, TRUE_FALSE), + new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.TRUST_SERVER_CERTIFICATE.toString(), Boolean.toString(SQLServerDriverBooleanProperty.TRUST_SERVER_CERTIFICATE.getDefaultValue()), false, TRUE_FALSE), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.TRUST_STORE_TYPE.toString(), SQLServerDriverStringProperty.TRUST_STORE_TYPE.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.TRUST_STORE.toString(), SQLServerDriverStringProperty.TRUST_STORE.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.TRUST_STORE_PASSWORD.toString(), SQLServerDriverStringProperty.TRUST_STORE_PASSWORD.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.TRUST_MANAGER_CLASS.toString(), SQLServerDriverStringProperty.TRUST_MANAGER_CLASS.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.TRUST_MANAGER_CONSTRUCTOR_ARG.toString(), SQLServerDriverStringProperty.TRUST_MANAGER_CONSTRUCTOR_ARG.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.toString(), Boolean.toString(SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.getDefaultValue()), false, TRUE_FALSE), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.USER.toString(), SQLServerDriverStringProperty.USER.getDefaultValue(), true, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.WORKSTATION_ID.toString(), SQLServerDriverStringProperty.WORKSTATION_ID.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.XOPEN_STATES.toString(), Boolean.toString(SQLServerDriverBooleanProperty.XOPEN_STATES.getDefaultValue()), false, TRUE_FALSE), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.AUTHENTICATION_SCHEME.toString(), SQLServerDriverStringProperty.AUTHENTICATION_SCHEME.getDefaultValue(), false, new String[] {AuthenticationScheme.javaKerberos.toString(),AuthenticationScheme.nativeAuthentication.toString()}), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.AUTHENTICATION.toString(), SQLServerDriverStringProperty.AUTHENTICATION.getDefaultValue(), false, new String[] {SqlAuthentication.NotSpecified.toString(),SqlAuthentication.SqlPassword.toString(),SqlAuthentication.ActiveDirectoryPassword.toString(),SqlAuthentication.ActiveDirectoryIntegrated.toString()}), + new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.SOCKET_TIMEOUT.toString(), Integer.toString(SQLServerDriverIntProperty.SOCKET_TIMEOUT.getDefaultValue()), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.FIPS.toString(), Boolean.toString(SQLServerDriverBooleanProperty.FIPS.getDefaultValue()), false, TRUE_FALSE), + new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT.toString(), Boolean.toString(SQLServerDriverBooleanProperty.ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT.getDefaultValue()), false,TRUE_FALSE), + new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.SERVER_PREPARED_STATEMENT_DISCARD_THRESHOLD.toString(), Integer.toString(SQLServerDriverIntProperty.SERVER_PREPARED_STATEMENT_DISCARD_THRESHOLD.getDefaultValue()), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.STATEMENT_POOLING_CACHE_SIZE.toString(), Integer.toString(SQLServerDriverIntProperty.STATEMENT_POOLING_CACHE_SIZE.getDefaultValue()), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.JAAS_CONFIG_NAME.toString(), SQLServerDriverStringProperty.JAAS_CONFIG_NAME.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SSL_PROTOCOL.toString(), SQLServerDriverStringProperty.SSL_PROTOCOL.getDefaultValue(), false, new String[] {SSLProtocol.TLS.toString(), SSLProtocol.TLS_V10.toString(), SSLProtocol.TLS_V11.toString(), SSLProtocol.TLS_V12.toString()}), new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.CONNECT_RETRY_COUNT.toString(), Integer.toString(SQLServerDriverIntProperty.CONNECT_RETRY_COUNT.getDefaultValue()), false, null), new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.CONNECT_RETRY_INTERVAL.toString(), Integer.toString(SQLServerDriverIntProperty.CONNECT_RETRY_INTERVAL.getDefaultValue()), false, null), - }; + }; // Properties that can only be set by using Properties. // Cannot set in connection string @@ -493,8 +548,8 @@ static Properties mergeURLAndSuppliedProperties(Properties urlProps, return urlProps; Properties suppliedPropertiesFixed = fixupProperties(suppliedProperties); // Merge URL properties and supplied properties. - for (int i = 0; i < DRIVER_PROPERTIES.length; i++) { - String sProp = DRIVER_PROPERTIES[i].getName(); + for (SQLServerDriverPropertyInfo DRIVER_PROPERTY : DRIVER_PROPERTIES) { + String sProp = DRIVER_PROPERTY.getName(); String sPropVal = suppliedPropertiesFixed.getProperty(sProp); // supplied properties have precedence if (null != sPropVal) { // overwrite the property in urlprops if already exists. supp prop has more precedence @@ -503,8 +558,8 @@ static Properties mergeURLAndSuppliedProperties(Properties urlProps, } // Merge URL properties with property-only properties - for (int i = 0; i < DRIVER_PROPERTIES_PROPERTY_ONLY.length; i++) { - String sProp = DRIVER_PROPERTIES_PROPERTY_ONLY[i].getName(); + for (SQLServerDriverPropertyInfo aDRIVER_PROPERTIES_PROPERTY_ONLY : DRIVER_PROPERTIES_PROPERTY_ONLY) { + String sProp = aDRIVER_PROPERTIES_PROPERTY_ONLY.getName(); Object oPropVal = suppliedPropertiesFixed.get(sProp); // supplied properties have precedence if (null != oPropVal) { // overwrite the property in urlprops if already exists. supp prop has more precedence @@ -528,14 +583,14 @@ static String getNormalizedPropertyName(String name, if (null == name) return name; - for (int i = 0; i < driverPropertiesSynonyms.length; i++) { - if (driverPropertiesSynonyms[i][0].equalsIgnoreCase(name)) { - return driverPropertiesSynonyms[i][1]; + for (String[] driverPropertiesSynonym : driverPropertiesSynonyms) { + if (driverPropertiesSynonym[0].equalsIgnoreCase(name)) { + return driverPropertiesSynonym[1]; } } - for (int i = 0; i < DRIVER_PROPERTIES.length; i++) { - if (DRIVER_PROPERTIES[i].getName().equalsIgnoreCase(name)) { - return DRIVER_PROPERTIES[i].getName(); + for (SQLServerDriverPropertyInfo DRIVER_PROPERTY : DRIVER_PROPERTIES) { + if (DRIVER_PROPERTY.getName().equalsIgnoreCase(name)) { + return DRIVER_PROPERTY.getName(); } } @@ -557,9 +612,9 @@ static String getPropertyOnlyName(String name, if (null == name) return name; - for (int i = 0; i < DRIVER_PROPERTIES_PROPERTY_ONLY.length; i++) { - if (DRIVER_PROPERTIES_PROPERTY_ONLY[i].getName().equalsIgnoreCase(name)) { - return DRIVER_PROPERTIES_PROPERTY_ONLY[i].getName(); + for (SQLServerDriverPropertyInfo aDRIVER_PROPERTIES_PROPERTY_ONLY : DRIVER_PROPERTIES_PROPERTY_ONLY) { + if (aDRIVER_PROPERTIES_PROPERTY_ONLY.getName().equalsIgnoreCase(name)) { + return aDRIVER_PROPERTIES_PROPERTY_ONLY.getName(); } } @@ -647,25 +702,23 @@ static final DriverPropertyInfo[] getPropertyInfoFromProperties(Properties props public int getMajorVersion() { loggerExternal.entering(getClassNameLogging(), "getMajorVersion"); - loggerExternal.exiting(getClassNameLogging(), "getMajorVersion", new Integer(SQLJdbcVersion.major)); + loggerExternal.exiting(getClassNameLogging(), "getMajorVersion", SQLJdbcVersion.major); return SQLJdbcVersion.major; } public int getMinorVersion() { loggerExternal.entering(getClassNameLogging(), "getMinorVersion"); - loggerExternal.exiting(getClassNameLogging(), "getMinorVersion", new Integer(SQLJdbcVersion.minor)); + loggerExternal.exiting(getClassNameLogging(), "getMinorVersion", SQLJdbcVersion.minor); return SQLJdbcVersion.minor; } public Logger getParentLogger() throws SQLFeatureNotSupportedException { - DriverJDBCVersion.checkSupportsJDBC41(); - return parentLogger; } /* L0 */ public boolean jdbcCompliant() { loggerExternal.entering(getClassNameLogging(), "jdbcCompliant"); - loggerExternal.exiting(getClassNameLogging(), "jdbcCompliant", Boolean.valueOf(true)); + loggerExternal.exiting(getClassNameLogging(), "jdbcCompliant", Boolean.TRUE); return true; } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerEncryptionAlgorithmFactoryList.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerEncryptionAlgorithmFactoryList.java index 91c9a3973..a7eb1c0de 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerEncryptionAlgorithmFactoryList.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerEncryptionAlgorithmFactoryList.java @@ -21,7 +21,7 @@ final class SQLServerEncryptionAlgorithmFactoryList { private static final SQLServerEncryptionAlgorithmFactoryList instance = new SQLServerEncryptionAlgorithmFactoryList(); private SQLServerEncryptionAlgorithmFactoryList() { - encryptionAlgoFactoryMap = new ConcurrentHashMap(); + encryptionAlgoFactoryMap = new ConcurrentHashMap<>(); encryptionAlgoFactoryMap.putIfAbsent(SQLServerAeadAes256CbcHmac256Algorithm.algorithmName, new SQLServerAeadAes256CbcHmac256Factory()); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerEncryptionType.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerEncryptionType.java index 71ca022ee..3e7812e17 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerEncryptionType.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerEncryptionType.java @@ -20,7 +20,7 @@ enum SQLServerEncryptionType { Randomized ((byte) 2), PlainText ((byte) 0); - byte value; + final byte value; SQLServerEncryptionType(byte val) { this.value = val; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java index 527cac546..e8ed55d5a 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java @@ -103,14 +103,12 @@ final void setDriverErrorCode(int value) { if (exLogger.isLoggable(Level.FINE)) { StringBuilder sb = new StringBuilder(100); StackTraceElement st[] = this.getStackTrace(); - for (int i = 0; i < st.length; i++) - sb.append(st[i].toString()); + for (StackTraceElement aSt : st) sb.append(aSt.toString()); Throwable t = this.getCause(); if (t != null) { sb.append("\n caused by " + t + "\n"); StackTraceElement tst[] = t.getStackTrace(); - for (int i = 0; i < tst.length; i++) - sb.append(tst[i].toString()); + for (StackTraceElement aTst : tst) sb.append(aTst.toString()); } exLogger.fine(sb.toString()); } @@ -125,21 +123,22 @@ static String getErrString(String errCode) { * Make a new SQLException * * @param errText - * the excception message - * @param errState - * the excpeption state + * the exception message + * @param sqlState + * the statement * @param driverError + * the driver error object * @param cause * The exception that caused this exception */ - SQLServerException(String errText, + public SQLServerException(String errText, SQLState sqlState, DriverError driverError, Throwable cause) { this(errText, sqlState.getSQLStateCode(), driverError.getErrorCode(), cause); } - SQLServerException(String errText, + public SQLServerException(String errText, String errState, int errNum, Throwable cause) { @@ -149,7 +148,7 @@ static String getErrString(String errCode) { ActivityCorrelator.setCurrentActivityIdSentFlag(); // set the activityid flag so that we don't send the current ActivityId later. } - SQLServerException(String errText, + public SQLServerException(String errText, Throwable cause) { super(errText); initCause(cause); @@ -157,7 +156,7 @@ static String getErrString(String errCode) { ActivityCorrelator.setCurrentActivityIdSentFlag(); } - /* L0 */ SQLServerException(Object obj, + /* L0 */ public SQLServerException(Object obj, String errText, String errState, int errNum, @@ -171,6 +170,7 @@ static String getErrString(String errCode) { * Make a new SQLException * * @param obj + * the object * @param errText * the exception message * @param errState @@ -180,7 +180,7 @@ static String getErrString(String errCode) { * @param bStack * true to generate the stack trace */ - /* L0 */ SQLServerException(Object obj, + /* L0 */ public SQLServerException(Object obj, String errText, String errState, StreamError streamError, diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerJdbc41.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerJdbc41.java index f33c45c96..09bb912bf 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerJdbc41.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerJdbc41.java @@ -19,12 +19,6 @@ final class DriverJDBCVersion { static final int major = 4; static final int minor = 1; - static final void checkSupportsJDBC4() { - } - - static final void checkSupportsJDBC41() { - } - static final void checkSupportsJDBC42() { throw new UnsupportedOperationException(SQLServerException.getErrString("R_notSupported")); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerJdbc42.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerJdbc42.java index aaceef8bc..0d078cc1a 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerJdbc42.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerJdbc42.java @@ -22,12 +22,6 @@ final class DriverJDBCVersion { static final int major = 4; static final int minor = 2; - static final void checkSupportsJDBC4() { - } - - static final void checkSupportsJDBC41() { - } - static final void checkSupportsJDBC42() { } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerKeyVaultAuthenticationCallback.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerKeyVaultAuthenticationCallback.java deleted file mode 100644 index c940dd8eb..000000000 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerKeyVaultAuthenticationCallback.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Microsoft JDBC Driver for SQL Server - * - * Copyright(c) Microsoft Corporation All rights reserved. - * - * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. - */ - -package com.microsoft.sqlserver.jdbc; - -public interface SQLServerKeyVaultAuthenticationCallback { - - /** - * The authentication callback delegate which is to be implemented by the client code - * - * @param authority - * - Identifier of the authority, a URL. - * @param resource - * - Identifier of the target resource that is the recipient of the requested token, a URL. - * @param scope - * - The scope of the authentication request. - * @return access token - */ - public String getAccessToken(String authority, - String resource, - String scope); -} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMetaData.java index c2ee1460d..00614ff8b 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMetaData.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMetaData.java @@ -26,7 +26,8 @@ public class SQLServerMetaData { boolean isUniqueKey = false; SQLServerSortOrder sortOrder = SQLServerSortOrder.Unspecified; int sortOrdinal; - + private SQLCollation collation; + static final int defaultSortOrdinal = -1; /** @@ -186,6 +187,10 @@ public SQLServerSortOrder getSortOrder() { public int getSortOrdinal() { return sortOrdinal; } + + SQLCollation getCollation() { + return this.collation; + } void validateSortOrder() throws SQLServerException { // should specify both sort order and ordinal, or neither diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerNClob.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerNClob.java index af8e37347..213bc422b 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerNClob.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerNClob.java @@ -21,12 +21,12 @@ public final class SQLServerNClob extends SQLServerClobBase implements NClob { private static final Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.internals.SQLServerNClob"); SQLServerNClob(SQLServerConnection connection) { - super(connection, "", connection.getDatabaseCollation(), logger); + super(connection, "", connection.getDatabaseCollation(), logger, null); } SQLServerNClob(BaseInputStream stream, TypeInfo typeInfo) throws SQLServerException, UnsupportedEncodingException { - super(null, new String(stream.getBytes(), typeInfo.getCharset()), typeInfo.getSQLCollation(), logger); + super(null, stream, typeInfo.getSQLCollation(), logger , typeInfo); } final JDBCType getJdbcType() { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java index d4fb480a8..dca580ec9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java @@ -9,11 +9,13 @@ package com.microsoft.sqlserver.jdbc; import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.text.MessageFormat; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.StringTokenizer; @@ -41,6 +43,8 @@ public final class SQLServerParameterMetaData implements ParameterMetaData { /* Used for callable statement meta data */ private Statement stmtCall; private SQLServerResultSet rsProcedureMeta; + + protected boolean procedureIsFound = false; static final private java.util.logging.Logger logger = java.util.logging.Logger .getLogger("com.microsoft.sqlserver.jdbc.internals.SQLServerParameterMetaData"); @@ -48,6 +52,9 @@ public final class SQLServerParameterMetaData implements ParameterMetaData { static private final AtomicInteger baseID = new AtomicInteger(0); // Unique id generator for each instance (used for logging). final private String traceID = " SQLServerParameterMetaData:" + nextInstanceID(); boolean isTVP = false; + + private String stringToParse = null; + private int indexToBeginParse = -1; // Returns unique id for each instance. private static int nextInstanceID() { @@ -70,10 +77,11 @@ final public String toString() { * the list of columns * @param columnStartToken * the token that prfixes the column set + * @throws SQLServerException */ /* L2 */ private String parseColumns(String columnSet, - String columnStartToken) { - StringTokenizer st = new StringTokenizer(columnSet, " =?<>!", true); + String columnStartToken) throws SQLServerException { + StringTokenizer st = new StringTokenizer(columnSet, " =?<>!\r\n\t\f", true); final int START = 0; final int PARAMNAME = 1; final int PARAMVALUE = 2; @@ -82,9 +90,12 @@ final public String toString() { String sLastField = null; StringBuilder sb = new StringBuilder(); + int sTokenIndex = 0; while (st.hasMoreTokens()) { String sToken = st.nextToken(); + sTokenIndex = sTokenIndex + sToken.length(); + if (sToken.equalsIgnoreCase(columnStartToken)) { nState = PARAMNAME; continue; @@ -117,6 +128,7 @@ final public String toString() { } } + indexToBeginParse = sTokenIndex; return sb.toString(); } @@ -127,16 +139,20 @@ final public String toString() { * the sql syntax * @param columnMarker * the token that denotes the start of the column set + * @throws SQLServerException */ /* L2 */ private String parseInsertColumns(String sql, - String columnMarker) { + String columnMarker) throws SQLServerException { StringTokenizer st = new StringTokenizer(sql, " (),", true); int nState = 0; String sLastField = null; StringBuilder sb = new StringBuilder(); + int sTokenIndex = 0; while (st.hasMoreTokens()) { String sToken = st.nextToken(); + sTokenIndex = sTokenIndex + sToken.length(); + if (sToken.equalsIgnoreCase(columnMarker)) { nState = 1; continue; @@ -160,12 +176,18 @@ final public String toString() { } if (nState == 1) { if (sToken.trim().length() > 0) { - if (sToken.charAt(0) != ',') + if (sToken.charAt(0) != ',') { sLastField = escapeParse(st, sToken); + + // in case the parameter has braces in its name, e.g. [c2_nvarchar(max)], the original sToken variable just + // contains [c2_nvarchar, sLastField actually has the whole name [c2_nvarchar(max)] + sTokenIndex = sTokenIndex + (sLastField.length() - sToken.length()); + } } } } + indexToBeginParse = sTokenIndex; return sb.toString(); } @@ -230,7 +252,7 @@ else if (SSType.Category.CHARACTER == ssType.category || SSType.Category.BINARY } catch (NumberFormatException e) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_metaDataErrorForParameter")); - Object[] msgArgs = {new Integer(paramOrdinal)}; + Object[] msgArgs = {paramOrdinal}; SQLServerException.makeFromDriverError(con, stmtParent, form.format(msgArgs) + " " + e.toString(), null, false); } } @@ -319,23 +341,29 @@ private void parseQueryMetaFor2008(ResultSet rsQueryMeta) throws SQLServerExcept * @param st * string tokenizer * @param firstToken + * @throws SQLServerException * @returns the full token */ private String escapeParse(StringTokenizer st, - String firstToken) { - String nameFragment; - String fullName; - nameFragment = firstToken; + String firstToken) throws SQLServerException { + + if (null == firstToken) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_NullValue")); + Object[] msgArgs1 = {"firstToken"}; + throw new SQLServerException(form.format(msgArgs1), null); + } + // skip spaces - while (nameFragment.equals(" ") && st.hasMoreTokens()) { - nameFragment = st.nextToken(); + while ((0 == firstToken.trim().length()) && st.hasMoreTokens()) { + firstToken = st.nextToken(); } - fullName = nameFragment; - if (nameFragment.charAt(0) == '[' && nameFragment.charAt(nameFragment.length() - 1) != ']') { + + String fullName = firstToken; + if (firstToken.charAt(0) == '[' && firstToken.charAt(firstToken.length() - 1) != ']') { while (st.hasMoreTokens()) { - nameFragment = st.nextToken(); - fullName = fullName.concat(nameFragment); - if (nameFragment.charAt(nameFragment.length() - 1) == ']') { + firstToken = st.nextToken(); + fullName = fullName.concat(firstToken); + if (firstToken.charAt(firstToken.length() - 1) == ']') { break; } @@ -363,10 +391,11 @@ private class MetaInfo { * String * @param sTableMarker * the location of the table in the syntax + * @throws SQLServerException */ private MetaInfo parseStatement(String sql, - String sTableMarker) { - StringTokenizer st = new StringTokenizer(sql, " ,", true); + String sTableMarker) throws SQLServerException { + StringTokenizer st = new StringTokenizer(sql, " ,\r\n\t\f(", true); /* Find the table */ @@ -375,6 +404,10 @@ private MetaInfo parseStatement(String sql, while (st.hasMoreTokens()) { String sToken = st.nextToken().trim(); + if(sToken.contains("*/")){ + sToken = removeCommentsInTheBeginning(sToken, 0, 0, "/*", "*/"); + } + if (sToken.equalsIgnoreCase(sTableMarker)) { if (st.hasMoreTokens()) { metaTable = escapeParse(st, st.nextToken()); @@ -384,12 +417,24 @@ private MetaInfo parseStatement(String sql, } if (null != metaTable) { - if (sTableMarker.equalsIgnoreCase("UPDATE")) + if (sTableMarker.equalsIgnoreCase("UPDATE")) { metaFields = parseColumns(sql, "SET"); // Get the set fields - else if (sTableMarker.equalsIgnoreCase("INTO")) // insert + stringToParse = ""; + } + else if (sTableMarker.equalsIgnoreCase("INTO")) { // insert metaFields = parseInsertColumns(sql, "("); // Get the value fields - else + stringToParse = sql.substring(indexToBeginParse); // the index of ')' + + // skip VALUES() clause + if (stringToParse.trim().toLowerCase().startsWith("values")) { + parseInsertColumns(stringToParse, "("); + stringToParse = stringToParse.substring(indexToBeginParse); // the index of ')' + } + } + else { metaFields = parseColumns(sql, "WHERE"); // Get the where fields + stringToParse = ""; + } return new MetaInfo(metaTable, metaFields); } @@ -405,10 +450,22 @@ else if (sTableMarker.equalsIgnoreCase("INTO")) // insert * @throws SQLServerException */ private MetaInfo parseStatement(String sql) throws SQLServerException { - StringTokenizer st = new StringTokenizer(sql, " "); + StringTokenizer st = new StringTokenizer(sql, " \r\n\t\f"); if (st.hasMoreTokens()) { String sToken = st.nextToken().trim(); + // filter out multiple line comments in the beginning of the query + if (sToken.contains("/*")) { + String sqlWithoutCommentsInBeginning = removeCommentsInTheBeginning(sql, 0, 0, "/*", "*/"); + return parseStatement(sqlWithoutCommentsInBeginning); + } + + // filter out single line comments in the beginning of the query + if (sToken.contains("--")) { + String sqlWithoutCommentsInBeginning = removeCommentsInTheBeginning(sql, 0, 0, "--", "\n"); + return parseStatement(sqlWithoutCommentsInBeginning); + } + if (sToken.equalsIgnoreCase("INSERT")) return parseStatement(sql, "INTO"); // INTO marks the table name @@ -424,82 +481,71 @@ private MetaInfo parseStatement(String sql) throws SQLServerException { return null; } + + private String removeCommentsInTheBeginning(String sql, + int startCommentMarkCount, + int endCommentMarkCount, + String startMark, + String endMark) { + int startCommentMarkIndex = sql.indexOf(startMark); + int endCommentMarkIndex = sql.indexOf(endMark); - String parseThreePartNames(String threeName) throws SQLServerException { - int noofitems = 0; - String procedureName = null; - String procedureOwner = null; - String procedureQualifier = null; - StringTokenizer st = new StringTokenizer(threeName, ".", true); + if (-1 == startCommentMarkIndex) { + startCommentMarkIndex = Integer.MAX_VALUE; + } + if (-1 == endCommentMarkIndex) { + endCommentMarkIndex = Integer.MAX_VALUE; + } - // parse left to right looking for three part name - // note the user can provide three part, two part or one part name - while (st.hasMoreTokens()) { - String sToken = st.nextToken(); - String nextItem = escapeParse(st, sToken); - if (nextItem.equals(".") == false) { - switch (noofitems) { - case 2: - procedureQualifier = procedureOwner; - procedureOwner = procedureName; - procedureName = nextItem; - noofitems++; - break; - case 1: - procedureOwner = procedureName; - procedureName = nextItem; - noofitems++; - break; - case 0: - procedureName = nextItem; - noofitems++; - break; - default: - noofitems++; - break; - } + // Base case. startCommentMarkCount is guaranteed to be bigger than 0 because the method is called when /* occurs + if (startCommentMarkCount == endCommentMarkCount) { + if (startCommentMarkCount != 0 && endCommentMarkCount != 0) { + return sql; } } - StringBuilder sb = new StringBuilder(100); - if (noofitems > 3 && 1 < noofitems) - SQLServerException.makeFromDriverError(con, stmtParent, SQLServerException.getErrString("R_noMetadata"), null, false); + // filter out first start comment mark + if (startCommentMarkIndex < endCommentMarkIndex) { + String sqlWithoutCommentsInBeginning = sql.substring(startCommentMarkIndex + startMark.length()); + return removeCommentsInTheBeginning(sqlWithoutCommentsInBeginning, ++startCommentMarkCount, endCommentMarkCount, startMark, endMark); + } + // filter out first end comment mark + else { + if (Integer.MAX_VALUE == endCommentMarkIndex) { + return sql; + } - switch (noofitems) { - case 3: - sb.append("@procedure_qualifier ="); - sb.append(procedureQualifier); - sb.append(", "); - sb.append("@procedure_owner ="); - sb.append(procedureOwner); - sb.append(", "); - sb.append("@procedure_name ="); - sb.append(procedureName); - sb.append(", "); - break; + String sqlWithoutCommentsInBeginning = sql.substring(endCommentMarkIndex + endMark.length()); + return removeCommentsInTheBeginning(sqlWithoutCommentsInBeginning, startCommentMarkCount, ++endCommentMarkCount, startMark, endMark); + } + } - case 2: - sb.append("@procedure_owner ="); - sb.append(procedureOwner); - sb.append(", "); - sb.append("@procedure_name ="); - sb.append(procedureName); - sb.append(", "); - break; - case 1: - sb.append("@procedure_name ="); - sb.append(procedureName); - sb.append(", "); - break; - default: - break; + String parseProcIdentifier(String procIdentifier) throws SQLServerException { + ThreePartName threePartName = ThreePartName.parse(procIdentifier); + StringBuilder sb = new StringBuilder(); + if (threePartName.getDatabasePart() != null) { + sb.append("@procedure_qualifier="); + sb.append(threePartName.getDatabasePart()); + sb.append(", "); + } + if (threePartName.getOwnerPart() != null) { + sb.append("@procedure_owner="); + sb.append(threePartName.getOwnerPart()); + sb.append(", "); + } + if (threePartName.getProcedurePart() != null) { + sb.append("@procedure_name="); + sb.append(threePartName.getProcedurePart()); + } else { + SQLServerException.makeFromDriverError(con, stmtParent, SQLServerException.getErrString("R_noMetadata"), null, false); } return sb.toString(); - } private void checkClosed() throws SQLServerException { - stmtParent.checkClosed(); + // stmtParent does not seem to be re-used, should just verify connection is not closed. + // stmtParent.checkClosed(); + con.checkClosed(); } /** @@ -517,6 +563,8 @@ private void checkClosed() throws SQLServerException { assert null != st; stmtParent = st; con = st.connection; + SQLServerStatement s = null; + SQLServerStatement stmt = null; if (logger.isLoggable(java.util.logging.Level.FINE)) { logger.fine(toString() + " created by (" + st.toString() + ")"); } @@ -525,12 +573,22 @@ private void checkClosed() throws SQLServerException { // If the CallableStatement/PreparedStatement is a stored procedure call // then we can extract metadata using sp_sproc_columns if (null != st.procedureName) { - SQLServerStatement s = (SQLServerStatement) con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); - String sProc = parseThreePartNames(st.procedureName); + s = (SQLServerStatement) con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); + String sProc = parseProcIdentifier(st.procedureName); if (con.isKatmaiOrLater()) - rsProcedureMeta = s.executeQueryInternal("exec sp_sproc_columns_100 " + sProc + " @ODBCVer=3"); + rsProcedureMeta = s.executeQueryInternal("exec sp_sproc_columns_100 " + sProc + ", @ODBCVer=3"); else - rsProcedureMeta = s.executeQueryInternal("exec sp_sproc_columns " + sProc + " @ODBCVer=3"); + rsProcedureMeta = s.executeQueryInternal("exec sp_sproc_columns " + sProc + ", @ODBCVer=3"); + + // if rsProcedureMeta has next row, it means the stored procedure is found + if (rsProcedureMeta.next()) { + procedureIsFound = true; + } + else { + procedureIsFound = false; + } + rsProcedureMeta.beforeFirst(); + // Sixth is DATA_TYPE rsProcedureMeta.getColumn(6).setFilter(new DataTypeFilter()); if (con.isKatmaiOrLater()) { @@ -545,7 +603,7 @@ private void checkClosed() throws SQLServerException { // procedure "sp_describe_undeclared_parameters" to retrieve parameter meta data // if SQL server version is 2008, then use FMTONLY else { - queryMetaMap = new HashMap(); + queryMetaMap = new HashMap<>(); if (con.getServerMajorVersion() >= SQL_SERVER_2012_VERSION) { // new implementation for SQL verser 2012 and above @@ -559,38 +617,80 @@ private void checkClosed() throws SQLServerException { } else { // old implementation for SQL server 2008 - MetaInfo metaInfo = parseStatement(sProcString); - if (null == metaInfo) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_cantIdentifyTableMetadata")); - Object[] msgArgs = {sProcString}; - SQLServerException.makeFromDriverError(con, stmtParent, form.format(msgArgs), null, false); - } + stringToParse = sProcString; + ArrayList metaInfoList = new ArrayList<>(); + + while (stringToParse.length() > 0) { + MetaInfo metaInfo = parseStatement(stringToParse); + if (null == metaInfo) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_cantIdentifyTableMetadata")); + Object[] msgArgs = {stringToParse}; + SQLServerException.makeFromDriverError(con, stmtParent, form.format(msgArgs), null, false); + } - if (metaInfo.fields.length() <= 0) + metaInfoList.add(metaInfo); + } + if (metaInfoList.size() <= 0 || metaInfoList.get(0).fields.length() <= 0) { return; + } + + StringBuilder sbColumns = new StringBuilder(); + + for (MetaInfo mi : metaInfoList) { + sbColumns = sbColumns.append(mi.fields + ","); + } + sbColumns.deleteCharAt(sbColumns.length() - 1); + + String columns = sbColumns.toString(); + + StringBuilder sbTablesAndJoins = new StringBuilder(); + for (int i = 0; i < metaInfoList.size(); i++) { + if (i == 0) { + sbTablesAndJoins = sbTablesAndJoins.append(metaInfoList.get(i).table); + } + else { + if (metaInfoList.get(i).table.equals(metaInfoList.get(i - 1).table) + && metaInfoList.get(i).fields.equals(metaInfoList.get(i - 1).fields)) { + continue; + } + sbTablesAndJoins = sbTablesAndJoins + .append(" LEFT JOIN " + metaInfoList.get(i).table + " ON " + metaInfoList.get(i - 1).table + "." + + metaInfoList.get(i - 1).fields + "=" + metaInfoList.get(i).table + "." + metaInfoList.get(i).fields); + } + } + + String tablesAndJoins = sbTablesAndJoins.toString(); + + stmt = (SQLServerStatement) con.createStatement(); + String sCom = "sp_executesql N'SET FMTONLY ON SELECT " + columns + " FROM " + tablesAndJoins + " '"; - Statement stmt = con.createStatement(); - String sCom = "sp_executesql N'SET FMTONLY ON SELECT " + metaInfo.fields + " FROM " + metaInfo.table + " WHERE 1 = 2'"; ResultSet rs = stmt.executeQuery(sCom); parseQueryMetaFor2008(rs); - stmt.close(); - rs.close(); } } } + // Do not need to wrapper SQLServerException again + catch (SQLServerException e) { + throw e; + } catch (SQLException e) { SQLServerException.makeFromDriverError(con, stmtParent, e.toString(), null, false); } + catch(StringIndexOutOfBoundsException e){ + SQLServerException.makeFromDriverError(con, stmtParent, e.toString(), null, false); + } + finally { + if (null != stmt) + stmt.close(); + } } public boolean isWrapperFor(Class iface) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); boolean f = iface.isInstance(this); return f; } public T unwrap(Class iface) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); T t; try { t = iface.cast(this); @@ -613,12 +713,12 @@ public T unwrap(Class iface) throws SQLException { } catch (SQLException e) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_metaDataErrorForParameter")); - Object[] msgArgs = {new Integer(param)}; + Object[] msgArgs = {param}; SQLServerException.makeFromDriverError(con, stmtParent, form.format(msgArgs) + " " + e.toString(), null, false); } if (!bFound) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidParameterNumber")); - Object[] msgArgs = {new Integer(param)}; + Object[] msgArgs = {param}; SQLServerException.makeFromDriverError(con, stmtParent, form.format(msgArgs), null, false); } } @@ -693,7 +793,7 @@ public T unwrap(Class iface) throws SQLException { } catch (SQLException e) { SQLServerException.makeFromDriverError(con, stmtParent, e.toString(), null, false); - return 0; + return parameterModeUnknown; } } @@ -813,7 +913,7 @@ public T unwrap(Class iface) throws SQLException { } catch (SQLException e) { SQLServerException.makeFromDriverError(con, stmtParent, e.toString(), null, false); - return 0; + return parameterNoNulls; } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java index 7b22f529e..cc059434d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java @@ -38,7 +38,7 @@ public class SQLServerPooledConnection implements PooledConnection { SQLServerPooledConnection(SQLServerDataSource ds, String user, String password) throws SQLException { - listeners = new Vector(); + listeners = new Vector<>(); // Piggyback SQLServerDataSource logger for now. pcLogger = SQLServerDataSource.dsLogger; @@ -198,15 +198,11 @@ public void removeConnectionEventListener(ConnectionEventListener listener) { } public void addStatementEventListener(StatementEventListener listener) { - DriverJDBCVersion.checkSupportsJDBC4(); - // Not implemented throw new UnsupportedOperationException(SQLServerException.getErrString("R_notSupported")); } public void removeStatementEventListener(StatementEventListener listener) { - DriverJDBCVersion.checkSupportsJDBC4(); - // Not implemented throw new UnsupportedOperationException(SQLServerException.getErrString("R_notSupported")); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index d9a791d03..9ae3e0e7a 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -6,7 +6,10 @@ * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. */ -package com.microsoft.sqlserver.jdbc; +package com.microsoft.sqlserver.jdbc; + +import static com.microsoft.sqlserver.jdbc.SQLServerConnection.getCachedParsedSQL; +import static com.microsoft.sqlserver.jdbc.SQLServerConnection.parseAndCacheSQL; import java.io.InputStream; import java.io.Reader; @@ -29,6 +32,9 @@ import java.util.Vector; import java.util.logging.Level; +import com.microsoft.sqlserver.jdbc.SQLServerConnection.PreparedStatementHandle; +import com.microsoft.sqlserver.jdbc.SQLServerConnection.Sha1HashKey; + /** * SQLServerPreparedStatement provides JDBC prepared statement functionality. SQLServerPreparedStatement provides methods for the user to supply * parameters as any native Java type and many Java object types. @@ -52,18 +58,24 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS private static final int BATCH_STATEMENT_DELIMITER_TDS_72 = 0xFF; final int nBatchStatementDelimiter = BATCH_STATEMENT_DELIMITER_TDS_72; - /** the user's prepared sql syntax */ - private String sqlCommand; - /** The prepared type definitions */ private String preparedTypeDefinitions; - /** The users SQL statement text */ + /** Processed SQL statement text, may not be same as what user initially passed. */ final String userSQL; /** SQL statement with expanded parameter tokens */ private String preparedSQL; + /** True if this execute has been called for this statement at least once */ + private boolean isExecutedAtLeastOnce = false; + + /** Reference to cache item for statement handle pooling. Only used to decrement ref count on statement close. */ + private PreparedStatementHandle cachedPreparedStatementHandle; + + /** Hash of user supplied SQL statement used for various cache lookups */ + private Sha1HashKey sqlTextCacheKey; + /** * Array with parameter names generated in buildParamTypeDefinitions For mapping encryption information to parameters, as the second result set * returned by sp_describe_parameter_encryption doesn't depend on order of input parameter @@ -92,9 +104,47 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS /** The prepared statement handle returned by the server */ private int prepStmtHandle = 0; + + /** Statement used for getMetadata(). Declared as a field to facilitate closing the statement. */ + private SQLServerStatement internalStmt = null; + + private void setPreparedStatementHandle(int handle) { + this.prepStmtHandle = handle; + } + + /** The server handle for this prepared statement. If a value {@literal <} 1 is returned no handle has been created. + * + * @return + * Per the description. + * @throws SQLServerException when an error occurs + */ + public int getPreparedStatementHandle() throws SQLServerException { + checkClosed(); + return prepStmtHandle; + } + + /** Returns true if this statement has a server handle. + * + * @return + * Per the description. + */ + private boolean hasPreparedStatementHandle() { + return 0 < prepStmtHandle; + } + + /** Resets the server handle for this prepared statement to no handle. + */ + private void resetPrepStmtHandle() { + prepStmtHandle = 0; + } /** Flag set to true when statement execution is expected to return the prepared statement handle */ private boolean expectPrepStmtHandle = false; + + /** + * Flag set to true when all encryption metadata of inOutParam is retrieved + */ + private boolean encryptionMetadataIsRetrieved = false; // Internal function used in tracing String getClassNameInternal() { @@ -123,65 +173,98 @@ String getClassNameInternal() { int nRSConcur, SQLServerStatementColumnEncryptionSetting stmtColEncSetting) throws SQLServerException { super(conn, nRSType, nRSConcur, stmtColEncSetting); + + if (null == sql) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_NullValue")); + Object[] msgArgs1 = {"Statement SQL"}; + throw new SQLServerException(form.format(msgArgs1), null); + } + stmtPoolable = true; - sqlCommand = sql; - JDBCSyntaxTranslator translator = new JDBCSyntaxTranslator(); - sql = translator.translate(sql); - procedureName = translator.getProcedureName(); // may return null - bReturnValueSyntax = translator.hasReturnValueSyntax(); + // Create a cache key for this statement. + sqlTextCacheKey = new Sha1HashKey(sql); + + // Parse or fetch SQL metadata from cache. + ParsedSQLCacheItem parsedSQL = getCachedParsedSQL(sqlTextCacheKey); + if(null != parsedSQL) { + isExecutedAtLeastOnce = true; + } + else { + parsedSQL = parseAndCacheSQL(sqlTextCacheKey, sql); + } - userSQL = sql; - initParams(userSQL); + // Retrieve meta data from cache item. + procedureName = parsedSQL.procedureName; + bReturnValueSyntax = parsedSQL.bReturnValueSyntax; + userSQL = parsedSQL.processedSQL; + initParams(parsedSQL.parameterCount); } /** * Close the prepared statement's prepared handle. */ private void closePreparedHandle() { - if (0 == prepStmtHandle) + if (!hasPreparedStatementHandle()) return; // If the connection is already closed, don't bother trying to close // the prepared handle. We won't be able to, and it's already closed // on the server anyway. if (connection.isSessionUnAvailable()) { - if (getStatementLogger().isLoggable(java.util.logging.Level.FINER)) - getStatementLogger().finer(this + ": Not closing PreparedHandle:" + prepStmtHandle + "; connection is already closed."); + if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) + loggerExternal.finer(this + ": Not closing PreparedHandle:" + prepStmtHandle + "; connection is already closed."); } else { - if (getStatementLogger().isLoggable(java.util.logging.Level.FINER)) - getStatementLogger().finer(this + ": Closing PreparedHandle:" + prepStmtHandle); + isExecutedAtLeastOnce = false; + final int handleToClose = prepStmtHandle; + resetPrepStmtHandle(); + + // Handle unprepare actions through statement pooling. + if (null != cachedPreparedStatementHandle) { + connection.returnCachedPreparedStatementHandle(cachedPreparedStatementHandle); + } + // If no reference to a statement pool cache item is found handle unprepare actions through batching @ connection level. + else if(connection.isPreparedStatementUnprepareBatchingEnabled()) { + connection.enqueueUnprepareStatementHandle(connection.new PreparedStatementHandle(null, handleToClose, executedSqlDirectly, true)); + } + else { + // Non batched behavior (same as pre batch clean-up implementation) + if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) + loggerExternal.finer(this + ": Closing PreparedHandle:" + handleToClose); + + final class PreparedHandleClose extends UninterruptableTDSCommand { + PreparedHandleClose() { + super("closePreparedHandle"); + } - final class PreparedHandleClose extends UninterruptableTDSCommand { - PreparedHandleClose() { - super("closePreparedHandle"); + final boolean doExecute() throws SQLServerException { + TDSWriter tdsWriter = startRequest(TDS.PKT_RPC); + tdsWriter.writeShort((short) 0xFFFF); // procedure name length -> use ProcIDs + tdsWriter.writeShort(executedSqlDirectly ? TDS.PROCID_SP_UNPREPARE : TDS.PROCID_SP_CURSORUNPREPARE); + tdsWriter.writeByte((byte) 0); // RPC procedure option 1 + tdsWriter.writeByte((byte) 0); // RPC procedure option 2 + tdsWriter.writeRPCInt(null, handleToClose, false); + TDSParser.parse(startResponse(), getLogContext()); + return true; + } } - final boolean doExecute() throws SQLServerException { - TDSWriter tdsWriter = startRequest(TDS.PKT_RPC); - tdsWriter.writeShort((short) 0xFFFF); // procedure name length -> use ProcIDs - tdsWriter.writeShort(executedSqlDirectly ? TDS.PROCID_SP_UNPREPARE : TDS.PROCID_SP_CURSORUNPREPARE); - tdsWriter.writeByte((byte) 0); // RPC procedure option 1 - tdsWriter.writeByte((byte) 0); // RPC procedure option 2 - tdsWriter.writeRPCInt(null, new Integer(prepStmtHandle), false); - prepStmtHandle = 0; - TDSParser.parse(startResponse(), getLogContext()); - return true; + // Try to close the server cursor. Any failure is caught, logged, and ignored. + try { + executeCommand(new PreparedHandleClose()); + } + catch (SQLServerException e) { + if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) + loggerExternal.log(Level.FINER, this + ": Error (ignored) closing PreparedHandle:" + handleToClose, e); } - } - // Try to close the server cursor. Any failure is caught, logged, and ignored. - try { - executeCommand(new PreparedHandleClose()); - } - catch (SQLServerException e) { - if (getStatementLogger().isLoggable(java.util.logging.Level.FINER)) - getStatementLogger().log(Level.FINER, this + ": Error (ignored) closing PreparedHandle:" + prepStmtHandle, e); + if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) + loggerExternal.finer(this + ": Closed PreparedHandle:" + handleToClose); } - if (getStatementLogger().isLoggable(java.util.logging.Level.FINER)) - getStatementLogger().finer(this + ": Closed PreparedHandle:" + prepStmtHandle); + // Always run any outstanding discard actions as statement pooling always uses batched sp_unprepare. + connection.unprepareUnreferencedPreparedStatementHandles(false); } } @@ -197,25 +280,30 @@ final void closeInternal() { // If we have a prepared statement handle, close it. closePreparedHandle(); + + // Close the statement that was used to generate empty statement from getMetadata(). + try { + if (null != internalStmt) + internalStmt.close(); + } catch (SQLServerException e) { + if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) + loggerExternal.finer("Ignored error closing internal statement: " + e.getErrorCode() + " " + e.getMessage()); + } + finally { + internalStmt = null; + } // Clean up client-side state batchParamValues = null; } - /** + /** * Intialize the statement parameters. * - * @param sql + * @param nParams + * Number of parameters to Intialize. */ - /* L0 */ final void initParams(String sql) { - int nParams = 0; - - // Figure out the expected number of parameters by counting the - // parameter placeholders in the SQL string. - int offset = -1; - while ((offset = ParameterUtils.scanSQLForChar('?', sql, ++offset)) < sql.length()) - ++nParams; - + /* L0 */ final void initParams(int nParams) { inOutParam = new Parameter[nParams]; for (int i = 0; i < nParams; i++) { inOutParam[i] = new Parameter(Util.shouldHonorAEForParameters(stmtColumnEncriptionSetting, connection)); @@ -225,6 +313,7 @@ final void closeInternal() { /* L0 */ public final void clearParameters() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "clearParameters"); checkClosed(); + encryptionMetadataIsRetrieved = false; int i; if (inOutParam == null) return; @@ -270,7 +359,7 @@ private String buildParamTypeDefinitions(Parameter[] params, StringBuilder sb = new StringBuilder(); int nCols = params.length; char cParamName[] = new char[10]; - parameterNames = new ArrayList(); + parameterNames = new ArrayList<>(); for (int i = 0; i < nCols; i++) { if (i > 0) @@ -287,7 +376,7 @@ private String buildParamTypeDefinitions(Parameter[] params, String typeDefinition = params[i].getTypeDefinition(connection, resultsReader()); if (null == typeDefinition) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_valueNotSetForParameter")); - Object[] msgArgs = {new Integer(i + 1)}; + Object[] msgArgs = {i + 1}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, false); } @@ -343,7 +432,7 @@ public int executeUpdate() throws SQLServerException { if (updateCount < Integer.MIN_VALUE || updateCount > Integer.MAX_VALUE) SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_updateCountOutofRange"), null, true); - loggerExternal.exiting(getClassNameLogging(), "executeUpdate", new Long(updateCount)); + loggerExternal.exiting(getClassNameLogging(), "executeUpdate", updateCount); return (int) updateCount; } @@ -357,7 +446,7 @@ public long executeLargeUpdate() throws SQLServerException { } checkClosed(); executeStatement(new PrepStmtExecCmd(this, EXECUTE_UPDATE)); - loggerExternal.exiting(getClassNameLogging(), "executeLargeUpdate", new Long(updateCount)); + loggerExternal.exiting(getClassNameLogging(), "executeLargeUpdate", updateCount); return updateCount; } @@ -375,7 +464,7 @@ public boolean execute() throws SQLServerException { } checkClosed(); executeStatement(new PrepStmtExecCmd(this, EXECUTE)); - loggerExternal.exiting(getClassNameLogging(), "execute", Boolean.valueOf(null != resultSet)); + loggerExternal.exiting(getClassNameLogging(), "execute", null != resultSet); return null != resultSet; } @@ -419,27 +508,54 @@ final void doExecutePreparedStatement(PrepStmtExecCmd command) throws SQLServerE loggerExternal.finer(toString() + " ActivityId: " + ActivityCorrelator.getNext().toString()); } - boolean hasNewTypeDefinitions = buildPreparedStrings(inOutParam, false); + boolean hasExistingTypeDefinitions = preparedTypeDefinitions != null; + boolean hasNewTypeDefinitions = true; + if (!encryptionMetadataIsRetrieved) { + hasNewTypeDefinitions = buildPreparedStrings(inOutParam, false); + } + if ((Util.shouldHonorAEForParameters(stmtColumnEncriptionSetting, connection)) && (0 < inOutParam.length) && !isInternalEncryptionQuery) { - getParameterEncryptionMetadata(inOutParam); - // maxRows is set to 0 when retreving encryption metadata, - // need to set it back - setMaxRowsAndMaxFieldSize(); + // retrieve paramater encryption metadata if they are not retrieved yet + if (!encryptionMetadataIsRetrieved) { + getParameterEncryptionMetadata(inOutParam); + encryptionMetadataIsRetrieved = true; + + // maxRows is set to 0 when retreving encryption metadata, + // need to set it back + setMaxRowsAndMaxFieldSize(); + } // fix an issue when inserting unicode into non-encrypted nchar column using setString() and AE is on on Connection - buildPreparedStrings(inOutParam, true); + hasNewTypeDefinitions = buildPreparedStrings(inOutParam, true); } - // Start the request and detach the response reader so that we can - // continue using it after we return. - TDSWriter tdsWriter = command.startRequest(TDS.PKT_RPC); - - doPrepExec(tdsWriter, inOutParam, hasNewTypeDefinitions); - - ensureExecuteResultsReader(command.startResponse(getIsResponseBufferingAdaptive())); - startResults(); - getNextResult(); + // Retry execution if existing handle could not be re-used. + for(int attempt = 1; attempt <= 2; ++attempt) { + try { + // Re-use handle if available, requires parameter definitions which are not available until here. + if (reuseCachedHandle(hasNewTypeDefinitions, 1 < attempt)) { + hasNewTypeDefinitions = false; + } + + // Start the request and detach the response reader so that we can + // continue using it after we return. + TDSWriter tdsWriter = command.startRequest(TDS.PKT_RPC); + + doPrepExec(tdsWriter, inOutParam, hasNewTypeDefinitions, hasExistingTypeDefinitions); + + ensureExecuteResultsReader(command.startResponse(getIsResponseBufferingAdaptive())); + startResults(); + getNextResult(); + } + catch(SQLException e) { + if (retryBasedOnFailedReuseOfCachedHandle(e, attempt)) + continue; + else + throw e; + } + break; + } if (EXECUTE_QUERY == executeMethod && null == resultSet) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_noResultset"), null, true); @@ -449,6 +565,15 @@ else if (EXECUTE_UPDATE == executeMethod && null != resultSet) { } } + /** Should the execution be retried because the re-used cached handle could not be re-used due to server side state changes? */ + private boolean retryBasedOnFailedReuseOfCachedHandle(SQLException e, int attempt) { + // Only retry based on these error codes: + // 586: The prepared statement handle %d is not valid in this context. Please verify that current database, user default schema, and ANSI_NULLS and QUOTED_IDENTIFIER set options are not changed since the handle is prepared. + // 8179: Could not find prepared statement with handle %d. + // 99586: Error used for testing. + return 1 == attempt && (586 == e.getErrorCode() || 8179 == e.getErrorCode() || 99586 == e.getErrorCode()); + } + /** * Consume the OUT parameter for the statement object itself. * @@ -469,7 +594,14 @@ boolean onRetValue(TDSReader tdsReader) throws SQLServerException { expectPrepStmtHandle = false; Parameter param = new Parameter(Util.shouldHonorAEForParameters(stmtColumnEncriptionSetting, connection)); param.skipRetValStatus(tdsReader); - prepStmtHandle = param.getInt(tdsReader); + + setPreparedStatementHandle(param.getInt(tdsReader)); + + // Cache the reference to the newly created handle, NOT for cursorable handles. + if (null == cachedPreparedStatementHandle && !isCursorable(executeMethod)) { + cachedPreparedStatementHandle = connection.registerCachedPreparedStatementHandle(new Sha1HashKey(preparedSQL, preparedTypeDefinitions), prepStmtHandle, executedSqlDirectly); + } + param.skipValue(tdsReader, true); if (getStatementLogger().isLoggable(java.util.logging.Level.FINER)) getStatementLogger().finer(toString() + ": Setting PreparedHandle:" + prepStmtHandle); @@ -505,7 +637,7 @@ void sendParamsByRPC(TDSWriter tdsWriter, private void buildServerCursorPrepExecParams(TDSWriter tdsWriter) throws SQLServerException { if (getStatementLogger().isLoggable(java.util.logging.Level.FINE)) - getStatementLogger().fine(toString() + ": calling sp_cursorprepexec: PreparedHandle:" + prepStmtHandle + ", SQL:" + preparedSQL); + getStatementLogger().fine(toString() + ": calling sp_cursorprepexec: PreparedHandle:" + getPreparedStatementHandle() + ", SQL:" + preparedSQL); expectPrepStmtHandle = true; executedSqlDirectly = false; @@ -520,11 +652,11 @@ private void buildServerCursorPrepExecParams(TDSWriter tdsWriter) throws SQLServ // // IN (reprepare): Old handle to unprepare before repreparing // OUT: The newly prepared handle - tdsWriter.writeRPCInt(null, new Integer(prepStmtHandle), true); - prepStmtHandle = 0; + tdsWriter.writeRPCInt(null, getPreparedStatementHandle(), true); + resetPrepStmtHandle(); // OUT - tdsWriter.writeRPCInt(null, new Integer(0), true); // cursor ID (OUTPUT) + tdsWriter.writeRPCInt(null, 0, true); // cursor ID (OUTPUT) // IN tdsWriter.writeRPCStringUnicode((preparedTypeDefinitions.length() > 0) ? preparedTypeDefinitions : null); @@ -536,18 +668,18 @@ private void buildServerCursorPrepExecParams(TDSWriter tdsWriter) throws SQLServ // Note: we must strip out SCROLLOPT_PARAMETERIZED_STMT if we don't // actually have any parameters. tdsWriter.writeRPCInt(null, - new Integer(getResultSetScrollOpt() & ~((0 == preparedTypeDefinitions.length()) ? TDS.SCROLLOPT_PARAMETERIZED_STMT : 0)), false); + getResultSetScrollOpt() & ~((0 == preparedTypeDefinitions.length()) ? TDS.SCROLLOPT_PARAMETERIZED_STMT : 0), false); // IN - tdsWriter.writeRPCInt(null, new Integer(getResultSetCCOpt()), false); + tdsWriter.writeRPCInt(null, getResultSetCCOpt(), false); // OUT - tdsWriter.writeRPCInt(null, new Integer(0), true); + tdsWriter.writeRPCInt(null, 0, true); } private void buildPrepExecParams(TDSWriter tdsWriter) throws SQLServerException { if (getStatementLogger().isLoggable(java.util.logging.Level.FINE)) - getStatementLogger().fine(toString() + ": calling sp_prepexec: PreparedHandle:" + prepStmtHandle + ", SQL:" + preparedSQL); + getStatementLogger().fine(toString() + ": calling sp_prepexec: PreparedHandle:" + getPreparedStatementHandle() + ", SQL:" + preparedSQL); expectPrepStmtHandle = true; executedSqlDirectly = true; @@ -562,8 +694,8 @@ private void buildPrepExecParams(TDSWriter tdsWriter) throws SQLServerException // // IN (reprepare): Old handle to unprepare before repreparing // OUT: The newly prepared handle - tdsWriter.writeRPCInt(null, new Integer(prepStmtHandle), true); - prepStmtHandle = 0; + tdsWriter.writeRPCInt(null, getPreparedStatementHandle(), true); + resetPrepStmtHandle(); // IN tdsWriter.writeRPCStringUnicode((preparedTypeDefinitions.length() > 0) ? preparedTypeDefinitions : null); @@ -572,9 +704,34 @@ private void buildPrepExecParams(TDSWriter tdsWriter) throws SQLServerException tdsWriter.writeRPCStringUnicode(preparedSQL); } + private void buildExecSQLParams(TDSWriter tdsWriter) throws SQLServerException { + if (getStatementLogger().isLoggable(java.util.logging.Level.FINE)) + getStatementLogger().fine(toString() + ": calling sp_executesql: SQL:" + preparedSQL); + + expectPrepStmtHandle = false; + executedSqlDirectly = true; + expectCursorOutParams = false; + outParamIndexAdjustment = 2; + + tdsWriter.writeShort((short) 0xFFFF); // procedure name length -> use ProcIDs + tdsWriter.writeShort(TDS.PROCID_SP_EXECUTESQL); + tdsWriter.writeByte((byte) 0); // RPC procedure option 1 + tdsWriter.writeByte((byte) 0); // RPC procedure option 2 + + // No handle used. + resetPrepStmtHandle(); + + // IN + tdsWriter.writeRPCStringUnicode(preparedSQL); + + // IN + if (preparedTypeDefinitions.length() > 0) + tdsWriter.writeRPCStringUnicode(preparedTypeDefinitions); + } + private void buildServerCursorExecParams(TDSWriter tdsWriter) throws SQLServerException { if (getStatementLogger().isLoggable(java.util.logging.Level.FINE)) - getStatementLogger().fine(toString() + ": calling sp_cursorexecute: PreparedHandle:" + prepStmtHandle + ", SQL:" + preparedSQL); + getStatementLogger().fine(toString() + ": calling sp_cursorexecute: PreparedHandle:" + getPreparedStatementHandle() + ", SQL:" + preparedSQL); expectPrepStmtHandle = false; executedSqlDirectly = false; @@ -587,25 +744,25 @@ private void buildServerCursorExecParams(TDSWriter tdsWriter) throws SQLServerEx tdsWriter.writeByte((byte) 0); // RPC procedure option 2 */ // IN - assert 0 != prepStmtHandle; - tdsWriter.writeRPCInt(null, new Integer(prepStmtHandle), false); + assert hasPreparedStatementHandle(); + tdsWriter.writeRPCInt(null, getPreparedStatementHandle(), false); // OUT - tdsWriter.writeRPCInt(null, new Integer(0), true); + tdsWriter.writeRPCInt(null, 0, true); // IN - tdsWriter.writeRPCInt(null, new Integer(getResultSetScrollOpt() & ~TDS.SCROLLOPT_PARAMETERIZED_STMT), false); + tdsWriter.writeRPCInt(null, getResultSetScrollOpt() & ~TDS.SCROLLOPT_PARAMETERIZED_STMT, false); // IN - tdsWriter.writeRPCInt(null, new Integer(getResultSetCCOpt()), false); + tdsWriter.writeRPCInt(null, getResultSetCCOpt(), false); // OUT - tdsWriter.writeRPCInt(null, new Integer(0), true); + tdsWriter.writeRPCInt(null, 0, true); } private void buildExecParams(TDSWriter tdsWriter) throws SQLServerException { if (getStatementLogger().isLoggable(java.util.logging.Level.FINE)) - getStatementLogger().fine(toString() + ": calling sp_execute: PreparedHandle:" + prepStmtHandle + ", SQL:" + preparedSQL); + getStatementLogger().fine(toString() + ": calling sp_execute: PreparedHandle:" + getPreparedStatementHandle() + ", SQL:" + preparedSQL); expectPrepStmtHandle = false; executedSqlDirectly = true; @@ -618,8 +775,8 @@ private void buildExecParams(TDSWriter tdsWriter) throws SQLServerException { tdsWriter.writeByte((byte) 0); // RPC procedure option 2 */ // IN - assert 0 != prepStmtHandle; - tdsWriter.writeRPCInt(null, new Integer(prepStmtHandle), false); + assert hasPreparedStatementHandle(); + tdsWriter.writeRPCInt(null, getPreparedStatementHandle(), false); } private void getParameterEncryptionMetadata(Parameter[] params) throws SQLServerException { @@ -659,7 +816,7 @@ private void getParameterEncryptionMetadata(Parameter[] params) throws SQLServer return; } - Map cekList = new HashMap(); + Map cekList = new HashMap<>(); CekTableEntry cekEntry = null; try { while (rs.next()) { @@ -763,31 +920,91 @@ private void getParameterEncryptionMetadata(Parameter[] params) throws SQLServer connection.resetCurrentCommand(); } + /** Manage re-using cached handles */ + private boolean reuseCachedHandle(boolean hasNewTypeDefinitions, boolean discardCurrentCacheItem) { + + // No re-use of caching for cursorable statements (statements that WILL use sp_cursor*) + if (isCursorable(executeMethod)) + return false; + + // If current cache item should be discarded make sure it is not used again. + if (discardCurrentCacheItem && null != cachedPreparedStatementHandle) { + + cachedPreparedStatementHandle.removeReference(); + + // Make sure the cached handle does not get re-used more. + resetPrepStmtHandle(); + cachedPreparedStatementHandle.setIsExplicitlyDiscarded(); + cachedPreparedStatementHandle = null; + + return false; + } + + // New type definitions and existing cached handle reference then deregister cached handle. + if(hasNewTypeDefinitions) { + if (null != cachedPreparedStatementHandle && hasPreparedStatementHandle() && prepStmtHandle == cachedPreparedStatementHandle.getHandle()) { + cachedPreparedStatementHandle.removeReference(); + cachedPreparedStatementHandle.setIsExplicitlyDiscarded(); + } + cachedPreparedStatementHandle = null; + } + + // Check for new cache reference. + if (null == cachedPreparedStatementHandle) { + PreparedStatementHandle cachedHandle = connection.getCachedPreparedStatementHandle(new Sha1HashKey(preparedSQL, preparedTypeDefinitions)); + + // If handle was found then re-use, only if AE is not on and is not a batch query with new type definitions (We shouldn't reuse handle + // if it is batch query and has new type definition, or if it is on, make sure encryptionMetadataIsRetrieved is retrieved. + if (null != cachedHandle) { + if (!connection.isColumnEncryptionSettingEnabled() + || (connection.isColumnEncryptionSettingEnabled() && encryptionMetadataIsRetrieved)) { + if (cachedHandle.tryAddReference()) { + setPreparedStatementHandle(cachedHandle.getHandle()); + cachedPreparedStatementHandle = cachedHandle; + return true; + } + } + } + } + return false; + } + private boolean doPrepExec(TDSWriter tdsWriter, Parameter[] params, - boolean hasNewTypeDefinitions) throws SQLServerException { + boolean hasNewTypeDefinitions, + boolean hasExistingTypeDefinitions) throws SQLServerException { UUID currentClientConnectionId = connection.getClientConnectionId(); - // this condition is modified to account for connection id. - boolean needsPrepare = hasNewTypeDefinitions || 0 == prepStmtHandle || preparedClientConnectionId != currentClientConnectionId; - // boolean needsPrepare = buildPreparedStrings(params) || 0 == prepStmtHandle || preparedClientConnectionId != currentClientConnectionId; + // this condition is modified to account for connection id. + boolean needsPrepare = (hasNewTypeDefinitions && hasExistingTypeDefinitions) || !hasPreparedStatementHandle() || preparedClientConnectionId != currentClientConnectionId; - if (needsPrepare) { - if (isCursorable(executeMethod)) + // Cursors don't use statement pooling. + if (isCursorable(executeMethod)) { + + if (needsPrepare) buildServerCursorPrepExecParams(tdsWriter); else - buildPrepExecParams(tdsWriter); - - preparedClientConnectionId = currentClientConnectionId; + buildServerCursorExecParams(tdsWriter); } else { - if (isCursorable(executeMethod)) - buildServerCursorExecParams(tdsWriter); + // Move overhead of needing to do prepare & unprepare to only use cases that need more than one execution. + // First execution, use sp_executesql, optimizing for asumption we will not re-use statement. + if (needsPrepare + && !connection.getEnablePrepareOnFirstPreparedStatementCall() + && !isExecutedAtLeastOnce + ) { + buildExecSQLParams(tdsWriter); + isExecutedAtLeastOnce = true; + } + // Second execution, use prepared statements since we seem to be re-using it. + else if(needsPrepare) + buildPrepExecParams(tdsWriter); else buildExecParams(tdsWriter); } sendParamsByRPC(tdsWriter, params); + return needsPrepare; } @@ -824,16 +1041,13 @@ else if (resultSet != null) { * @return the result set containing the meta data */ /* L0 */ private ResultSet buildExecuteMetaData() throws SQLServerException { - String fmtSQL = sqlCommand; - if (fmtSQL.indexOf(LEFT_CURLY_BRACKET) >= 0) { - fmtSQL = (new JDBCSyntaxTranslator()).translate(fmtSQL); - } + String fmtSQL = userSQL; ResultSet emptyResultSet = null; try { fmtSQL = replaceMarkerWithNull(fmtSQL); - SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(); - emptyResultSet = stmt.executeQueryInternal("set fmtonly on " + fmtSQL + "\nset fmtonly off"); + internalStmt = (SQLServerStatement) connection.createStatement(); + emptyResultSet = internalStmt.executeQueryInternal("set fmtonly on " + fmtSQL + "\nset fmtonly off"); } catch (SQLException sqle) { if (false == sqle.getMessage().equals(SQLServerException.getErrString("R_noResultset"))) { @@ -861,7 +1075,7 @@ else if (resultSet != null) { /* L0 */ final Parameter setterGetParam(int index) throws SQLServerException { if (index < 1 || index > inOutParam.length) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_indexOutOfRange")); - Object[] msgArgs = {new Integer(index)}; + Object[] msgArgs = {index}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), "07009", false); } @@ -925,7 +1139,6 @@ final void setSQLXMLInternal(int parameterIndex, public final void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setAsciiStream", new Object[] {parameterIndex, x}); checkClosed(); @@ -946,7 +1159,6 @@ public final void setAsciiStream(int n, public final void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setAsciiStream", new Object[] {parameterIndex, x, length}); checkClosed(); @@ -958,7 +1170,7 @@ private Parameter getParam(int index) throws SQLServerException { index--; if (index < 0 || index >= inOutParam.length) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_indexOutOfRange")); - Object[] msgArgs = {new Integer(index + 1)}; + Object[] msgArgs = {index + 1}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), "07009", false); } return inOutParam[index]; @@ -1122,7 +1334,6 @@ public final void setSmallMoney(int n, public final void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setBinaryStreaml", new Object[] {parameterIndex, x}); checkClosed(); @@ -1143,7 +1354,6 @@ public final void setBinaryStream(int n, public final void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setBinaryStream", new Object[] {parameterIndex, x, length}); checkClosed(); @@ -1156,7 +1366,7 @@ public final void setBoolean(int n, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setBoolean", new Object[] {n, x}); checkClosed(); - setValue(n, JDBCType.BIT, Boolean.valueOf(x), JavaType.BOOLEAN, false); + setValue(n, JDBCType.BIT, x, JavaType.BOOLEAN, false); loggerExternal.exiting(getClassNameLogging(), "setBoolean"); } @@ -1181,7 +1391,7 @@ public final void setBoolean(int n, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setBoolean", new Object[] {n, x, forceEncrypt}); checkClosed(); - setValue(n, JDBCType.BIT, Boolean.valueOf(x), JavaType.BOOLEAN, forceEncrypt); + setValue(n, JDBCType.BIT, x, JavaType.BOOLEAN, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "setBoolean"); } @@ -1190,7 +1400,7 @@ public final void setByte(int n, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setByte", new Object[] {n, x}); checkClosed(); - setValue(n, JDBCType.TINYINT, Byte.valueOf(x), JavaType.BYTE, false); + setValue(n, JDBCType.TINYINT, x, JavaType.BYTE, false); loggerExternal.exiting(getClassNameLogging(), "setByte"); } @@ -1215,7 +1425,7 @@ public final void setByte(int n, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setByte", new Object[] {n, x, forceEncrypt}); checkClosed(); - setValue(n, JDBCType.TINYINT, Byte.valueOf(x), JavaType.BYTE, forceEncrypt); + setValue(n, JDBCType.TINYINT, x, JavaType.BYTE, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "setByte"); } @@ -1302,7 +1512,7 @@ public final void setDouble(int n, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setDouble", new Object[] {n, x}); checkClosed(); - setValue(n, JDBCType.DOUBLE, Double.valueOf(x), JavaType.DOUBLE, false); + setValue(n, JDBCType.DOUBLE, x, JavaType.DOUBLE, false); loggerExternal.exiting(getClassNameLogging(), "setDouble"); } @@ -1327,7 +1537,7 @@ public final void setDouble(int n, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setDouble", new Object[] {n, x, forceEncrypt}); checkClosed(); - setValue(n, JDBCType.DOUBLE, Double.valueOf(x), JavaType.DOUBLE, forceEncrypt); + setValue(n, JDBCType.DOUBLE, x, JavaType.DOUBLE, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "setDouble"); } @@ -1336,7 +1546,7 @@ public final void setFloat(int n, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setFloat", new Object[] {n, x}); checkClosed(); - setValue(n, JDBCType.REAL, Float.valueOf(x), JavaType.FLOAT, false); + setValue(n, JDBCType.REAL, x, JavaType.FLOAT, false); loggerExternal.exiting(getClassNameLogging(), "setFloat"); } @@ -1361,7 +1571,7 @@ public final void setFloat(int n, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setFloat", new Object[] {n, x, forceEncrypt}); checkClosed(); - setValue(n, JDBCType.REAL, Float.valueOf(x), JavaType.FLOAT, forceEncrypt); + setValue(n, JDBCType.REAL, x, JavaType.FLOAT, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "setFloat"); } @@ -1370,7 +1580,7 @@ public final void setInt(int n, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setInt", new Object[] {n, value}); checkClosed(); - setValue(n, JDBCType.INTEGER, Integer.valueOf(value), JavaType.INTEGER, false); + setValue(n, JDBCType.INTEGER, value, JavaType.INTEGER, false); loggerExternal.exiting(getClassNameLogging(), "setInt"); } @@ -1395,7 +1605,7 @@ public final void setInt(int n, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setInt", new Object[] {n, value, forceEncrypt}); checkClosed(); - setValue(n, JDBCType.INTEGER, Integer.valueOf(value), JavaType.INTEGER, forceEncrypt); + setValue(n, JDBCType.INTEGER, value, JavaType.INTEGER, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "setInt"); } @@ -1404,7 +1614,7 @@ public final void setLong(int n, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setLong", new Object[] {n, x}); checkClosed(); - setValue(n, JDBCType.BIGINT, Long.valueOf(x), JavaType.LONG, false); + setValue(n, JDBCType.BIGINT, x, JavaType.LONG, false); loggerExternal.exiting(getClassNameLogging(), "setLong"); } @@ -1429,7 +1639,7 @@ public final void setLong(int n, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setLong", new Object[] {n, x, forceEncrypt}); checkClosed(); - setValue(n, JDBCType.BIGINT, Long.valueOf(x), JavaType.LONG, forceEncrypt); + setValue(n, JDBCType.BIGINT, x, JavaType.LONG, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "setLong"); } @@ -1519,7 +1729,7 @@ public final void setObject(int parameterIndex, setObject(setterGetParam(parameterIndex), x, JavaType.of(x), JDBCType.of(targetSqlType), (java.sql.Types.NUMERIC == targetSqlType || java.sql.Types.DECIMAL == targetSqlType || java.sql.Types.TIMESTAMP == targetSqlType || java.sql.Types.TIME == targetSqlType || microsoft.sql.Types.DATETIMEOFFSET == targetSqlType - || InputStream.class.isInstance(x) || Reader.class.isInstance(x)) ? Integer.valueOf(scaleOrLength) : null, + || InputStream.class.isInstance(x) || Reader.class.isInstance(x)) ? scaleOrLength : null, null, false, parameterIndex, null); loggerExternal.exiting(getClassNameLogging(), "setObject"); @@ -1569,7 +1779,7 @@ public final void setObject(int parameterIndex, setObject(setterGetParam(parameterIndex), x, JavaType.of(x), JDBCType.of(targetSqlType), (java.sql.Types.NUMERIC == targetSqlType || java.sql.Types.DECIMAL == targetSqlType - || InputStream.class.isInstance(x) || Reader.class.isInstance(x)) ? Integer.valueOf(scale) : null, + || InputStream.class.isInstance(x) || Reader.class.isInstance(x)) ? scale : null, precision, false, parameterIndex, null); loggerExternal.exiting(getClassNameLogging(), "setObject"); @@ -1625,7 +1835,7 @@ public final void setObject(int parameterIndex, setObject(setterGetParam(parameterIndex), x, JavaType.of(x), JDBCType.of(targetSqlType), (java.sql.Types.NUMERIC == targetSqlType || java.sql.Types.DECIMAL == targetSqlType - || InputStream.class.isInstance(x) || Reader.class.isInstance(x)) ? Integer.valueOf(scale) : null, + || InputStream.class.isInstance(x) || Reader.class.isInstance(x)) ? scale : null, precision, forceEncrypt, parameterIndex, null); loggerExternal.exiting(getClassNameLogging(), "setObject"); @@ -1696,7 +1906,7 @@ public final void setShort(int index, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setShort", new Object[] {index, x}); checkClosed(); - setValue(index, JDBCType.SMALLINT, Short.valueOf(x), JavaType.SHORT, false); + setValue(index, JDBCType.SMALLINT, x, JavaType.SHORT, false); loggerExternal.exiting(getClassNameLogging(), "setShort"); } @@ -1721,7 +1931,7 @@ public final void setShort(int index, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setShort", new Object[] {index, x, forceEncrypt}); checkClosed(); - setValue(index, JDBCType.SMALLINT, Short.valueOf(x), JavaType.SHORT, forceEncrypt); + setValue(index, JDBCType.SMALLINT, x, JavaType.SHORT, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "setShort"); } @@ -1762,7 +1972,6 @@ public final void setString(int index, public final void setNString(int parameterIndex, String value) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setNString", new Object[] {parameterIndex, value}); checkClosed(); @@ -1789,7 +1998,6 @@ public final void setNString(int parameterIndex, public final void setNString(int parameterIndex, String value, boolean forceEncrypt) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setNString", new Object[] {parameterIndex, value, forceEncrypt}); checkClosed(); @@ -2152,6 +2360,13 @@ String getTVPNameIfNull(int n, if (null != this.procedureName) { SQLServerParameterMetaData pmd = (SQLServerParameterMetaData) this.getParameterMetaData(); pmd.isTVP = true; + + if (!pmd.procedureIsFound) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_StoredProcedureNotFound")); + Object[] msgArgs = {this.procedureName}; + SQLServerException.makeFromDriverError(connection, pmd, form.format(msgArgs), null, false); + } + try { String tvpNameWithoutSchema = pmd.getParameterTypeName(n); String tvpSchema = pmd.getTVPSchemaFromStoredProcedure(n); @@ -2184,7 +2399,7 @@ public final void addBatch() throws SQLServerException { // Create the list of batch parameter values first time through if (batchParamValues == null) - batchParamValues = new ArrayList(); + batchParamValues = new ArrayList<>(); final int numParams = inOutParam.length; Parameter paramValues[] = new Parameter[numParams]; @@ -2225,10 +2440,9 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException { // // OUT and INOUT parameter checking is done here, before executing the batch. If any // OUT or INOUT are present, the entire batch fails. - for (int batch = 0; batch < batchParamValues.size(); ++batch) { - Parameter paramValues[] = batchParamValues.get(batch); - for (int param = 0; param < paramValues.length; ++param) { - if (paramValues[param].isOutput()) { + for (Parameter[] paramValues : batchParamValues) { + for (Parameter paramValue : paramValues) { + if (paramValue.isOutput()) { throw new BatchUpdateException(SQLServerException.getErrString("R_outParamsNotPermittedinBatch"), null, 0, null); } } @@ -2283,10 +2497,9 @@ public long[] executeLargeBatch() throws SQLServerException, BatchUpdateExceptio // // OUT and INOUT parameter checking is done here, before executing the batch. If any // OUT or INOUT are present, the entire batch fails. - for (int batch = 0; batch < batchParamValues.size(); ++batch) { - Parameter paramValues[] = batchParamValues.get(batch); - for (int param = 0; param < paramValues.length; ++param) { - if (paramValues[param].isOutput()) { + for (Parameter[] paramValues : batchParamValues) { + for (Parameter paramValue : paramValues) { + if (paramValue.isOutput()) { throw new BatchUpdateException(SQLServerException.getErrString("R_outParamsNotPermittedinBatch"), null, 0, null); } } @@ -2298,8 +2511,7 @@ public long[] executeLargeBatch() throws SQLServerException, BatchUpdateExceptio updateCounts = new long[batchCommand.updateCounts.length]; - for (int i = 0; i < batchCommand.updateCounts.length; ++i) - updateCounts[i] = batchCommand.updateCounts[i]; + System.arraycopy(batchCommand.updateCounts, 0, updateCounts, 0, batchCommand.updateCounts.length); // Transform the SQLException into a BatchUpdateException with the update counts. if (null != batchCommand.batchException) { @@ -2346,7 +2558,7 @@ final void doExecutePreparedStatementBatch(PrepStmtBatchExecCmd batchCommand) th int numBatchesPrepared = 0; int numBatchesExecuted = 0; - Vector cryptoMetaBatch = new Vector(); + Vector cryptoMetaBatch = new Vector<>(); if (isSelect(userSQL)) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_selectNotPermittedinBatch"), null, true); @@ -2366,21 +2578,22 @@ final void doExecutePreparedStatementBatch(PrepStmtBatchExecCmd batchCommand) th // Fill in the parameter values for this batch Parameter paramValues[] = batchParamValues.get(numBatchesPrepared); assert paramValues.length == batchParam.length; - for (int i = 0; i < paramValues.length; i++) - batchParam[i] = paramValues[i]; - + System.arraycopy(paramValues, 0, batchParam, 0, paramValues.length); + + boolean hasExistingTypeDefinitions = preparedTypeDefinitions != null; boolean hasNewTypeDefinitions = buildPreparedStrings(batchParam, false); + // Get the encryption metadata for the first batch only. if ((0 == numBatchesExecuted) && (Util.shouldHonorAEForParameters(stmtColumnEncriptionSetting, connection)) && (0 < batchParam.length) - && !isInternalEncryptionQuery) { + && !isInternalEncryptionQuery && !encryptionMetadataIsRetrieved) { getParameterEncryptionMetadata(batchParam); // fix an issue when inserting unicode into non-encrypted nchar column using setString() and AE is on on Connection buildPreparedStrings(batchParam, true); // Save the crypto metadata retrieved for the first batch. We will re-use these for the rest of the batches. - for (int i = 0; i < batchParam.length; i++) { - cryptoMetaBatch.add(batchParam[i].cryptoMeta); + for (Parameter aBatchParam : batchParam) { + cryptoMetaBatch.add(aBatchParam.cryptoMeta); } } @@ -2392,80 +2605,120 @@ final void doExecutePreparedStatementBatch(PrepStmtBatchExecCmd batchCommand) th } } - if (numBatchesExecuted < numBatchesPrepared) { - // assert null != tdsWriter; - tdsWriter.writeByte((byte) nBatchStatementDelimiter); - } - else { - resetForReexecute(); - tdsWriter = batchCommand.startRequest(TDS.PKT_RPC); - } - - // If we have to (re)prepare the statement then we must execute it so - // that we get back a (new) prepared statement handle to use to - // execute additional batches. - // - // We must always prepare the statement the first time through. - // But we may also need to reprepare the statement if, for example, - // the size of a batch's string parameter values changes such - // that repreparation is necessary. - ++numBatchesPrepared; - if (doPrepExec(tdsWriter, batchParam, hasNewTypeDefinitions) || numBatchesPrepared == numBatches) { - ensureExecuteResultsReader(batchCommand.startResponse(getIsResponseBufferingAdaptive())); - - while (numBatchesExecuted < numBatchesPrepared) { - // NOTE: - // When making changes to anything below, consider whether similar changes need - // to be made to Statement batch execution. - - startResults(); - - try { - // Get the first result from the batch. If there is no result for this batch - // then bail, leaving EXECUTE_FAILED in the current and remaining slots of - // the update count array. - if (!getNextResult()) - return; - - // If the result is a ResultSet (rather than an update count) then throw an - // exception for this result. The exception gets caught immediately below and - // translated into (or added to) a BatchUpdateException. - if (null != resultSet) { - SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_resultsetGeneratedForUpdate"), - null, false); - } + // Retry execution if existing handle could not be re-used. + for(int attempt = 1; attempt <= 2; ++attempt) { + try { + + // Re-use handle if available, requires parameter definitions which are not available until here. + if (reuseCachedHandle(hasNewTypeDefinitions, 1 < attempt)) { + hasNewTypeDefinitions = false; } - catch (SQLServerException e) { - // If the failure was severe enough to close the connection or roll back a - // manual transaction, then propagate the error up as a SQLServerException - // now, rather than continue with the batch. - if (connection.isSessionUnAvailable() || connection.rolledBackTransaction()) - throw e; - - // Otherwise, the connection is OK and the transaction is still intact, - // so just record the failure for the particular batch item. - updateCount = Statement.EXECUTE_FAILED; - if (null == batchCommand.batchException) - batchCommand.batchException = e; + + if (numBatchesExecuted < numBatchesPrepared) { + // assert null != tdsWriter; + tdsWriter.writeByte((byte) nBatchStatementDelimiter); + } + else { + resetForReexecute(); + tdsWriter = batchCommand.startRequest(TDS.PKT_RPC); } - // In batch execution, we have a special update count - // to indicate that no information was returned - batchCommand.updateCounts[numBatchesExecuted++] = (-1 == updateCount) ? Statement.SUCCESS_NO_INFO : updateCount; + // If we have to (re)prepare the statement then we must execute it so + // that we get back a (new) prepared statement handle to use to + // execute additional batches. + // + // We must always prepare the statement the first time through. + // But we may also need to reprepare the statement if, for example, + // the size of a batch's string parameter values changes such + // that repreparation is necessary. + ++numBatchesPrepared; + + if (doPrepExec(tdsWriter, batchParam, hasNewTypeDefinitions, hasExistingTypeDefinitions) || numBatchesPrepared == numBatches) { + ensureExecuteResultsReader(batchCommand.startResponse(getIsResponseBufferingAdaptive())); + + boolean retry = false; + while (numBatchesExecuted < numBatchesPrepared) { + // NOTE: + // When making changes to anything below, consider whether similar changes need + // to be made to Statement batch execution. + + startResults(); + + try { + // Get the first result from the batch. If there is no result for this batch + // then bail, leaving EXECUTE_FAILED in the current and remaining slots of + // the update count array. + if (!getNextResult()) + return; + + // If the result is a ResultSet (rather than an update count) then throw an + // exception for this result. The exception gets caught immediately below and + // translated into (or added to) a BatchUpdateException. + if (null != resultSet) { + SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_resultsetGeneratedForUpdate"), + null, false); + } + } + catch (SQLServerException e) { + // If the failure was severe enough to close the connection or roll back a + // manual transaction, then propagate the error up as a SQLServerException + // now, rather than continue with the batch. + if (connection.isSessionUnAvailable() || connection.rolledBackTransaction()) + throw e; + + // Retry if invalid handle exception. + if (retryBasedOnFailedReuseOfCachedHandle(e, attempt)) { + //reset number of batches prepare + numBatchesPrepared = numBatchesExecuted; + retry = true; + break; + } + + // Otherwise, the connection is OK and the transaction is still intact, + // so just record the failure for the particular batch item. + updateCount = Statement.EXECUTE_FAILED; + if (null == batchCommand.batchException) + batchCommand.batchException = e; + } + + // In batch execution, we have a special update count + // to indicate that no information was returned + batchCommand.updateCounts[numBatchesExecuted] = (-1 == updateCount) ? Statement.SUCCESS_NO_INFO : updateCount; + processBatch(); + + numBatchesExecuted++; + } + if(retry) + continue; - processBatch(); + // Only way to proceed with preparing the next set of batches is if + // we successfully executed the previously prepared set. + assert numBatchesExecuted == numBatchesPrepared; + } } - - // Only way to proceed with preparing the next set of batches is if - // we successfully executed the previously prepared set. - assert numBatchesExecuted == numBatchesPrepared; + catch(SQLException e) { + if (retryBasedOnFailedReuseOfCachedHandle(e, attempt)) { + // Reset number of batches prepared. + numBatchesPrepared = numBatchesExecuted; + continue; + } + else if (null != batchCommand.batchException) { + // if batch exception occurred, loop out to throw the initial batchException + numBatchesExecuted = numBatchesPrepared; + attempt++; + continue; + } + else { + throw e; + } + } + break; } } } public final void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setCharacterStream", new Object[] {parameterIndex, reader}); checkClosed(); @@ -2486,7 +2739,6 @@ public final void setCharacterStream(int n, public final void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setCharacterStream", new Object[] {parameterIndex, reader, length}); checkClosed(); @@ -2496,7 +2748,6 @@ public final void setCharacterStream(int parameterIndex, public final void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setNCharacterStream", new Object[] {parameterIndex, value}); checkClosed(); @@ -2507,7 +2758,6 @@ public final void setNCharacterStream(int parameterIndex, public final void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setNCharacterStream", new Object[] {parameterIndex, value, length}); checkClosed(); @@ -2531,7 +2781,6 @@ public final void setBlob(int i, public final void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setBlob", new Object[] {parameterIndex, inputStream}); checkClosed(); @@ -2542,7 +2791,6 @@ public final void setBlob(int parameterIndex, public final void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setBlob", new Object[] {parameterIndex, inputStream, length}); checkClosed(); @@ -2561,7 +2809,6 @@ public final void setClob(int parameterIndex, public final void setClob(int parameterIndex, Reader reader) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setClob", new Object[] {parameterIndex, reader}); checkClosed(); @@ -2572,7 +2819,6 @@ public final void setClob(int parameterIndex, public final void setClob(int parameterIndex, Reader reader, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setClob", new Object[] {parameterIndex, reader, length}); checkClosed(); @@ -2582,7 +2828,6 @@ public final void setClob(int parameterIndex, public final void setNClob(int parameterIndex, NClob value) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setNClob", new Object[] {parameterIndex, value}); checkClosed(); @@ -2592,7 +2837,6 @@ public final void setNClob(int parameterIndex, public final void setNClob(int parameterIndex, Reader reader) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setNClob", new Object[] {parameterIndex, reader}); checkClosed(); @@ -2603,7 +2847,6 @@ public final void setNClob(int parameterIndex, public final void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setNClob", new Object[] {parameterIndex, reader, length}); checkClosed(); @@ -2747,14 +2990,41 @@ public final void setNull(int paramIndex, loggerExternal.exiting(getClassNameLogging(), "setNull"); } + /** + * Returns parameter metadata for the prepared statement. + * + * @param forceRefresh: + * If true the cache will not be used to retrieve the metadata. + * + * @return + * Per the description. + * + * @throws SQLServerException when an error occurs + */ + public final ParameterMetaData getParameterMetaData(boolean forceRefresh) throws SQLServerException { + + SQLServerParameterMetaData pmd = this.connection.getCachedParameterMetadata(sqlTextCacheKey); + + if (!forceRefresh && null != pmd) { + return pmd; + } + else { + loggerExternal.entering(getClassNameLogging(), "getParameterMetaData"); + checkClosed(); + pmd = new SQLServerParameterMetaData(this, userSQL); + + connection.registerCachedParameterMetadata(sqlTextCacheKey, pmd); + + loggerExternal.exiting(getClassNameLogging(), "getParameterMetaData", pmd); + + return pmd; + } + } + /* JDBC 3.0 */ /* L3 */ public final ParameterMetaData getParameterMetaData() throws SQLServerException { - loggerExternal.entering(getClassNameLogging(), "getParameterMetaData"); - checkClosed(); - SQLServerParameterMetaData pmd = new SQLServerParameterMetaData(this, userSQL); - loggerExternal.exiting(getClassNameLogging(), "getParameterMetaData", pmd); - return pmd; + return getParameterMetaData(false); } /* L3 */ public final void setURL(int parameterIndex, @@ -2764,15 +3034,12 @@ public final void setNull(int paramIndex, public final void setRowId(int parameterIndex, RowId x) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - // Not implemented throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); } public final void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setSQLXML", new Object[] {parameterIndex, xmlObject}); checkClosed(); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement42Helper.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement42Helper.java index 702f3e205..efd9c1774 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement42Helper.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement42Helper.java @@ -28,7 +28,7 @@ static final void setObject(SQLServerPreparedStatement ps, SQLServerStatement.loggerExternal.entering(ps.getClassNameLogging(), "setObject", new Object[] {index, obj, jdbcType}); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - ps.setObject(index, obj, jdbcType.getVendorTypeNumber().intValue()); + ps.setObject(index, obj, jdbcType.getVendorTypeNumber()); SQLServerStatement.loggerExternal.exiting(ps.getClassNameLogging(), "setObject"); } @@ -45,7 +45,7 @@ static final void setObject(SQLServerPreparedStatement ps, new Object[] {parameterIndex, x, targetSqlType, scaleOrLength}); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - ps.setObject(parameterIndex, x, targetSqlType.getVendorTypeNumber().intValue(), scaleOrLength); + ps.setObject(parameterIndex, x, targetSqlType.getVendorTypeNumber(), scaleOrLength); SQLServerStatement.loggerExternal.exiting(ps.getClassNameLogging(), "setObject"); } @@ -63,7 +63,7 @@ static final void setObject(SQLServerPreparedStatement ps, new Object[] {parameterIndex, x, targetSqlType, precision, scale}); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - ps.setObject(parameterIndex, x, targetSqlType.getVendorTypeNumber().intValue(), precision, scale, false); + ps.setObject(parameterIndex, x, targetSqlType.getVendorTypeNumber(), precision, scale, false); SQLServerStatement.loggerExternal.exiting(ps.getClassNameLogging(), "setObject"); } @@ -82,7 +82,7 @@ static final void setObject(SQLServerPreparedStatement ps, new Object[] {parameterIndex, x, targetSqlType, precision, scale, forceEncrypt}); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - ps.setObject(parameterIndex, x, targetSqlType.getVendorTypeNumber().intValue(), precision, scale, forceEncrypt); + ps.setObject(parameterIndex, x, targetSqlType.getVendorTypeNumber(), precision, scale, forceEncrypt); SQLServerStatement.loggerExternal.exiting(ps.getClassNameLogging(), "setObject"); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index b16a7fdbc..e80f0cf84 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -33,12 +33,12 @@ protected Object[][] getContents() { {"R_dbMirroringWithMultiSubnetFailover", "Connecting to a mirrored SQL Server instance using the multiSubnetFailover connection property is not supported."}, {"R_dbMirroringWithReadOnlyIntent", "Connecting to a mirrored SQL Server instance using the ApplicationIntent ReadOnly connection property is not supported."}, {"R_ipAddressLimitWithMultiSubnetFailover", "Connecting with the multiSubnetFailover connection property to a SQL Server instance configured with more than {0} IP addresses is not supported."}, - {"R_connectionTimedOut", "Connection timed out: no further information"}, + {"R_connectionTimedOut", "Connection timed out: no further information."}, {"R_invalidPositionIndex", "The position index {0} is not valid."}, {"R_invalidLength", "The length {0} is not valid."}, {"R_unknownSSType", "Invalid SQL Server data type {0}."}, {"R_unknownJDBCType", "Invalid JDBC data type {0}."}, - {"R_notSQLServer", "The driver received an unexpected pre-login response. Verify the connection properties and check that an instance of SQL Server is running on the host and accepting TCP/IP connections at the port. This driver can be used only with SQL Server 2000 or later."}, + {"R_notSQLServer", "The driver received an unexpected pre-login response. Verify the connection properties and check that an instance of SQL Server is running on the host and accepting TCP/IP connections at the port. This driver can be used only with SQL Server 2005 or later."}, {"R_tcpOpenFailed", "{0}. Verify the connection properties. Make sure that an instance of SQL Server is running on the host and accepting TCP/IP connections at the port. Make sure that TCP connections to the port are not blocked by a firewall."}, {"R_unsupportedJREVersion", "Java Runtime Environment (JRE) version {0} is not supported by this driver. Use the sqljdbc4.jar class library, which provides support for JDBC 4.0."}, {"R_unsupportedServerVersion", "SQL Server version {0} is not supported by this driver."}, @@ -96,7 +96,6 @@ protected Object[][] getContents() { {"R_noColumnParameterValue", "No column parameter values were specified to update the row."}, {"R_statementMustBeExecuted", "The statement must be executed before any results can be obtained."}, {"R_modeSuppliedNotValid", "The supplied mode is not valid."}, - {"R_variantNotSupported", "The \"variant\" data type is not supported."}, {"R_errorConnectionString", "The connection string contains a badly formed name or value."}, {"R_errorProcessingComplexQuery", "An error occurred while processing the complex query."}, {"R_invalidOffset", "The offset {0} is not valid."}, @@ -104,6 +103,7 @@ protected Object[][] getContents() { {"R_invalidConnection", "The connection URL is invalid."}, {"R_cannotTakeArgumentsPreparedOrCallable", "The method {0} cannot take arguments on a PreparedStatement or CallableStatement."}, {"R_unsupportedConversionFromTo", "The conversion from {0} to {1} is unsupported."}, // Invalid conversion (e.g. MONEY to Timestamp) + {"R_unsupportedConversionTo", "The conversion to {0} is unsupported."}, // Invalid conversion to an unknown type {"R_errorConvertingValue","An error occurred while converting the {0} value to JDBC data type {1}."}, // Data-dependent conversion failure (e.g. "foo" vs. "123", to Integer) {"R_streamIsClosed", "The stream is closed."}, {"R_invalidTDS", "The TDS protocol stream is not valid."}, @@ -180,14 +180,19 @@ protected Object[][] getContents() { {"R_packetSizePropertyDescription", "The network packet size used to communicate with SQL Server."}, {"R_encryptPropertyDescription", "Determines if Secure Sockets Layer (SSL) encryption should be used between the client and the server."}, {"R_trustServerCertificatePropertyDescription", "Determines if the driver should validate the SQL Server Secure Sockets Layer (SSL) certificate."}, - {"R_trustStoreTypePropertyDescription", "Type of trust store type like JKS / PKCS12 or any FIPS Provider KeyStore implementation Type."}, - {"R_trustStorePropertyDescription", "The path to the certificate trust store file."}, + {"R_trustStoreTypePropertyDescription", "KeyStore type."}, + {"R_trustStorePropertyDescription", "The path to the certificate TrustStore file."}, {"R_trustStorePasswordPropertyDescription", "The password used to check the integrity of the trust store data."}, + {"R_trustManagerClassPropertyDescription", "The class to instantiate as the TrustManager for SSL connections."}, + {"R_trustManagerConstructorArgPropertyDescription", "The optional argument to pass to the constructor specified by trustManagerClass."}, {"R_hostNameInCertificatePropertyDescription", "The host name to be used when validating the SQL Server Secure Sockets Layer (SSL) certificate."}, {"R_sendTimeAsDatetimePropertyDescription", "Determines whether to use the SQL Server datetime data type to send java.sql.Time values to the database."}, {"R_TransparentNetworkIPResolutionPropertyDescription", "Determines whether to use the Transparent Network IP Resolution feature."}, {"R_queryTimeoutPropertyDescription", "The number of seconds to wait before the database reports a query time-out."}, {"R_socketTimeoutPropertyDescription", "The number of milliseconds to wait before the java.net.SocketTimeoutException is raised."}, + {"R_serverPreparedStatementDiscardThresholdPropertyDescription", "The threshold for when to close discarded prepare statements on the server (calling a batch of sp_unprepares). A value of 1 or less will cause sp_unprepare to be called immediately on PreparedStatment close."}, + {"R_enablePrepareOnFirstPreparedStatementCallPropertyDescription", "This setting specifies whether a prepared statement is prepared (sp_prepexec) on first use (property=true) or on second after first calling sp_executesql (property=false)."}, + {"R_statementPoolingCacheSizePropertyDescription", "This setting specifies the size of the prepared statement cache for a connection. A value less than 1 means no cache."}, {"R_gsscredentialPropertyDescription", "Impersonated GSS Credential to access SQL Server."}, {"R_noParserSupport", "An error occurred while instantiating the required parser. Error: \"{0}\""}, {"R_writeOnlyXML", "Cannot read from this SQLXML instance. This instance is for writing data only."}, @@ -200,7 +205,7 @@ protected Object[][] getContents() { {"R_isFreed", "This {0} object has been freed. It can no longer be accessed."}, {"R_invalidProperty", "This property is not supported: {0}." }, {"R_referencingFailedTSP", "The DataSource trustStore password needs to be set." }, - {"R_valueOutOfRange", "One or more values is out of range of values for the {0} SQL Server data type" }, + {"R_valueOutOfRange", "One or more values is out of range of values for the {0} SQL Server data type." }, {"R_integratedAuthenticationFailed", "Integrated authentication failed."}, {"R_permissionDenied", "Security violation. Permission to target \"{0}\" denied."}, {"R_getSchemaError", "Error getting default schema name."}, @@ -219,10 +224,10 @@ protected Object[][] getContents() { {"R_unableRetrieveSourceData", "Unable to retrieve data from the source."}, {"R_ParsingError", "Failed to parse data for the {0} type."}, {"R_BulkTypeNotSupported", "Data type {0} is not supported in bulk copy."}, - {"R_invalidTransactionOption", "UseInternalTransaction option can not be set to TRUE when used with a Connection object."}, + {"R_invalidTransactionOption", "UseInternalTransaction option cannot be set to TRUE when used with a Connection object."}, {"R_invalidNegativeArg", "The {0} argument cannot be negative."}, {"R_BulkColumnMappingsIsEmpty", "Cannot perform bulk copy operation if the only mapping is an identity column and KeepIdentity is set to false."}, - {"R_BulkCSVDataSchemaMismatch", "Source data does not match source schema."}, + {"R_CSVDataSchemaMismatch", "Source data does not match source schema."}, {"R_BulkCSVDataDuplicateColumn", "Duplicate column names are not allowed."}, {"R_invalidColumnOrdinal", "Column {0} is invalid. Column number should be greater than zero."}, {"R_unsupportedEncoding", "The encoding {0} is not supported."}, @@ -257,7 +262,7 @@ protected Object[][] getContents() { {"R_CertificateError", "Error occurred while retrieving certificate \"{0}\" from keystore \"{1}\"."}, {"R_ByteToShortConversion", "Error occurred while decrypting column encryption key."}, {"R_InvalidCertificateSignature", "The specified encrypted column encryption key signature does not match the signature computed with the column master key (certificate) in \"{0}\". The encrypted column encryption key may be corrupt, or the specified path may be incorrect."}, - {"R_CEKDecryptionFailed", "Exception while decryption of encrypted column encryption key : {0} "}, + {"R_CEKDecryptionFailed", "Exception while decryption of encrypted column encryption key: {0} "}, {"R_NullKeyEncryptionAlgorithm", "Key encryption algorithm cannot be null."}, {"R_NullKeyEncryptionAlgorithmInternal", "Internal error. Key encryption algorithm cannot be null."}, {"R_InvalidKeyEncryptionAlgorithm", "Invalid key encryption algorithm specified: {0}. Expected value: {1}."}, @@ -265,7 +270,7 @@ protected Object[][] getContents() { {"R_NullColumnEncryptionKey", "Column encryption key cannot be null."}, {"R_EmptyColumnEncryptionKey", "Empty column encryption key specified."}, {"R_CertificateNotFoundForAlias", "Certificate with alias {0} not found in the store provided by {1}. Verify the certificate has been imported correctly into the certificate location/store."}, - {"R_UnrecoverableKeyAE", "Cannot recover private key from keystore with certificate details {0}. Verify that imported AE certificate contains private key and password provided for certificate is correct."}, + {"R_UnrecoverableKeyAE", "Cannot recover private key from keystore with certificate details {0}. Verify that imported certificate for Always Encrypted contains private key and password provided for certificate is correct."}, {"R_KeyStoreNotFound", "System cannot find the key store file at the specified path. Verify that the path is correct and you have proper permissions to access it."}, {"R_CustomKeyStoreProviderMapNull", "Column encryption key store provider map cannot be null. Expecting a non-null value."}, {"R_EmptyCustomKeyStoreProviderName", "Invalid key store provider name specified. Key store provider names cannot be null or empty."}, @@ -303,7 +308,7 @@ protected Object[][] getContents() { {"R_ForceEncryptionTrue_HonorAETrue_UnencryptedColumnRS", "Cannot execute update because Force Encryption was set as true for parameter {0} and the database expects this parameter to be sent as plaintext. This may be due to a configuration error."}, {"R_NullValue","{0} cannot be null."}, {"R_AKVPathNull", "Azure Key Vault key path cannot be null." }, - {"R_AKVURLInvalid", "Invalid url specified: {0}." }, + {"R_AKVURLInvalid", "Invalid URL specified: {0}." }, {"R_AKVMasterKeyPathInvalid", "Invalid Azure Key Vault key path specified: {0}."}, {"R_EmptyCEK","Empty column encryption key specified."}, {"R_EncryptedCEKNull","Encrypted column encryption key cannot be null."}, @@ -317,8 +322,8 @@ protected Object[][] getContents() { {"R_NoSHA256Algorithm","SHA-256 Algorithm is not supported."}, {"R_VerifySignature","Unable to verify signature of the column encryption key."}, {"R_CEKSignatureNotMatchCMK","The specified encrypted column encryption key signature does not match the signature computed with the column master key (Asymmetric key in Azure Key Vault) in {0}. The encrypted column encryption key may be corrupt, or the specified path may be incorrect."}, - {"R_DecryptCEKError","Unable to decrypt CEK using specified Azure Key Vault key."}, - {"R_EncryptCEKError","Unable to encrypt CEK using specified Azure Key Vault key."}, + {"R_DecryptCEKError","Unable to decrypt column encryption key using specified Azure Key Vault key."}, + {"R_EncryptCEKError","Unable to encrypt column encryption key using specified Azure Key Vault key."}, {"R_CipherTextLengthNotMatchRSASize","CipherText length does not match the RSA key size."}, {"R_GenerateSignature","Unable to generate signature using a specified Azure Key Vault Key URL."}, {"R_SignedHashLengthError","Signed hash length does not match the RSA key size."}, @@ -360,8 +365,7 @@ protected Object[][] getContents() { {"R_keyStoreAuthenticationPropertyDescription", "The name that identifies a key store."}, {"R_keyStoreSecretPropertyDescription", "The authentication secret or information needed to locate the secret."}, {"R_keyStoreLocationPropertyDescription", "The key store location."}, - {"R_fipsProviderPropertyDescription", "FIPS Provider."}, - {"R_keyStoreAuthenticationNotSet", "\"keyStoreAuthentication\" connection string keyword must be specified, if \"{0}\" is specified."}, + {"R_keyStoreAuthenticationNotSet", "\"keyStoreAuthentication\" connection string keyword must be specified, if \"{0}\" is specified."}, {"R_keyStoreSecretOrLocationNotSet", "Both \"keyStoreSecret\" and \"keyStoreLocation\" must be set, if \"keyStoreAuthentication=JavaKeyStorePassword\" has been specified in the connection string."}, {"R_certificateStoreInvalidKeyword", "Cannot set \"keyStoreSecret\", if \"keyStoreAuthentication=CertificateStore\" has been specified in the connection string."}, {"R_certificateStoreLocationNotSet", "\"keyStoreLocation\" must be specified, if \"keyStoreAuthentication=CertificateStore\" has been specified in the connection string."}, @@ -369,14 +373,25 @@ protected Object[][] getContents() { {"R_invalidKeyStoreFile", "Cannot parse \"{0}\". Either the file format is not valid or the password is not correct."}, // for JKS/PKCS {"R_invalidCEKCacheTtl", "Invalid column encryption key cache time-to-live specified. The columnEncryptionKeyCacheTtl value cannot be negative and timeUnit can only be DAYS, HOURS, MINUTES or SECONDS."}, {"R_sendTimeAsDateTimeForAE", "Use sendTimeAsDateTime=false with Always Encrypted."}, - {"R_invalidServerCursorForTVP" , "Use different Connection for source ResultSet and prepared query, if selectMethod is set to cursor for Table-Valued Parameter."}, - {"R_TVPnotWorkWithSetObjectResultSet" , "setObject() with ResultSet is not supported for Table-Valued Parameter. Please use setStructured()"}, + {"R_TVPnotWorkWithSetObjectResultSet", "setObject() with ResultSet is not supported for Table-Valued Parameter. Please use setStructured()."}, {"R_invalidQueryTimeout", "The queryTimeout {0} is not valid."}, {"R_invalidSocketTimeout", "The socketTimeout {0} is not valid."}, - {"R_fipsPropertyDescription", "Determines if enable FIPS compilant SSL connection between the client and the server."}, - {"R_invalidFipsConfig", "Could not enable FIPS."}, - {"R_invalidFipsEncryptConfig", "Could not enable FIPS due to either encrypt is not true or using trusted certificate settings."}, - {"R_invalidFipsProviderConfig", "Could not enable FIPS due to invalid FIPSProvider or TrustStoreType."}, + {"R_fipsPropertyDescription", "Determines if FIPS mode is enabled."}, + {"R_invalidFipsConfig", "Unable to verify FIPS mode settings."}, + {"R_serverPreparedStatementDiscardThreshold", "The serverPreparedStatementDiscardThreshold {0} is not valid."}, + {"R_statementPoolingCacheSize", "The statementPoolingCacheSize {0} is not valid."}, + {"R_kerberosLoginFailedForUsername", "Cannot login with Kerberos principal {0}, check your credentials. {1}"}, + {"R_kerberosLoginFailed", "Kerberos Login failed: {0} due to {1} ({2})"}, + {"R_StoredProcedureNotFound", "Could not find stored procedure ''{0}''."}, + {"R_jaasConfigurationNamePropertyDescription", "Login configuration file for Kerberos authentication."}, + {"R_AKVKeyNotFound", "Key not found: {0}"}, + {"R_SQLVariantSupport", "SQL_VARIANT is not supported in versions of SQL Server before 2008."}, + {"R_invalidProbbytes", "SQL_VARIANT: invalid probBytes for {0} type."}, + {"R_invalidStringValue", "SQL_VARIANT does not support string values of length greater than 8000."}, + {"R_invalidValueForTVPWithSQLVariant", "Use of TVPs containing null sql_variant columns is not supported."}, + {"R_invalidDataTypeSupportForSQLVariant", "Unexpected TDS type ' '{0}' ' in SQL_VARIANT."}, + {"R_sslProtocolPropertyDescription", "SSL protocol label from TLS, TLSv1, TLSv1.1 & TLSv1.2. The default is TLS."}, + {"R_invalidSSLProtocol", "SSL Protocol {0} label is not valid. Only TLS, TLSv1, TLSv1.1 & TLSv1.2 are supported."}, {"R_invalidConnectRetryCount", "Connection retry count {0} is not valid."}, {"R_connectRetryCountPropertyDescription", "The number of attempts the driver will make to reconnect after identifying a connection failure."}, {"R_invalidConnectRetryInterval", "Connection retry interval {0} is not valid."}, @@ -386,6 +401,5 @@ protected Object[][] getContents() { {"R_crClientTDSVersionNotRecoverable", "The server did not preserve the exact client TDS version requested during a recovery attempt, connection recovery is not possible."}, {"R_crServerSessionStateNotRecoverable", "The connection is broken and recovery is not possible. The connection is marked by the server as unrecoverable. No attempt was made to restore the connection."}, {"R_crClientSSLStateNotRecoverable", "The server did not preserve SSL encryption during a recovery attempt, connection recovery is not possible."}, - }; -} +} \ No newline at end of file diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java index 33446ed72..e937b27c3 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java @@ -27,6 +27,7 @@ import java.sql.SQLXML; import java.text.MessageFormat; import java.util.Calendar; +import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; @@ -83,6 +84,10 @@ String getClassNameLogging() { private boolean isClosed = false; private final int serverCursorId; + + protected int getServerCursorId() { + return serverCursorId; + } /** the intended fetch direction to optimize cursor performance */ private int fetchDirection; @@ -200,6 +205,10 @@ private void skipColumns(int columnsToSkip, /** TDS reader from which row values are read */ private TDSReader tdsReader; + + protected TDSReader getTDSReader() { + return tdsReader; + } private final FetchBuffer fetchBuffer; @@ -386,15 +395,13 @@ boolean onDone(TDSReader tdsReader) throws SQLServerException { public boolean isWrapperFor(Class iface) throws SQLException { loggerExternal.entering(getClassNameLogging(), "isWrapperFor"); - DriverJDBCVersion.checkSupportsJDBC4(); boolean f = iface.isInstance(this); - loggerExternal.exiting(getClassNameLogging(), "isWrapperFor", Boolean.valueOf(f)); + loggerExternal.exiting(getClassNameLogging(), "isWrapperFor", f); return f; } public T unwrap(Class iface) throws SQLException { loggerExternal.entering(getClassNameLogging(), "unwrap"); - DriverJDBCVersion.checkSupportsJDBC4(); T t; try { t = iface.cast(this); @@ -429,8 +436,6 @@ public T unwrap(Class iface) throws SQLException { } public boolean isClosed() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - loggerExternal.entering(getClassNameLogging(), "isClosed"); boolean result = isClosed || stmt.isClosed(); loggerExternal.exiting(getClassNameLogging(), "isClosed", result); @@ -448,7 +453,7 @@ private void throwNotScrollable() throws SQLServerException { true); } - private boolean isForwardOnly() { + protected boolean isForwardOnly() { return TYPE_SS_DIRECT_FORWARD_ONLY == stmt.getSQLResultSetType() || TYPE_SS_SERVER_CURSOR_FORWARD_ONLY == stmt.getSQLResultSetType(); } @@ -535,7 +540,7 @@ private void verifyValidColumnIndex(int index) throws SQLServerException { if (index < 1 || index > nCols) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_indexOutOfRange")); - Object[] msgArgs = {new Integer(index)}; + Object[] msgArgs = {index}; SQLServerException.makeFromDriverError(stmt.connection, stmt, form.format(msgArgs), "07009", false); } } @@ -1791,8 +1796,7 @@ private void cancelInsert() { /** Clear any updated column values for the current row in the result set. */ final void clearColumnsValues() { int l = columns.length; - for (int i = 0; i < l; i++) - columns[i].cancelUpdates(); + for (Column column : columns) column.cancelUpdates(); } /* L0 */ public SQLWarning getWarnings() throws SQLServerException { @@ -1814,7 +1818,7 @@ public void setFetchDirection(int direction) throws SQLServerException { (ResultSet.FETCH_FORWARD != direction && (SQLServerResultSet.TYPE_SS_DIRECT_FORWARD_ONLY == stmt.resultSetType || SQLServerResultSet.TYPE_SS_SERVER_CURSOR_FORWARD_ONLY == stmt.resultSetType))) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidFetchDirection")); - Object[] msgArgs = {new Integer(direction)}; + Object[] msgArgs = {direction}; SQLServerException.makeFromDriverError(stmt.connection, stmt, form.format(msgArgs), null, false); } @@ -1922,7 +1926,15 @@ private Object getValue(int columnIndex, lastValueWasNull = (null == o); return o; } - + + void setInternalVariantType(int columnIndex, SqlVariant type) throws SQLServerException{ + getterGetColumn(columnIndex).setInternalVariant(type); + } + + SqlVariant getVariantInternalType(int columnIndex) throws SQLServerException { + return getterGetColumn(columnIndex).getInternalVariant(); + } + private Object getStream(int columnIndex, StreamType streamType) throws SQLServerException { Object value = getValue(columnIndex, streamType.getJDBCType(), @@ -2004,7 +2016,7 @@ public boolean getBoolean(int columnIndex) throws SQLServerException { checkClosed(); Boolean value = (Boolean) getValue(columnIndex, JDBCType.BIT); loggerExternal.exiting(getClassNameLogging(), "getBoolean", value); - return null != value ? value.booleanValue() : false; + return null != value ? value : false; } public boolean getBoolean(String columnName) throws SQLServerException { @@ -2012,7 +2024,7 @@ public boolean getBoolean(String columnName) throws SQLServerException { checkClosed(); Boolean value = (Boolean) getValue(findColumn(columnName), JDBCType.BIT); loggerExternal.exiting(getClassNameLogging(), "getBoolean", value); - return null != value ? value.booleanValue() : false; + return null != value ? value : false; } public byte getByte(int columnIndex) throws SQLServerException { @@ -2088,7 +2100,7 @@ public double getDouble(int columnIndex) throws SQLServerException { checkClosed(); Double value = (Double) getValue(columnIndex, JDBCType.DOUBLE); loggerExternal.exiting(getClassNameLogging(), "getDouble", value); - return null != value ? value.doubleValue() : 0; + return null != value ? value : 0; } public double getDouble(String columnName) throws SQLServerException { @@ -2096,7 +2108,7 @@ public double getDouble(String columnName) throws SQLServerException { checkClosed(); Double value = (Double) getValue(findColumn(columnName), JDBCType.DOUBLE); loggerExternal.exiting(getClassNameLogging(), "getDouble", value); - return null != value ? value.doubleValue() : 0; + return null != value ? value : 0; } public float getFloat(int columnIndex) throws SQLServerException { @@ -2104,7 +2116,7 @@ public float getFloat(int columnIndex) throws SQLServerException { checkClosed(); Float value = (Float) getValue(columnIndex, JDBCType.REAL); loggerExternal.exiting(getClassNameLogging(), "getFloat", value); - return null != value ? value.floatValue() : 0; + return null != value ? value : 0; } public float getFloat(String columnName) throws SQLServerException { @@ -2112,7 +2124,7 @@ public float getFloat(String columnName) throws SQLServerException { checkClosed(); Float value = (Float) getValue(findColumn(columnName), JDBCType.REAL); loggerExternal.exiting(getClassNameLogging(), "getFloat", value); - return null != value ? value.floatValue() : 0; + return null != value ? value : 0; } public int getInt(int columnIndex) throws SQLServerException { @@ -2120,7 +2132,7 @@ public int getInt(int columnIndex) throws SQLServerException { checkClosed(); Integer value = (Integer) getValue(columnIndex, JDBCType.INTEGER); loggerExternal.exiting(getClassNameLogging(), "getInt", value); - return null != value ? value.intValue() : 0; + return null != value ? value : 0; } public int getInt(String columnName) throws SQLServerException { @@ -2128,7 +2140,7 @@ public int getInt(String columnName) throws SQLServerException { checkClosed(); Integer value = (Integer) getValue(findColumn(columnName), JDBCType.INTEGER); loggerExternal.exiting(getClassNameLogging(), "getInt", value); - return null != value ? value.intValue() : 0; + return null != value ? value : 0; } public long getLong(int columnIndex) throws SQLServerException { @@ -2136,7 +2148,7 @@ public long getLong(int columnIndex) throws SQLServerException { checkClosed(); Long value = (Long) getValue(columnIndex, JDBCType.BIGINT); loggerExternal.exiting(getClassNameLogging(), "getLong", value); - return null != value ? value.longValue() : 0; + return null != value ? value : 0; } public long getLong(String columnName) throws SQLServerException { @@ -2144,7 +2156,7 @@ public long getLong(String columnName) throws SQLServerException { checkClosed(); Long value = (Long) getValue(findColumn(columnName), JDBCType.BIGINT); loggerExternal.exiting(getClassNameLogging(), "getLong", value); - return null != value ? value.longValue() : 0; + return null != value ? value : 0; } public java.sql.ResultSetMetaData getMetaData() throws SQLServerException { @@ -2166,10 +2178,84 @@ public Object getObject(int columnIndex) throws SQLServerException { public T getObject(int columnIndex, Class type) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC41(); - - // The driver currently does not implement the optional JDBC APIs - throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); + loggerExternal.entering(getClassNameLogging(), "getObject", columnIndex); + checkClosed(); + Object returnValue; + if (type == String.class) { + returnValue = getString(columnIndex); + } + else if (type == Byte.class) { + byte byteValue = getByte(columnIndex); + returnValue = wasNull() ? null : byteValue; + } + else if (type == Short.class) { + short shortValue = getShort(columnIndex); + returnValue = wasNull() ? null : shortValue; + } + else if (type == Integer.class) { + int intValue = getInt(columnIndex); + returnValue = wasNull() ? null : intValue; + } + else if (type == Long.class) { + long longValue = getLong(columnIndex); + returnValue = wasNull() ? null : longValue; + } + else if (type == BigDecimal.class) { + returnValue = getBigDecimal(columnIndex); + } + else if (type == Boolean.class) { + boolean booleanValue = getBoolean(columnIndex); + returnValue = wasNull() ? null : booleanValue; + } + else if (type == java.sql.Date.class) { + returnValue = getDate(columnIndex); + } + else if (type == java.sql.Time.class) { + returnValue = getTime(columnIndex); + } + else if (type == java.sql.Timestamp.class) { + returnValue = getTimestamp(columnIndex); + } + else if (type == microsoft.sql.DateTimeOffset.class) { + returnValue = getDateTimeOffset(columnIndex); + } + else if (type == UUID.class) { + // read binary, avoid string allocation and parsing + byte[] guid = getBytes(columnIndex); + returnValue = guid != null ? Util.readGUIDtoUUID(guid) : null; + } + else if (type == SQLXML.class) { + returnValue = getSQLXML(columnIndex); + } + else if (type == Blob.class) { + returnValue = getBlob(columnIndex); + } + else if (type == Clob.class) { + returnValue = getClob(columnIndex); + } + else if (type == NClob.class) { + returnValue = getNClob(columnIndex); + } + else if (type == byte[].class) { + returnValue = getBytes(columnIndex); + } + else if (type == Float.class) { + float floatValue = getFloat(columnIndex); + returnValue = wasNull() ? null : floatValue; + } + else if (type == Double.class) { + double doubleValue = getDouble(columnIndex); + returnValue = wasNull() ? null : doubleValue; + } + else { + // if the type is not supported the specification says the should + // a SQLException instead of SQLFeatureNotSupportedException + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unsupportedConversionTo")); + Object[] msgArgs = {type}; + throw new SQLServerException(form.format(msgArgs), SQLState.DATA_EXCEPTION_NOT_SPECIFIC, DriverError.NOT_SET, null); + } + loggerExternal.exiting(getClassNameLogging(), "getObject", columnIndex); + return type.cast(returnValue); } public Object getObject(String columnName) throws SQLServerException { @@ -2182,10 +2268,11 @@ public Object getObject(String columnName) throws SQLServerException { public T getObject(String columnName, Class type) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC41(); - - // The driver currently does not implement the optional JDBC APIs - throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); + loggerExternal.entering(getClassNameLogging(), "getObject", columnName); + checkClosed(); + T value = getObject(findColumn(columnName), type); + loggerExternal.exiting(getClassNameLogging(), "getObject", value); + return value; } public short getShort(int columnIndex) throws SQLServerException { @@ -2193,7 +2280,7 @@ public short getShort(int columnIndex) throws SQLServerException { checkClosed(); Short value = (Short) getValue(columnIndex, JDBCType.SMALLINT); loggerExternal.exiting(getClassNameLogging(), "getShort", value); - return null != value ? value.shortValue() : 0; + return null != value ? value : 0; } public short getShort(String columnName) throws SQLServerException { @@ -2201,7 +2288,7 @@ public short getShort(String columnName) throws SQLServerException { checkClosed(); Short value = (Short) getValue(findColumn(columnName), JDBCType.SMALLINT); loggerExternal.exiting(getClassNameLogging(), "getShort", value); - return null != value ? value.shortValue() : 0; + return null != value ? value : 0; } public String getString(int columnIndex) throws SQLServerException { @@ -2232,7 +2319,6 @@ public String getString(String columnName) throws SQLServerException { public String getNString(int columnIndex) throws SQLException { loggerExternal.entering(getClassNameLogging(), "getNString", columnIndex); - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); String value = (String) getValue(columnIndex, JDBCType.NCHAR); loggerExternal.exiting(getClassNameLogging(), "getNString", value); @@ -2241,7 +2327,6 @@ public String getNString(int columnIndex) throws SQLException { public String getNString(String columnLabel) throws SQLException { loggerExternal.entering(getClassNameLogging(), "getNString", columnLabel); - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); String value = (String) getValue(findColumn(columnLabel), JDBCType.NCHAR); loggerExternal.exiting(getClassNameLogging(), "getNString", value); @@ -2606,7 +2691,6 @@ public Clob getClob(String colName) throws SQLServerException { } public NClob getNClob(int columnIndex) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "getNClob", columnIndex); checkClosed(); NClob value = (NClob) getValue(columnIndex, JDBCType.NCLOB); @@ -2615,7 +2699,6 @@ public NClob getNClob(int columnIndex) throws SQLException { } public NClob getNClob(String columnLabel) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "getNClob", columnLabel); checkClosed(); NClob value = (NClob) getValue(findColumn(columnLabel), JDBCType.NCLOB); @@ -2668,7 +2751,6 @@ public java.io.Reader getCharacterStream(String columnName) throws SQLServerExce } public Reader getNCharacterStream(int columnIndex) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "getNCharacterStream", columnIndex); checkClosed(); Reader value = (Reader) getStream(columnIndex, StreamType.NCHARACTER); @@ -2677,7 +2759,6 @@ public Reader getNCharacterStream(int columnIndex) throws SQLException { } public Reader getNCharacterStream(String columnLabel) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "getNCharacterStream", columnLabel); checkClosed(); Reader value = (Reader) getStream(findColumn(columnLabel), StreamType.NCHARACTER); @@ -2770,21 +2851,17 @@ public BigDecimal getSmallMoney(String columnName) throws SQLServerException { } public RowId getRowId(int columnIndex) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - // Not implemented throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); } public RowId getRowId(String columnLabel) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); // Not implemented throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); } public SQLXML getSQLXML(int columnIndex) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "getSQLXML", columnIndex); SQLXML xml = getSQLXMLInternal(columnIndex); loggerExternal.exiting(getClassNameLogging(), "getSQLXML", xml); @@ -2792,7 +2869,6 @@ public SQLXML getSQLXML(int columnIndex) throws SQLException { } public SQLXML getSQLXML(String columnLabel) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "getSQLXML", columnLabel); SQLXML xml = getSQLXMLInternal(findColumn(columnLabel)); loggerExternal.exiting(getClassNameLogging(), "getSQLXML", xml); @@ -2957,7 +3033,7 @@ public void updateBoolean(int index, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateBoolean", new Object[] {index, x}); checkClosed(); - updateValue(index, JDBCType.BIT, Boolean.valueOf(x), JavaType.BOOLEAN, false); + updateValue(index, JDBCType.BIT, x, JavaType.BOOLEAN, false); loggerExternal.exiting(getClassNameLogging(), "updateBoolean"); } @@ -2984,7 +3060,7 @@ public void updateBoolean(int index, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateBoolean", new Object[] {index, x, forceEncrypt}); checkClosed(); - updateValue(index, JDBCType.BIT, Boolean.valueOf(x), JavaType.BOOLEAN, forceEncrypt); + updateValue(index, JDBCType.BIT, x, JavaType.BOOLEAN, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "updateBoolean"); } @@ -2995,7 +3071,7 @@ public void updateByte(int index, loggerExternal.entering(getClassNameLogging(), "updateByte", new Object[] {index, x}); checkClosed(); - updateValue(index, JDBCType.TINYINT, Byte.valueOf(x), JavaType.BYTE, false); + updateValue(index, JDBCType.TINYINT, x, JavaType.BYTE, false); loggerExternal.exiting(getClassNameLogging(), "updateByte"); } @@ -3023,7 +3099,7 @@ public void updateByte(int index, loggerExternal.entering(getClassNameLogging(), "updateByte", new Object[] {index, x, forceEncrypt}); checkClosed(); - updateValue(index, JDBCType.TINYINT, Byte.valueOf(x), JavaType.BYTE, forceEncrypt); + updateValue(index, JDBCType.TINYINT, x, JavaType.BYTE, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "updateByte"); } @@ -3034,7 +3110,7 @@ public void updateShort(int index, loggerExternal.entering(getClassNameLogging(), "updateShort", new Object[] {index, x}); checkClosed(); - updateValue(index, JDBCType.SMALLINT, Short.valueOf(x), JavaType.SHORT, false); + updateValue(index, JDBCType.SMALLINT, x, JavaType.SHORT, false); loggerExternal.exiting(getClassNameLogging(), "updateShort"); } @@ -3062,7 +3138,7 @@ public void updateShort(int index, loggerExternal.entering(getClassNameLogging(), "updateShort", new Object[] {index, x, forceEncrypt}); checkClosed(); - updateValue(index, JDBCType.SMALLINT, Short.valueOf(x), JavaType.SHORT, forceEncrypt); + updateValue(index, JDBCType.SMALLINT, x, JavaType.SHORT, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "updateShort"); } @@ -3073,7 +3149,7 @@ public void updateInt(int index, loggerExternal.entering(getClassNameLogging(), "updateInt", new Object[] {index, x}); checkClosed(); - updateValue(index, JDBCType.INTEGER, Integer.valueOf(x), JavaType.INTEGER, false); + updateValue(index, JDBCType.INTEGER, x, JavaType.INTEGER, false); loggerExternal.exiting(getClassNameLogging(), "updateInt"); } @@ -3101,7 +3177,7 @@ public void updateInt(int index, loggerExternal.entering(getClassNameLogging(), "updateInt", new Object[] {index, x, forceEncrypt}); checkClosed(); - updateValue(index, JDBCType.INTEGER, Integer.valueOf(x), JavaType.INTEGER, forceEncrypt); + updateValue(index, JDBCType.INTEGER, x, JavaType.INTEGER, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "updateInt"); } @@ -3112,7 +3188,7 @@ public void updateLong(int index, loggerExternal.entering(getClassNameLogging(), "updateLong", new Object[] {index, x}); checkClosed(); - updateValue(index, JDBCType.BIGINT, Long.valueOf(x), JavaType.LONG, false); + updateValue(index, JDBCType.BIGINT, x, JavaType.LONG, false); loggerExternal.exiting(getClassNameLogging(), "updateLong"); } @@ -3140,7 +3216,7 @@ public void updateLong(int index, loggerExternal.entering(getClassNameLogging(), "updateLong", new Object[] {index, x, forceEncrypt}); checkClosed(); - updateValue(index, JDBCType.BIGINT, Long.valueOf(x), JavaType.LONG, forceEncrypt); + updateValue(index, JDBCType.BIGINT, x, JavaType.LONG, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "updateLong"); } @@ -3151,7 +3227,7 @@ public void updateFloat(int index, loggerExternal.entering(getClassNameLogging(), "updateFloat", new Object[] {index, x}); checkClosed(); - updateValue(index, JDBCType.REAL, Float.valueOf(x), JavaType.FLOAT, false); + updateValue(index, JDBCType.REAL, x, JavaType.FLOAT, false); loggerExternal.exiting(getClassNameLogging(), "updateFloat"); } @@ -3179,7 +3255,7 @@ public void updateFloat(int index, loggerExternal.entering(getClassNameLogging(), "updateFloat", new Object[] {index, x, forceEncrypt}); checkClosed(); - updateValue(index, JDBCType.REAL, Float.valueOf(x), JavaType.FLOAT, forceEncrypt); + updateValue(index, JDBCType.REAL, x, JavaType.FLOAT, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "updateFloat"); } @@ -3190,7 +3266,7 @@ public void updateDouble(int index, loggerExternal.entering(getClassNameLogging(), "updateDouble", new Object[] {index, x}); checkClosed(); - updateValue(index, JDBCType.DOUBLE, Double.valueOf(x), JavaType.DOUBLE, false); + updateValue(index, JDBCType.DOUBLE, x, JavaType.DOUBLE, false); loggerExternal.exiting(getClassNameLogging(), "updateDouble"); } @@ -3218,7 +3294,7 @@ public void updateDouble(int index, loggerExternal.entering(getClassNameLogging(), "updateDouble", new Object[] {index, x, forceEncrypt}); checkClosed(); - updateValue(index, JDBCType.DOUBLE, Double.valueOf(x), JavaType.DOUBLE, forceEncrypt); + updateValue(index, JDBCType.DOUBLE, x, JavaType.DOUBLE, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "updateDouble"); } @@ -3537,7 +3613,6 @@ public void updateNString(int columnIndex, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateNString", new Object[] {columnIndex, nString}); - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); updateValue(columnIndex, JDBCType.NVARCHAR, nString, JavaType.STRING, false); @@ -3567,7 +3642,6 @@ public void updateNString(int columnIndex, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateNString", new Object[] {columnIndex, nString, forceEncrypt}); - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); updateValue(columnIndex, JDBCType.NVARCHAR, nString, JavaType.STRING, forceEncrypt); @@ -3579,7 +3653,6 @@ public void updateNString(String columnLabel, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateNString", new Object[] {columnLabel, nString}); - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); updateValue(findColumn(columnLabel), JDBCType.NVARCHAR, nString, JavaType.STRING, false); @@ -3610,7 +3683,6 @@ public void updateNString(String columnLabel, if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateNString", new Object[] {columnLabel, nString, forceEncrypt}); - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); updateValue(findColumn(columnLabel), JDBCType.NVARCHAR, nString, JavaType.STRING, forceEncrypt); @@ -4108,7 +4180,6 @@ public void updateUniqueIdentifier(int index, public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateAsciiStream", new Object[] {columnIndex, x}); @@ -4133,7 +4204,6 @@ public void updateAsciiStream(int index, public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "updateAsciiStream", new Object[] {columnIndex, x, length}); checkClosed(); @@ -4144,7 +4214,6 @@ public void updateAsciiStream(int columnIndex, public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateAsciiStream", new Object[] {columnLabel, x}); @@ -4169,7 +4238,6 @@ public void updateAsciiStream(java.lang.String columnName, public void updateAsciiStream(String columnName, InputStream streamValue, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateAsciiStream", new Object[] {columnName, streamValue, length}); @@ -4181,7 +4249,6 @@ public void updateAsciiStream(String columnName, public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateBinaryStream", new Object[] {columnIndex, x}); @@ -4206,7 +4273,6 @@ public void updateBinaryStream(int columnIndex, public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateBinaryStream", new Object[] {columnIndex, x, length}); @@ -4218,7 +4284,6 @@ public void updateBinaryStream(int columnIndex, public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateBinaryStream", new Object[] {columnLabel, x}); @@ -4243,7 +4308,6 @@ public void updateBinaryStream(String columnName, public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateBinaryStream", new Object[] {columnLabel, x, length}); @@ -4255,7 +4319,6 @@ public void updateBinaryStream(String columnLabel, public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateCharacterStream", new Object[] {columnIndex, x}); @@ -4280,7 +4343,6 @@ public void updateCharacterStream(int columnIndex, public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateCharacterStream", new Object[] {columnIndex, x, length}); @@ -4292,7 +4354,6 @@ public void updateCharacterStream(int columnIndex, public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateCharacterStream", new Object[] {columnLabel, reader}); @@ -4317,7 +4378,6 @@ public void updateCharacterStream(String columnName, public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateCharacterStream", new Object[] {columnLabel, reader, length}); @@ -4329,7 +4389,6 @@ public void updateCharacterStream(String columnLabel, public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateNCharacterStream", new Object[] {columnIndex, x}); @@ -4342,7 +4401,6 @@ public void updateNCharacterStream(int columnIndex, public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateNCharacterStream", new Object[] {columnIndex, x, length}); @@ -4354,7 +4412,6 @@ public void updateNCharacterStream(int columnIndex, public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateNCharacterStream", new Object[] {columnLabel, reader}); @@ -4367,7 +4424,6 @@ public void updateNCharacterStream(String columnLabel, public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateNCharacterStream", new Object[] {columnLabel, reader, length}); @@ -4395,7 +4451,7 @@ public void updateObject(int index, loggerExternal.entering(getClassNameLogging(), "updateObject", new Object[] {index, x, scale}); checkClosed(); - updateObject(index, x, Integer.valueOf(scale), null, null, false); + updateObject(index, x, scale, null, null, false); loggerExternal.exiting(getClassNameLogging(), "updateObject"); } @@ -4425,7 +4481,7 @@ public void updateObject(int index, loggerExternal.entering(getClassNameLogging(), "updateObject", new Object[] {index, x, scale}); checkClosed(); - updateObject(index, x, Integer.valueOf(scale), null, precision, false); + updateObject(index, x, scale, null, precision, false); loggerExternal.exiting(getClassNameLogging(), "updateObject"); } @@ -4460,7 +4516,7 @@ public void updateObject(int index, loggerExternal.entering(getClassNameLogging(), "updateObject", new Object[] {index, x, scale, forceEncrypt}); checkClosed(); - updateObject(index, x, Integer.valueOf(scale), null, precision, forceEncrypt); + updateObject(index, x, scale, null, precision, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "updateObject"); } @@ -4538,7 +4594,7 @@ public void updateBoolean(String columnName, loggerExternal.entering(getClassNameLogging(), "updateBoolean", new Object[] {columnName, x}); checkClosed(); - updateValue(findColumn(columnName), JDBCType.BIT, Boolean.valueOf(x), JavaType.BOOLEAN, false); + updateValue(findColumn(columnName), JDBCType.BIT, x, JavaType.BOOLEAN, false); loggerExternal.exiting(getClassNameLogging(), "updateBoolean"); } @@ -4566,7 +4622,7 @@ public void updateBoolean(String columnName, loggerExternal.entering(getClassNameLogging(), "updateBoolean", new Object[] {columnName, x, forceEncrypt}); checkClosed(); - updateValue(findColumn(columnName), JDBCType.BIT, Boolean.valueOf(x), JavaType.BOOLEAN, forceEncrypt); + updateValue(findColumn(columnName), JDBCType.BIT, x, JavaType.BOOLEAN, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "updateBoolean"); } @@ -4617,7 +4673,7 @@ public void updateShort(String columnName, loggerExternal.entering(getClassNameLogging(), "updateShort", new Object[] {columnName, x}); checkClosed(); - updateValue(findColumn(columnName), JDBCType.SMALLINT, Short.valueOf(x), JavaType.SHORT, false); + updateValue(findColumn(columnName), JDBCType.SMALLINT, x, JavaType.SHORT, false); loggerExternal.exiting(getClassNameLogging(), "updateShort"); } @@ -4645,7 +4701,7 @@ public void updateShort(String columnName, loggerExternal.entering(getClassNameLogging(), "updateShort", new Object[] {columnName, x, forceEncrypt}); checkClosed(); - updateValue(findColumn(columnName), JDBCType.SMALLINT, Short.valueOf(x), JavaType.SHORT, forceEncrypt); + updateValue(findColumn(columnName), JDBCType.SMALLINT, x, JavaType.SHORT, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "updateShort"); } @@ -4656,7 +4712,7 @@ public void updateInt(String columnName, loggerExternal.entering(getClassNameLogging(), "updateInt", new Object[] {columnName, x}); checkClosed(); - updateValue(findColumn(columnName), JDBCType.INTEGER, Integer.valueOf(x), JavaType.INTEGER, false); + updateValue(findColumn(columnName), JDBCType.INTEGER, x, JavaType.INTEGER, false); loggerExternal.exiting(getClassNameLogging(), "updateInt"); } @@ -4684,7 +4740,7 @@ public void updateInt(String columnName, loggerExternal.entering(getClassNameLogging(), "updateInt", new Object[] {columnName, x, forceEncrypt}); checkClosed(); - updateValue(findColumn(columnName), JDBCType.INTEGER, Integer.valueOf(x), JavaType.INTEGER, forceEncrypt); + updateValue(findColumn(columnName), JDBCType.INTEGER, x, JavaType.INTEGER, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "updateInt"); } @@ -4695,7 +4751,7 @@ public void updateLong(String columnName, loggerExternal.entering(getClassNameLogging(), "updateLong", new Object[] {columnName, x}); checkClosed(); - updateValue(findColumn(columnName), JDBCType.BIGINT, Long.valueOf(x), JavaType.LONG, false); + updateValue(findColumn(columnName), JDBCType.BIGINT, x, JavaType.LONG, false); loggerExternal.exiting(getClassNameLogging(), "updateLong"); } @@ -4723,7 +4779,7 @@ public void updateLong(String columnName, loggerExternal.entering(getClassNameLogging(), "updateLong", new Object[] {columnName, x, forceEncrypt}); checkClosed(); - updateValue(findColumn(columnName), JDBCType.BIGINT, Long.valueOf(x), JavaType.LONG, forceEncrypt); + updateValue(findColumn(columnName), JDBCType.BIGINT, x, JavaType.LONG, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "updateLong"); } @@ -4734,7 +4790,7 @@ public void updateFloat(String columnName, loggerExternal.entering(getClassNameLogging(), "updateFloat", new Object[] {columnName, x}); checkClosed(); - updateValue(findColumn(columnName), JDBCType.REAL, Float.valueOf(x), JavaType.FLOAT, false); + updateValue(findColumn(columnName), JDBCType.REAL, x, JavaType.FLOAT, false); loggerExternal.exiting(getClassNameLogging(), "updateFloat"); } @@ -4762,7 +4818,7 @@ public void updateFloat(String columnName, loggerExternal.entering(getClassNameLogging(), "updateFloat", new Object[] {columnName, x, forceEncrypt}); checkClosed(); - updateValue(findColumn(columnName), JDBCType.REAL, Float.valueOf(x), JavaType.FLOAT, forceEncrypt); + updateValue(findColumn(columnName), JDBCType.REAL, x, JavaType.FLOAT, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "updateFloat"); } @@ -4773,7 +4829,7 @@ public void updateDouble(String columnName, loggerExternal.entering(getClassNameLogging(), "updateDouble", new Object[] {columnName, x}); checkClosed(); - updateValue(findColumn(columnName), JDBCType.DOUBLE, Double.valueOf(x), JavaType.DOUBLE, false); + updateValue(findColumn(columnName), JDBCType.DOUBLE, x, JavaType.DOUBLE, false); loggerExternal.exiting(getClassNameLogging(), "updateDouble"); } @@ -4801,7 +4857,7 @@ public void updateDouble(String columnName, loggerExternal.entering(getClassNameLogging(), "updateDouble", new Object[] {columnName, x, forceEncrypt}); checkClosed(); - updateValue(findColumn(columnName), JDBCType.DOUBLE, Double.valueOf(x), JavaType.DOUBLE, forceEncrypt); + updateValue(findColumn(columnName), JDBCType.DOUBLE, x, JavaType.DOUBLE, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "updateDouble"); } @@ -5446,7 +5502,7 @@ public void updateObject(String columnName, loggerExternal.entering(getClassNameLogging(), "updateObject", new Object[] {columnName, x, scale}); checkClosed(); - updateObject(findColumn(columnName), x, Integer.valueOf(scale), null, null, false); + updateObject(findColumn(columnName), x, scale, null, null, false); loggerExternal.exiting(getClassNameLogging(), "updateObject"); } @@ -5476,7 +5532,7 @@ public void updateObject(String columnName, loggerExternal.entering(getClassNameLogging(), "updateObject", new Object[] {columnName, x, precision, scale}); checkClosed(); - updateObject(findColumn(columnName), x, Integer.valueOf(scale), null, precision, false); + updateObject(findColumn(columnName), x, scale, null, precision, false); loggerExternal.exiting(getClassNameLogging(), "updateObject"); } @@ -5511,7 +5567,7 @@ public void updateObject(String columnName, loggerExternal.entering(getClassNameLogging(), "updateObject", new Object[] {columnName, x, precision, scale, forceEncrypt}); checkClosed(); - updateObject(findColumn(columnName), x, Integer.valueOf(scale), null, precision, forceEncrypt); + updateObject(findColumn(columnName), x, scale, null, precision, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "updateObject"); } @@ -5529,23 +5585,18 @@ public void updateObject(String columnName, public void updateRowId(int columnIndex, RowId x) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - // Not implemented throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); } public void updateRowId(String columnLabel, RowId x) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - // Not implemented throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported")); } public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateSQLXML", new Object[] {columnIndex, xmlObject}); updateSQLXMLInternal(columnIndex, xmlObject); @@ -5556,7 +5607,6 @@ public void updateSQLXML(String columnLabel, SQLXML x) throws SQLException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateSQLXML", new Object[] {columnLabel, x}); - DriverJDBCVersion.checkSupportsJDBC4(); updateSQLXMLInternal(findColumn(columnLabel), x); loggerExternal.exiting(getClassNameLogging(), "updateSQLXML"); } @@ -5564,7 +5614,6 @@ public void updateSQLXML(String columnLabel, public int getHoldability() throws SQLException { loggerExternal.entering(getClassNameLogging(), "getHoldability"); - DriverJDBCVersion.checkSupportsJDBC4(); checkClosed(); int holdability = @@ -5634,14 +5683,14 @@ final boolean doExecute() throws SQLServerException { // If no values were set for any columns and no columns are updatable, // then the table name cannot be determined, so error. Column tableColumn = null; - for (int i = 0; i < columns.length; i++) { - if (columns[i].hasUpdates()) { - tableColumn = columns[i]; + for (Column column : columns) { + if (column.hasUpdates()) { + tableColumn = column; break; } - if (null == tableColumn && columns[i].isUpdatable()) - tableColumn = columns[i]; + if (null == tableColumn && column.isUpdatable()) + tableColumn = column; } if (null == tableColumn) { @@ -5669,15 +5718,14 @@ private void doInsertRowRPC(TDSCommand command, tdsWriter.writeShort(TDS.PROCID_SP_CURSOR); tdsWriter.writeByte((byte) 0); // RPC procedure option 1 tdsWriter.writeByte((byte) 0); // RPC procedure option 2 - tdsWriter.writeRPCInt(null, new Integer(serverCursorId), false); - tdsWriter.writeRPCInt(null, new Integer(TDS.SP_CURSOR_OP_INSERT), false); - tdsWriter.writeRPCInt(null, new Integer(fetchBufferGetRow()), false); + tdsWriter.writeRPCInt(null, serverCursorId, false); + tdsWriter.writeRPCInt(null, (int) TDS.SP_CURSOR_OP_INSERT, false); + tdsWriter.writeRPCInt(null, fetchBufferGetRow(), false); if (hasUpdatedColumns()) { tdsWriter.writeRPCStringUnicode(tableName); - for (int i = 0; i < columns.length; i++) - columns[i].sendByRPC(tdsWriter, stmt.connection); + for (Column column : columns) column.sendByRPC(tdsWriter, stmt.connection); } else { tdsWriter.writeRPCStringUnicode(""); @@ -5744,23 +5792,22 @@ private void doUpdateRowRPC(TDSCommand command) throws SQLServerException { tdsWriter.writeShort(TDS.PROCID_SP_CURSOR); tdsWriter.writeByte((byte) 0); // RPC procedure option 1 tdsWriter.writeByte((byte) 0); // RPC procedure option 2 - tdsWriter.writeRPCInt(null, new Integer(serverCursorId), false); - tdsWriter.writeRPCInt(null, new Integer(TDS.SP_CURSOR_OP_UPDATE | TDS.SP_CURSOR_OP_SETPOSITION), false); - tdsWriter.writeRPCInt(null, new Integer(fetchBufferGetRow()), false); + tdsWriter.writeRPCInt(null, serverCursorId, false); + tdsWriter.writeRPCInt(null, TDS.SP_CURSOR_OP_UPDATE | TDS.SP_CURSOR_OP_SETPOSITION, false); + tdsWriter.writeRPCInt(null, fetchBufferGetRow(), false); tdsWriter.writeRPCStringUnicode(""); assert hasUpdatedColumns(); - for (int i = 0; i < columns.length; i++) - columns[i].sendByRPC(tdsWriter, stmt.connection); + for (Column column : columns) column.sendByRPC(tdsWriter, stmt.connection); TDSParser.parse(command.startResponse(), command.getLogContext()); } /** Determines whether there are updated columns in this result set. */ final boolean hasUpdatedColumns() { - for (int i = 0; i < columns.length; i++) - if (columns[i].hasUpdates()) + for (Column column : columns) + if (column.hasUpdates()) return true; return false; @@ -5817,9 +5864,9 @@ private void doDeleteRowRPC(TDSCommand command) throws SQLServerException { tdsWriter.writeShort(TDS.PROCID_SP_CURSOR); tdsWriter.writeByte((byte) 0); // RPC procedure option 1 tdsWriter.writeByte((byte) 0); // RPC procedure option 2 - tdsWriter.writeRPCInt(null, new Integer(serverCursorId), false); - tdsWriter.writeRPCInt(null, new Integer(TDS.SP_CURSOR_OP_DELETE | TDS.SP_CURSOR_OP_SETPOSITION), false); - tdsWriter.writeRPCInt(null, new Integer(fetchBufferGetRow()), false); + tdsWriter.writeRPCInt(null, serverCursorId, false); + tdsWriter.writeRPCInt(null, TDS.SP_CURSOR_OP_DELETE | TDS.SP_CURSOR_OP_SETPOSITION, false); + tdsWriter.writeRPCInt(null, fetchBufferGetRow(), false); tdsWriter.writeRPCStringUnicode(""); TDSParser.parse(command.startResponse(), command.getLogContext()); @@ -5976,7 +6023,6 @@ public void updateClob(int columnIndex, public void updateClob(int columnIndex, Reader reader) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateClob", new Object[] {columnIndex, reader}); @@ -5989,7 +6035,6 @@ public void updateClob(int columnIndex, public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateClob", new Object[] {columnIndex, reader, length}); @@ -6012,7 +6057,6 @@ public void updateClob(String columnName, public void updateClob(String columnLabel, Reader reader) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateClob", new Object[] {columnLabel, reader}); @@ -6025,7 +6069,6 @@ public void updateClob(String columnLabel, public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateClob", new Object[] {columnLabel, reader, length}); @@ -6037,7 +6080,6 @@ public void updateClob(String columnLabel, public void updateNClob(int columnIndex, NClob nClob) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateClob", new Object[] {columnIndex, nClob}); @@ -6049,7 +6091,6 @@ public void updateNClob(int columnIndex, public void updateNClob(int columnIndex, Reader reader) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateNClob", new Object[] {columnIndex, reader}); @@ -6062,7 +6103,6 @@ public void updateNClob(int columnIndex, public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateNClob", new Object[] {columnIndex, reader, length}); @@ -6074,7 +6114,6 @@ public void updateNClob(int columnIndex, public void updateNClob(String columnLabel, NClob nClob) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateNClob", new Object[] {columnLabel, nClob}); @@ -6086,7 +6125,6 @@ public void updateNClob(String columnLabel, public void updateNClob(String columnLabel, Reader reader) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateNClob", new Object[] {columnLabel, reader}); @@ -6099,7 +6137,6 @@ public void updateNClob(String columnLabel, public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateNClob", new Object[] {columnLabel, reader, length}); @@ -6122,7 +6159,6 @@ public void updateBlob(int columnIndex, public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateBlob", new Object[] {columnIndex, inputStream}); @@ -6135,7 +6171,6 @@ public void updateBlob(int columnIndex, public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateBlob", new Object[] {columnIndex, inputStream, length}); @@ -6158,7 +6193,6 @@ public void updateBlob(String columnName, public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateBlob", new Object[] {columnLabel, inputStream}); @@ -6171,7 +6205,6 @@ public void updateBlob(String columnLabel, public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "updateBlob", new Object[] {columnLabel, inputStream, length}); @@ -6389,10 +6422,10 @@ final boolean doExecute() throws SQLServerException { tdsWriter.writeShort(TDS.PROCID_SP_CURSORFETCH); tdsWriter.writeByte(TDS.RPC_OPTION_NO_METADATA); tdsWriter.writeByte((byte) 0); // RPC procedure option 2 - tdsWriter.writeRPCInt(null, new Integer(serverCursorId), false); - tdsWriter.writeRPCInt(null, new Integer(fetchType), false); - tdsWriter.writeRPCInt(null, new Integer(startRow), false); - tdsWriter.writeRPCInt(null, new Integer(numRows), false); + tdsWriter.writeRPCInt(null, serverCursorId, false); + tdsWriter.writeRPCInt(null, fetchType, false); + tdsWriter.writeRPCInt(null, startRow, false); + tdsWriter.writeRPCInt(null, numRows, false); // To free up the thread on the server that is feeding us these results, // read the entire response off the wire UNLESS this is a forward only @@ -6541,7 +6574,7 @@ final boolean doExecute() throws SQLServerException { tdsWriter.writeShort(TDS.PROCID_SP_CURSORCLOSE); tdsWriter.writeByte((byte) 0); // RPC procedure option 1 tdsWriter.writeByte((byte) 0); // RPC procedure option 2 - tdsWriter.writeRPCInt(null, new Integer(serverCursorId), false); + tdsWriter.writeRPCInt(null, serverCursorId, false); TDSParser.parse(startResponse(), getLogContext()); return true; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet42.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet42.java index a780e7e39..cc900e65f 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet42.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet42.java @@ -57,7 +57,7 @@ public void updateObject(int index, checkClosed(); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - updateObject(index, obj, Integer.valueOf(scale), JDBCType.of(targetSqlType.getVendorTypeNumber()), null, false); + updateObject(index, obj, scale, JDBCType.of(targetSqlType.getVendorTypeNumber()), null, false); loggerExternal.exiting(getClassNameLogging(), "updateObject"); } @@ -74,7 +74,7 @@ public void updateObject(int index, checkClosed(); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - updateObject(index, obj, Integer.valueOf(scale), JDBCType.of(targetSqlType.getVendorTypeNumber()), null, forceEncrypt); + updateObject(index, obj, scale, JDBCType.of(targetSqlType.getVendorTypeNumber()), null, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "updateObject"); } @@ -91,7 +91,7 @@ public void updateObject(String columnName, checkClosed(); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - updateObject(findColumn(columnName), obj, Integer.valueOf(scale), JDBCType.of(targetSqlType.getVendorTypeNumber()), null, false); + updateObject(findColumn(columnName), obj, scale, JDBCType.of(targetSqlType.getVendorTypeNumber()), null, false); loggerExternal.exiting(getClassNameLogging(), "updateObject"); } @@ -109,7 +109,7 @@ public void updateObject(String columnName, checkClosed(); // getVendorTypeNumber() returns the same constant integer values as in java.sql.Types - updateObject(findColumn(columnName), obj, Integer.valueOf(scale), JDBCType.of(targetSqlType.getVendorTypeNumber()), null, forceEncrypt); + updateObject(findColumn(columnName), obj, scale, JDBCType.of(targetSqlType.getVendorTypeNumber()), null, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "updateObject"); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSetMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSetMetaData.java index 6f7366cfc..693f3fe0c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSetMetaData.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSetMetaData.java @@ -63,13 +63,11 @@ private void checkClosed() throws SQLServerException { /* ------------------ JDBC API Methods --------------------- */ public boolean isWrapperFor(Class iface) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); boolean f = iface.isInstance(this); return f; } public T unwrap(Class iface) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); T t; try { t = iface.cast(this); @@ -122,8 +120,12 @@ public int getColumnType(int column) throws SQLServerException { if (null != cryptoMetadata) { typeInfo = cryptoMetadata.getBaseTypeInfo(); } - + JDBCType jdbcType = typeInfo.getSSType().getJDBCType(); + // in bulkcopy for instance, we need to return the real jdbc type which is sql variant and not the default Char one. + if ( SSType.SQL_VARIANT == typeInfo.getSSType()){ + jdbcType = JDBCType.SQL_VARIANT; + } int r = jdbcType.asJavaSqlType(); if (con.isKatmaiOrLater()) { SSType sqlType = typeInfo.getSSType(); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSecurityUtility.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSecurityUtility.java index fd0ee82b7..a54c8a6aa 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSecurityUtility.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSecurityUtility.java @@ -171,7 +171,6 @@ static void decryptSymmetricKey(CryptoMetadata md, assert null != cipherAlgorithm : "Cipher algorithm cannot be null in DecryptSymmetricKey"; md.cipherAlgorithm = cipherAlgorithm; md.encryptionKeyInfo = encryptionkeyInfoChosen; - return; } /* diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSortOrder.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSortOrder.java index acb2125ca..542912b76 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSortOrder.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSortOrder.java @@ -18,7 +18,7 @@ public enum SQLServerSortOrder { Descending (1), Unspecified (-1); - int value; + final int value; SQLServerSortOrder(int sortOrderVal) { value = sortOrderVal; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index af2418c07..622b285a8 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -8,6 +8,9 @@ package com.microsoft.sqlserver.jdbc; +import static com.microsoft.sqlserver.jdbc.SQLServerConnection.getCachedParsedSQL; +import static com.microsoft.sqlserver.jdbc.SQLServerConnection.parseAndCacheSQL; + import java.sql.BatchUpdateException; import java.sql.ResultSet; import java.sql.SQLException; @@ -24,6 +27,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.microsoft.sqlserver.jdbc.SQLServerConnection.Sha1HashKey; + /** * SQLServerStatment provides the basic implementation of JDBC statement functionality. It also provides a number of base class implementation methods * for the JDBC prepared statement and callable Statements. SQLServerStatement's basic role is to execute SQL statements and return update counts and @@ -424,7 +429,7 @@ boolean onDone(TDSReader tdsReader) throws SQLServerException { * The array of objects in a batched call. Applicable to statements and prepared statements When the iterativeBatching property is turned on. */ /** The buffer that accumulates batchable statements */ - private final ArrayList batchStatementBuffer = new ArrayList(); + private final ArrayList batchStatementBuffer = new ArrayList<>(); /** logging init at the construction */ static final private java.util.logging.Logger stmtlogger = java.util.logging.Logger @@ -626,8 +631,6 @@ public void close() throws SQLServerException { } public void closeOnCompletion() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC41(); - loggerExternal.entering(getClassNameLogging(), "closeOnCompletion"); checkClosed(); @@ -685,7 +688,7 @@ public int executeUpdate(String sql) throws SQLServerException { if (updateCount < Integer.MIN_VALUE || updateCount > Integer.MAX_VALUE) SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_updateCountOutofRange"), null, true); - loggerExternal.exiting(getClassNameLogging(), "executeUpdate", new Long(updateCount)); + loggerExternal.exiting(getClassNameLogging(), "executeUpdate", updateCount); return (int) updateCount; } @@ -709,7 +712,7 @@ public long executeLargeUpdate(String sql) throws SQLServerException { checkClosed(); executeStatement(new StmtExecCmd(this, sql, EXECUTE_UPDATE, NO_GENERATED_KEYS)); - loggerExternal.exiting(getClassNameLogging(), "executeLargeUpdate", new Long(updateCount)); + loggerExternal.exiting(getClassNameLogging(), "executeLargeUpdate", updateCount); return updateCount; } @@ -729,7 +732,7 @@ public boolean execute(String sql) throws SQLServerException { } checkClosed(); executeStatement(new StmtExecCmd(this, sql, EXECUTE, NO_GENERATED_KEYS)); - loggerExternal.exiting(getClassNameLogging(), "execute", Boolean.valueOf(null != resultSet)); + loggerExternal.exiting(getClassNameLogging(), "execute", null != resultSet); return null != resultSet; } @@ -763,10 +766,17 @@ final void processResponse(TDSReader tdsReader) throws SQLServerException { private String ensureSQLSyntax(String sql) throws SQLServerException { if (sql.indexOf(LEFT_CURLY_BRACKET) >= 0) { - JDBCSyntaxTranslator translator = new JDBCSyntaxTranslator(); - String execSyntax = translator.translate(sql); - procedureName = translator.getProcedureName(); - return execSyntax; + + Sha1HashKey cacheKey = new Sha1HashKey(sql); + + // Check for cached SQL metadata. + ParsedSQLCacheItem cacheItem = getCachedParsedSQL(cacheKey); + if (null == cacheItem) + cacheItem = parseAndCacheSQL(cacheKey, sql); + + // Retrieve from cache item. + procedureName = cacheItem.procedureName; + return cacheItem.processedSQL; } return sql; @@ -1022,16 +1032,16 @@ final void resetForReexecute() throws SQLServerException { /* L0 */ public final int getMaxFieldSize() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getMaxFieldSize"); checkClosed(); - loggerExternal.exiting(getClassNameLogging(), "getMaxFieldSize", new Integer(maxFieldSize)); + loggerExternal.exiting(getClassNameLogging(), "getMaxFieldSize", maxFieldSize); return maxFieldSize; } /* L0 */ public final void setMaxFieldSize(int max) throws SQLServerException { - loggerExternal.entering(getClassNameLogging(), "setMaxFieldSize", new Integer(max)); + loggerExternal.entering(getClassNameLogging(), "setMaxFieldSize", max); checkClosed(); if (max < 0) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLength")); - Object[] msgArgs = {new Integer(max)}; + Object[] msgArgs = {max}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, true); } maxFieldSize = max; @@ -1041,7 +1051,7 @@ final void resetForReexecute() throws SQLServerException { /* L0 */ public final int getMaxRows() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getMaxRows"); checkClosed(); - loggerExternal.exiting(getClassNameLogging(), "getMaxRows", new Integer(maxRows)); + loggerExternal.exiting(getClassNameLogging(), "getMaxRows", maxRows); return maxRows; } @@ -1052,18 +1062,18 @@ public final long getLargeMaxRows() throws SQLServerException { // SQL Server only supports integer limits for setting max rows. // So, getLargeMaxRows() and getMaxRows() will return the same value. - loggerExternal.exiting(getClassNameLogging(), "getLargeMaxRows", new Long(maxRows)); + loggerExternal.exiting(getClassNameLogging(), "getLargeMaxRows", (long) maxRows); return (long) getMaxRows(); } /* L0 */ public final void setMaxRows(int max) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "setMaxRows", new Integer(max)); + loggerExternal.entering(getClassNameLogging(), "setMaxRows", max); checkClosed(); if (max < 0) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidRowcount")); - Object[] msgArgs = {new Integer(max)}; + Object[] msgArgs = {max}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, true); } @@ -1079,7 +1089,7 @@ public final void setLargeMaxRows(long max) throws SQLServerException { DriverJDBCVersion.checkSupportsJDBC42(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "setLargeMaxRows", new Long(max)); + loggerExternal.entering(getClassNameLogging(), "setLargeMaxRows", max); // SQL server only supports integer limits for setting max rows. // If is bigger than integer limits then throw an exception, otherwise call setMaxRows(int) @@ -1092,7 +1102,7 @@ public final void setLargeMaxRows(long max) throws SQLServerException { /* L0 */ public final void setEscapeProcessing(boolean enable) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "setEscapeProcessing", Boolean.valueOf(enable)); + loggerExternal.entering(getClassNameLogging(), "setEscapeProcessing", enable); checkClosed(); escapeProcessing = enable; loggerExternal.exiting(getClassNameLogging(), "setEscapeProcessing"); @@ -1101,16 +1111,16 @@ public final void setLargeMaxRows(long max) throws SQLServerException { /* L0 */ public final int getQueryTimeout() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getQueryTimeout"); checkClosed(); - loggerExternal.exiting(getClassNameLogging(), "getQueryTimeout", new Integer(queryTimeout)); + loggerExternal.exiting(getClassNameLogging(), "getQueryTimeout", queryTimeout); return queryTimeout; } /* L0 */ public final void setQueryTimeout(int seconds) throws SQLServerException { - loggerExternal.entering(getClassNameLogging(), "setQueryTimeout", new Integer(seconds)); + loggerExternal.entering(getClassNameLogging(), "setQueryTimeout", seconds); checkClosed(); if (seconds < 0) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidQueryTimeOutValue")); - Object[] msgArgs = {new Integer(seconds)}; + Object[] msgArgs = {seconds}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, true); } queryTimeout = seconds; @@ -1173,7 +1183,7 @@ public final java.sql.ResultSet getResultSet() throws SQLServerException { if (updateCount < Integer.MIN_VALUE || updateCount > Integer.MAX_VALUE) SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_updateCountOutofRange"), null, true); - loggerExternal.exiting(getClassNameLogging(), "getUpdateCount", new Long(updateCount)); + loggerExternal.exiting(getClassNameLogging(), "getUpdateCount", updateCount); return (int) updateCount; } @@ -1183,7 +1193,7 @@ public final long getLargeUpdateCount() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getUpdateCount"); checkClosed(); - loggerExternal.exiting(getClassNameLogging(), "getUpdateCount", new Long(updateCount)); + loggerExternal.exiting(getClassNameLogging(), "getUpdateCount", updateCount); return updateCount; } @@ -1258,7 +1268,7 @@ final void processResults() throws SQLServerException { // Don't just return the value from the getNextResult() call, however. // The getMoreResults method has a subtle spec for its return value (see above). getNextResult(); - loggerExternal.exiting(getClassNameLogging(), "getMoreResults", Boolean.valueOf(null != resultSet)); + loggerExternal.exiting(getClassNameLogging(), "getMoreResults", null != resultSet); return null != resultSet; } @@ -1504,7 +1514,7 @@ boolean onInfo(TDSReader tdsReader) throws SQLServerException { infoToken.msg.getErrorNumber()); if (sqlWarnings == null) { - sqlWarnings = new Vector(); + sqlWarnings = new Vector<>(); } else { int n = sqlWarnings.size(); @@ -1601,14 +1611,14 @@ boolean consumeExecOutParam(TDSReader tdsReader) throws SQLServerException { public final void setFetchDirection(int nDir) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "setFetchDirection", new Integer(nDir)); + loggerExternal.entering(getClassNameLogging(), "setFetchDirection", nDir); checkClosed(); if ((ResultSet.FETCH_FORWARD != nDir && ResultSet.FETCH_REVERSE != nDir && ResultSet.FETCH_UNKNOWN != nDir) || (ResultSet.FETCH_FORWARD != nDir && (SQLServerResultSet.TYPE_SS_DIRECT_FORWARD_ONLY == resultSetType || SQLServerResultSet.TYPE_SS_SERVER_CURSOR_FORWARD_ONLY == resultSetType))) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidFetchDirection")); - Object[] msgArgs = {new Integer(nDir)}; + Object[] msgArgs = {nDir}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, false); } @@ -1619,13 +1629,13 @@ public final void setFetchDirection(int nDir) throws SQLServerException { public final int getFetchDirection() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getFetchDirection"); checkClosed(); - loggerExternal.exiting(getClassNameLogging(), "getFetchDirection", new Integer(nFetchDirection)); + loggerExternal.exiting(getClassNameLogging(), "getFetchDirection", nFetchDirection); return nFetchDirection; } /* L0 */ public final void setFetchSize(int rows) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "setFetchSize", new Integer(rows)); + loggerExternal.entering(getClassNameLogging(), "setFetchSize", rows); checkClosed(); if (rows < 0) SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_invalidFetchSize"), null, false); @@ -1637,21 +1647,21 @@ public final int getFetchDirection() throws SQLServerException { /* L0 */ public final int getFetchSize() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getFetchSize"); checkClosed(); - loggerExternal.exiting(getClassNameLogging(), "getFetchSize", new Integer(nFetchSize)); + loggerExternal.exiting(getClassNameLogging(), "getFetchSize", nFetchSize); return nFetchSize; } /* L0 */ public final int getResultSetConcurrency() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getResultSetConcurrency"); checkClosed(); - loggerExternal.exiting(getClassNameLogging(), "getResultSetConcurrency", new Integer(resultSetConcurrency)); + loggerExternal.exiting(getClassNameLogging(), "getResultSetConcurrency", resultSetConcurrency); return resultSetConcurrency; } /* L0 */ public final int getResultSetType() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getResultSetType"); checkClosed(); - loggerExternal.exiting(getClassNameLogging(), "getResultSetType", new Integer(appResultSetType)); + loggerExternal.exiting(getClassNameLogging(), "getResultSetType", appResultSetType); return appResultSetType; } @@ -1921,19 +1931,19 @@ private void doExecuteCursored(StmtExecCmd execCmd, tdsWriter.writeByte((byte) 0); // RPC procedure option 2 // OUT - tdsWriter.writeRPCInt(null, new Integer(0), true); + tdsWriter.writeRPCInt(null, 0, true); // IN tdsWriter.writeRPCStringUnicode(sql); // IN - tdsWriter.writeRPCInt(null, new Integer(getResultSetScrollOpt()), false); + tdsWriter.writeRPCInt(null, getResultSetScrollOpt(), false); // IN - tdsWriter.writeRPCInt(null, new Integer(getResultSetCCOpt()), false); + tdsWriter.writeRPCInt(null, getResultSetCCOpt(), false); // OUT - tdsWriter.writeRPCInt(null, new Integer(0), true); + tdsWriter.writeRPCInt(null, 0, true); ensureExecuteResultsReader(execCmd.startResponse(isResponseBufferingAdaptive)); startResults(); @@ -1946,14 +1956,14 @@ private void doExecuteCursored(StmtExecCmd execCmd, loggerExternal.entering(getClassNameLogging(), "getResultSetHoldability"); checkClosed(); int holdability = connection.getHoldability(); // For SQL Server must be the same as the connection - loggerExternal.exiting(getClassNameLogging(), "getResultSetHoldability", new Integer(holdability)); + loggerExternal.exiting(getClassNameLogging(), "getResultSetHoldability", holdability); return holdability; } public final boolean execute(java.lang.String sql, int autoGeneratedKeys) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) { - loggerExternal.entering(getClassNameLogging(), "execute", new Object[] {sql, new Integer(autoGeneratedKeys)}); + loggerExternal.entering(getClassNameLogging(), "execute", new Object[] {sql, autoGeneratedKeys}); if (Util.IsActivityTraceOn()) { loggerExternal.finer(toString() + " ActivityId: " + ActivityCorrelator.getNext().toString()); } @@ -1961,12 +1971,12 @@ public final boolean execute(java.lang.String sql, checkClosed(); if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS && autoGeneratedKeys != Statement.NO_GENERATED_KEYS) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidAutoGeneratedKeys")); - Object[] msgArgs = {new Integer(autoGeneratedKeys)}; + Object[] msgArgs = {autoGeneratedKeys}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, false); } executeStatement(new StmtExecCmd(this, sql, EXECUTE, autoGeneratedKeys)); - loggerExternal.exiting(getClassNameLogging(), "execute", Boolean.valueOf(null != resultSet)); + loggerExternal.exiting(getClassNameLogging(), "execute", null != resultSet); return null != resultSet; } @@ -1979,7 +1989,7 @@ public final boolean execute(java.lang.String sql, SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_invalidColumnArrayLength"), null, false); } boolean fSuccess = execute(sql, Statement.RETURN_GENERATED_KEYS); - loggerExternal.exiting(getClassNameLogging(), "execute", Boolean.valueOf(fSuccess)); + loggerExternal.exiting(getClassNameLogging(), "execute", fSuccess); return fSuccess; } @@ -1992,14 +2002,14 @@ public final boolean execute(java.lang.String sql, SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_invalidColumnArrayLength"), null, false); } boolean fSuccess = execute(sql, Statement.RETURN_GENERATED_KEYS); - loggerExternal.exiting(getClassNameLogging(), "execute", Boolean.valueOf(fSuccess)); + loggerExternal.exiting(getClassNameLogging(), "execute", fSuccess); return fSuccess; } public final int executeUpdate(String sql, int autoGeneratedKeys) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) { - loggerExternal.entering(getClassNameLogging(), "executeUpdate", new Object[] {sql, new Integer(autoGeneratedKeys)}); + loggerExternal.entering(getClassNameLogging(), "executeUpdate", new Object[] {sql, autoGeneratedKeys}); if (Util.IsActivityTraceOn()) { loggerExternal.finer(toString() + " ActivityId: " + ActivityCorrelator.getNext().toString()); } @@ -2007,7 +2017,7 @@ public final int executeUpdate(String sql, checkClosed(); if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS && autoGeneratedKeys != Statement.NO_GENERATED_KEYS) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidAutoGeneratedKeys")); - Object[] msgArgs = {new Integer(autoGeneratedKeys)}; + Object[] msgArgs = {autoGeneratedKeys}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, false); } executeStatement(new StmtExecCmd(this, sql, EXECUTE_UPDATE, autoGeneratedKeys)); @@ -2016,7 +2026,7 @@ public final int executeUpdate(String sql, if (updateCount < Integer.MIN_VALUE || updateCount > Integer.MAX_VALUE) SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_updateCountOutofRange"), null, true); - loggerExternal.exiting(getClassNameLogging(), "executeUpdate", new Long(updateCount)); + loggerExternal.exiting(getClassNameLogging(), "executeUpdate", updateCount); return (int) updateCount; } @@ -2026,7 +2036,7 @@ public final long executeLargeUpdate(String sql, DriverJDBCVersion.checkSupportsJDBC42(); if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) { - loggerExternal.entering(getClassNameLogging(), "executeLargeUpdate", new Object[] {sql, new Integer(autoGeneratedKeys)}); + loggerExternal.entering(getClassNameLogging(), "executeLargeUpdate", new Object[] {sql, autoGeneratedKeys}); if (Util.IsActivityTraceOn()) { loggerExternal.finer(toString() + " ActivityId: " + ActivityCorrelator.getNext().toString()); } @@ -2034,11 +2044,11 @@ public final long executeLargeUpdate(String sql, checkClosed(); if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS && autoGeneratedKeys != Statement.NO_GENERATED_KEYS) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidAutoGeneratedKeys")); - Object[] msgArgs = {new Integer(autoGeneratedKeys)}; + Object[] msgArgs = {autoGeneratedKeys}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, false); } executeStatement(new StmtExecCmd(this, sql, EXECUTE_UPDATE, autoGeneratedKeys)); - loggerExternal.exiting(getClassNameLogging(), "executeLargeUpdate", new Long(updateCount)); + loggerExternal.exiting(getClassNameLogging(), "executeLargeUpdate", updateCount); return updateCount; } @@ -2051,7 +2061,7 @@ public final int executeUpdate(java.lang.String sql, SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_invalidColumnArrayLength"), null, false); } int count = executeUpdate(sql, Statement.RETURN_GENERATED_KEYS); - loggerExternal.exiting(getClassNameLogging(), "executeUpdate", new Integer(count)); + loggerExternal.exiting(getClassNameLogging(), "executeUpdate", count); return count; } @@ -2066,7 +2076,7 @@ public final long executeLargeUpdate(java.lang.String sql, SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_invalidColumnArrayLength"), null, false); } long count = executeLargeUpdate(sql, Statement.RETURN_GENERATED_KEYS); - loggerExternal.exiting(getClassNameLogging(), "executeLargeUpdate", new Long(count)); + loggerExternal.exiting(getClassNameLogging(), "executeLargeUpdate", count); return count; } @@ -2079,7 +2089,7 @@ public final int executeUpdate(java.lang.String sql, SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_invalidColumnArrayLength"), null, false); } int count = executeUpdate(sql, Statement.RETURN_GENERATED_KEYS); - loggerExternal.exiting(getClassNameLogging(), "executeUpdate", new Integer(count)); + loggerExternal.exiting(getClassNameLogging(), "executeUpdate", count); return count; } @@ -2094,7 +2104,7 @@ public final long executeLargeUpdate(java.lang.String sql, SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_invalidColumnArrayLength"), null, false); } long count = executeLargeUpdate(sql, Statement.RETURN_GENERATED_KEYS); - loggerExternal.exiting(getClassNameLogging(), "executeLargeUpdate", new Long(count)); + loggerExternal.exiting(getClassNameLogging(), "executeLargeUpdate", count); return count; } @@ -2121,7 +2131,7 @@ public final ResultSet getGeneratedKeys() throws SQLServerException { /* L3 */ public final boolean getMoreResults(int mode) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) - loggerExternal.entering(getClassNameLogging(), "getMoreResults", new Integer(mode)); + loggerExternal.entering(getClassNameLogging(), "getMoreResults", mode); checkClosed(); if (KEEP_CURRENT_RESULT == mode) NotImplemented(); @@ -2136,17 +2146,15 @@ public final ResultSet getGeneratedKeys() throws SQLServerException { rsPrevious.close(); } catch (SQLException e) { - throw new SQLServerException(null, e.getMessage(), null, 0, false); + throw new SQLServerException(e.getMessage(), null, 0, e); } } - loggerExternal.exiting(getClassNameLogging(), "getMoreResults", Boolean.valueOf(fResults)); + loggerExternal.exiting(getClassNameLogging(), "getMoreResults", fResults); return fResults; } public boolean isClosed() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); - loggerExternal.entering(getClassNameLogging(), "isClosed"); boolean result = bIsClosed || connection.isSessionUnAvailable(); loggerExternal.exiting(getClassNameLogging(), "isClosed", result); @@ -2154,8 +2162,6 @@ public boolean isClosed() throws SQLException { } public boolean isCloseOnCompletion() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC41(); - loggerExternal.entering(getClassNameLogging(), "isCloseOnCompletion"); checkClosed(); loggerExternal.exiting(getClassNameLogging(), "isCloseOnCompletion", isCloseOnCompletion); @@ -2163,7 +2169,6 @@ public boolean isCloseOnCompletion() throws SQLException { } public boolean isPoolable() throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "isPoolable"); checkClosed(); loggerExternal.exiting(getClassNameLogging(), "isPoolable", stmtPoolable); @@ -2171,7 +2176,6 @@ public boolean isPoolable() throws SQLException { } public void setPoolable(boolean poolable) throws SQLException { - DriverJDBCVersion.checkSupportsJDBC4(); loggerExternal.entering(getClassNameLogging(), "setPoolable", poolable); checkClosed(); stmtPoolable = poolable; @@ -2180,15 +2184,13 @@ public void setPoolable(boolean poolable) throws SQLException { public boolean isWrapperFor(Class iface) throws SQLException { loggerExternal.entering(getClassNameLogging(), "isWrapperFor"); - DriverJDBCVersion.checkSupportsJDBC4(); boolean f = iface.isInstance(this); - loggerExternal.exiting(getClassNameLogging(), "isWrapperFor", Boolean.valueOf(f)); + loggerExternal.exiting(getClassNameLogging(), "isWrapperFor", f); return f; } public T unwrap(Class iface) throws SQLException { loggerExternal.entering(getClassNameLogging(), "unwrap"); - DriverJDBCVersion.checkSupportsJDBC4(); T t; try { t = iface.cast(this); @@ -2221,11 +2223,11 @@ public T unwrap(Class iface) throws SQLException { public final void setResponseBuffering(String value) throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "setResponseBuffering", value); checkClosed(); - if (value.equalsIgnoreCase("full")) { + if ("full".equalsIgnoreCase(value)) { isResponseBufferingAdaptive = false; wasResponseBufferingSet = true; } - else if (value.equalsIgnoreCase("adaptive")) { + else if ("adaptive".equalsIgnoreCase(value)) { isResponseBufferingAdaptive = true; wasResponseBufferingSet = true; } @@ -2358,7 +2360,7 @@ enum State { */ private final static Pattern limitOnlyPattern = Pattern.compile("\\{\\s*[lL][iI][mM][iI][tT]\\s+(((\\(|\\s)*)(\\d*|\\?)((\\)|\\s)*))\\s*\\}"); - /* + /** * This function translates the LIMIT escape syntax, {LIMIT [OFFSET ]} SQL Server does not support LIMIT syntax, the LIMIT escape * syntax is thus translated to use "TOP" syntax The OFFSET clause is not supported, and will throw an exception if used. * @@ -2384,7 +2386,7 @@ int translateLimit(StringBuffer sql, Matcher offsetMatcher = limitSyntaxWithOffset.matcher(sql); int startIndx = indx; - Stack topPosition = new Stack(); + Stack topPosition = new Stack<>(); State nextState = State.START; while (indx < sql.length()) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatementColumnEncryptionSetting.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatementColumnEncryptionSetting.java index 5006fa9d6..078c4760c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatementColumnEncryptionSetting.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatementColumnEncryptionSetting.java @@ -14,23 +14,23 @@ * to bypass encryption and gain access to plaintext data. */ public enum SQLServerStatementColumnEncryptionSetting { - /* + /** * if "Column Encryption Setting=Enabled" in the connection string, use Enabled. Otherwise, maps to Disabled. */ UseConnectionSetting, - /* + /** * Enables TCE for the command. Overrides the connection level setting for this command. */ Enabled, - /* + /** * Parameters will not be encrypted, only the ResultSet will be decrypted. This is an optimization for queries that do not pass any encrypted * input parameters. Overrides the connection level setting for this command. */ ResultSetOnly, - /* + /** * Disables TCE for the command.Overrides the connection level setting for this command. */ Disabled, diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSymmetricKeyCache.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSymmetricKeyCache.java index 36b40d6b0..76b886786 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSymmetricKeyCache.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSymmetricKeyCache.java @@ -68,7 +68,7 @@ public Thread newThread(Runnable r) { .getLogger("com.microsoft.sqlserver.jdbc.SQLServerSymmetricKeyCache"); private SQLServerSymmetricKeyCache() { - cache = new ConcurrentHashMap(); + cache = new ConcurrentHashMap<>(); } static SQLServerSymmetricKeyCache getInstance() { @@ -94,8 +94,8 @@ SQLServerSymmetricKey getKey(EncryptionKeyInfo keyInfo, String serverName = connection.getTrustedServerNameAE(); assert null != serverName : "serverName should not be null in getKey."; - StringBuffer keyLookupValuebuffer = new StringBuffer(serverName); - String keyLookupValue = null; + StringBuilder keyLookupValuebuffer = new StringBuilder(serverName); + String keyLookupValue; keyLookupValuebuffer.append(":"); keyLookupValuebuffer.append(DatatypeConverter.printBase64Binary((new String(keyInfo.encryptedKey, UTF_8)).getBytes())); @@ -110,7 +110,7 @@ SQLServerSymmetricKey getKey(EncryptionKeyInfo keyInfo, } Boolean[] hasEntry = new Boolean[1]; List trustedKeyPaths = SQLServerConnection.getColumnEncryptionTrustedMasterKeyPaths(serverName, hasEntry); - if (true == hasEntry[0]) { + if (hasEntry[0]) { if ((null == trustedKeyPaths) || (0 == trustedKeyPaths.size()) || (!trustedKeyPaths.contains(keyInfo.keyPath))) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_UntrustedKeyPath")); Object[] msgArgs = {keyInfo.keyPath, serverName}; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java index d09def6ab..8e72d0f35 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java @@ -14,6 +14,7 @@ import java.sql.Statement; import java.sql.Types; import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Properties; import java.util.Vector; import java.util.concurrent.atomic.AtomicInteger; @@ -380,56 +381,52 @@ private String typeDisplay(int type) { SQLServerCallableStatement cs = null; try { synchronized (this) { - if (controlConnection == null) { + if (!xaInitDone) { try { synchronized (xaInitLock) { - if (!xaInitDone) { - SQLServerCallableStatement initCS = null; - - initCS = (SQLServerCallableStatement) controlConnection.prepareCall("{call master..xp_sqljdbc_xa_init_ex(?, ?,?)}"); - initCS.registerOutParameter(1, Types.INTEGER); // Return status - initCS.registerOutParameter(2, Types.CHAR); // Return error message - initCS.registerOutParameter(3, Types.CHAR); // Return version number + SQLServerCallableStatement initCS = null; + + initCS = (SQLServerCallableStatement) controlConnection.prepareCall("{call master..xp_sqljdbc_xa_init_ex(?, ?,?)}"); + initCS.registerOutParameter(1, Types.INTEGER); // Return status + initCS.registerOutParameter(2, Types.CHAR); // Return error message + initCS.registerOutParameter(3, Types.CHAR); // Return version number + try { + initCS.execute(); + } + catch (SQLServerException eX) { try { - initCS.execute(); - } - catch (SQLServerException eX) { - try { - initCS.close(); - // Mapping between control connection and xaresource is 1:1 - controlConnection.close(); - } - catch (SQLException e3) { - // we really want to ignore this failue - if (xaLogger.isLoggable(Level.FINER)) - xaLogger.finer(toString() + " Ignoring exception when closing failed execution. exception:" + e3); - } - if (xaLogger.isLoggable(Level.FINER)) - xaLogger.finer(toString() + " exception:" + eX); - throw eX; - } - - // Check for error response from xp_sqljdbc_xa_init. - int initStatus = initCS.getInt(1); - String initErr = initCS.getString(2); - String versionNumberXADLL = initCS.getString(3); - if (xaLogger.isLoggable(Level.FINE)) - xaLogger.fine(toString() + " Server XA DLL version:" + versionNumberXADLL); - initCS.close(); - if (XA_OK != initStatus) { - assert null != initErr && initErr.length() > 1; + initCS.close(); + // Mapping between control connection and xaresource is 1:1 controlConnection.close(); - - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_failedToInitializeXA")); - Object[] msgArgs = {String.valueOf(initStatus), initErr}; - XAException xex = new XAException(form.format(msgArgs)); - xex.errorCode = initStatus; + } + catch (SQLException e3) { + // we really want to ignore this failue if (xaLogger.isLoggable(Level.FINER)) - xaLogger.finer(toString() + " exception:" + xex); - throw xex; + xaLogger.finer(toString() + " Ignoring exception when closing failed execution. exception:" + e3); } + if (xaLogger.isLoggable(Level.FINER)) + xaLogger.finer(toString() + " exception:" + eX); + throw eX; + } - xaInitDone = true; + // Check for error response from xp_sqljdbc_xa_init. + int initStatus = initCS.getInt(1); + String initErr = initCS.getString(2); + String versionNumberXADLL = initCS.getString(3); + if (xaLogger.isLoggable(Level.FINE)) + xaLogger.fine(toString() + " Server XA DLL version:" + versionNumberXADLL); + initCS.close(); + if (XA_OK != initStatus) { + assert null != initErr && initErr.length() > 1; + controlConnection.close(); + + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_failedToInitializeXA")); + Object[] msgArgs = {String.valueOf(initStatus), initErr}; + XAException xex = new XAException(form.format(msgArgs)); + xex.errorCode = initStatus; + if (xaLogger.isLoggable(Level.FINER)) + xaLogger.finer(toString() + " exception:" + xex); + throw xex; } } } @@ -440,6 +437,7 @@ private String typeDisplay(int type) { xaLogger.finer(toString() + " exception:" + form.format(msgArgs)); SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), null, true); } + xaInitDone = true; } } @@ -447,6 +445,7 @@ private String typeDisplay(int type) { case XA_START: if (!serverInfoRetrieved) { + Statement stmt = null; try { serverInfoRetrieved = true; // data are converted to varchar as type variant returned by SERVERPROPERTY is not supported by driver @@ -455,7 +454,7 @@ private String typeDisplay(int type) { + " convert(varchar(100), SERVERPROPERTY('ProductVersion')) as version," + " SUBSTRING(@@VERSION, CHARINDEX('<', @@VERSION)+2, 2)"; - Statement stmt = controlConnection.createStatement(); + stmt = controlConnection.createStatement(); ResultSet rs = stmt.executeQuery(query); rs.next(); @@ -477,7 +476,6 @@ else if (-1 != version.indexOf('.')) { ArchitectureOS = Integer.parseInt(rs.getString(4)); rs.close(); - stmt.close(); } // Got caught in static analysis. Catch only the thrown exceptions, do not catch // run time exceptions. @@ -485,6 +483,16 @@ else if (-1 != version.indexOf('.')) { if (xaLogger.isLoggable(Level.WARNING)) xaLogger.warning(toString() + " Cannot retrieve server information: :" + e.getMessage()); } + finally { + if (null != stmt) + try { + stmt.close(); + } + catch (SQLException e) { + if (xaLogger.isLoggable(Level.FINER)) + xaLogger.finer(toString()); + } + } } sContext = "START:"; @@ -794,7 +802,7 @@ else if (-1 != version.indexOf('.')) { /* L0 */ public Xid[] recover(int flags) throws XAException { XAReturnValue r = DTC_XA_Interface(XA_RECOVER, null, flags | tightlyCoupled); int offset = 0; - Vector v = new Vector(); + ArrayList al = new ArrayList<>(); // If no XID's found, return zero length XID array (don't return null). // @@ -827,11 +835,11 @@ else if (-1 != version.indexOf('.')) { System.arraycopy(r.bData, offset, bid, 0, bid_len); offset += bid_len; XidImpl xid = new XidImpl(formatId, gid, bid); - v.add(xid); + al.add(xid); } - XidImpl xids[] = new XidImpl[v.size()]; - for (int i = 0; i < v.size(); i++) { - xids[i] = v.elementAt(i); + XidImpl xids[] = new XidImpl[al.size()]; + for (int i = 0; i < al.size(); i++) { + xids[i] = al.get(i); if (xaLogger.isLoggable(Level.FINER)) xaLogger.finer(toString() + xids[i].toString()); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SimpleInputStream.java b/src/main/java/com/microsoft/sqlserver/jdbc/SimpleInputStream.java index fa319101f..917ebca00 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SimpleInputStream.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SimpleInputStream.java @@ -302,7 +302,7 @@ public int read(byte b[], if (isEOS()) return -1; - int readAmount = 0; + int readAmount; if (streamPos + maxBytes > payloadLength) { readAmount = payloadLength - streamPos; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SqlVariant.java b/src/main/java/com/microsoft/sqlserver/jdbc/SqlVariant.java new file mode 100644 index 000000000..2d4c13436 --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SqlVariant.java @@ -0,0 +1,211 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc; + +import java.text.MessageFormat; + +/** + * This class holds information regarding the basetype of a sql_variant data. + * + */ + +/** + * Enum for valid probBytes for different TDSTypes + * + */ +enum sqlVariantProbBytes { + INTN(0), + INT8(0), + INT4(0), + INT2(0), + INT1(0), + FLOAT4(0), + FLOAT8(0), + DATETIME4(0), + DATETIME8(0), + MONEY4(0), + MONEY8(0), + BITN(0), + GUID(0), + DATEN(0), + TIMEN(1), + DATETIME2N(1), + DECIMALN(2), + NUMERICN(2), + BIGBINARY(2), + BIGVARBINARY(2), + BIGCHAR(7), + BIGVARCHAR(7), + NCHAR(7), + NVARCHAR(7); + + private final int intValue; + + private static final int MAXELEMENTS = 23; + private static final sqlVariantProbBytes valuesTypes[] = new sqlVariantProbBytes[MAXELEMENTS]; + + private sqlVariantProbBytes(int intValue) { + this.intValue = intValue; + } + + int getIntValue() { + return intValue; + } + + static sqlVariantProbBytes valueOf(int intValue) { + sqlVariantProbBytes tdsType; + + if (!(0 <= intValue && intValue < valuesTypes.length) || null == (tdsType = valuesTypes[intValue])) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unknownSSType")); + Object[] msgArgs = {new Integer(intValue)}; + throw new IllegalArgumentException(form.format(msgArgs)); + } + + return tdsType; + } + +} + +public class SqlVariant { + + private int baseType; + private int precision; + private int scale; + private int maxLength; // for Character basetypes in sqlVariant + private SQLCollation collation; // for Character basetypes in sqlVariant + private boolean isBaseTypeTime = false; // we need this when we need to read time as timestamp (for instance in bulkcopy) + private JDBCType baseJDBCType; + + /** + * Constructor for sqlVariant + */ + SqlVariant(int baseType) { + this.baseType = baseType; + } + + /** + * Check if the basetype for variant is of time value + * + * @return + */ + boolean isBaseTypeTimeValue() { + return this.isBaseTypeTime; + } + + void setIsBaseTypeTimeValue(boolean isBaseTypeTime) { + this.isBaseTypeTime = isBaseTypeTime; + } + + /** + * store the base type for sql-variant + * + * @param baseType + */ + void setBaseType(int baseType) { + this.baseType = baseType; + } + + /** + * retrieves the base type for sql-variant + * + * @return + */ + int getBaseType() { + return this.baseType; + } + + /** + * Store the basetype as jdbc type + * + * @param baseJDBCType + */ + void setBaseJDBCType(JDBCType baseJDBCType) { + this.baseJDBCType = baseJDBCType; + } + + /** + * retrieves the base type as jdbc type + * + * @return + */ + JDBCType getBaseJDBCType() { + return this.baseJDBCType; + } + + /** + * stores the scale if applicable + * + * @param scale + */ + void setScale(int scale) { + this.scale = scale; + } + + /** + * retrieves the scale + * + * @return + */ + int getScale() { + return this.scale; + } + + /** + * stores the precision if applicable + * + * @param precision + */ + void setPrecision(int precision) { + this.precision = precision; + } + + /** + * retrieves the precision + * + * @return + */ + int getPrecision() { + return this.precision; + } + + /** + * stores the collation if applicable + * + * @param collation + */ + void setCollation(SQLCollation collation) { + this.collation = collation; + } + + /** + * Retrieves the collation + * + * @return + */ + SQLCollation getCollation() { + return this.collation; + } + + /** + * stores the maximum length + * + * @param maxLength + */ + void setMaxLength(int maxLength) { + this.maxLength = maxLength; + } + + /** + * retrieves the maximum length + * + * @return + */ + int getMaxLength() { + return this.maxLength; + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/StreamColInfo.java b/src/main/java/com/microsoft/sqlserver/jdbc/StreamColInfo.java index a5fcc1b2f..e0d7eaeb5 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/StreamColInfo.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/StreamColInfo.java @@ -36,9 +36,7 @@ int applyTo(Column[] columns) throws SQLServerException { // Read and apply the column info for each column TDSReaderMark currentMark = tdsReader.mark(); tdsReader.reset(colInfoMark); - for (int i = 0; i < columns.length; i++) { - Column col = columns[i]; - + for (Column col : columns) { // Ignore the column number, per TDS spec. // Column info is returned for each column, ascending by column index, // so iterating through the column info is sufficient. diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/StreamTabName.java b/src/main/java/com/microsoft/sqlserver/jdbc/StreamTabName.java index 1dc2a5085..31acff46f 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/StreamTabName.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/StreamTabName.java @@ -44,14 +44,11 @@ void applyTo(Column[] columns, tableNames[i] = tdsReader.readSQLIdentifier(); // Apply the table names to their appropriate columns - for (int i = 0; i < columns.length; i++) { - Column col = columns[i]; - + for (Column col : columns) { if (col.getTableNum() > 0) col.setTableName(tableNames[col.getTableNum() - 1]); } tdsReader.reset(currentMark); - return; } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/StringUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/StringUtils.java index f43ed1caa..241a5e2ca 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/StringUtils.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/StringUtils.java @@ -54,16 +54,14 @@ public static boolean isNumeric(final String str) { * @return {@link Boolean} if provided String is Integer or not. */ public static boolean isInteger(final String str) { - boolean isInteger = false; - try { - int i = Integer.parseInt(str); - isInteger = true; - }catch(NumberFormatException e) { - //Nothing. this is not integer. + Integer.parseInt(str); + return true; } - - return isInteger; + catch (NumberFormatException e) { + // Nothing. this is not integer. + } + return false; } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/TVP.java b/src/main/java/com/microsoft/sqlserver/jdbc/TVP.java index dfd312926..fa3d07dd6 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/TVP.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/TVP.java @@ -12,10 +12,12 @@ import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.text.MessageFormat; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; enum TVPType { ResultSet, @@ -47,8 +49,8 @@ class TVP { Map columnMetadata = null; Iterator> sourceDataTableRowIterator = null; ISQLServerDataRecord sourceRecord = null; - TVPType tvpType = null; + Set columnNames = null; // MultiPartIdentifierState enum MPIState { @@ -63,7 +65,7 @@ enum MPIState { void initTVP(TVPType type, String tvpPartName) throws SQLServerException { tvpType = type; - columnMetadata = new LinkedHashMap(); + columnMetadata = new LinkedHashMap<>(); parseTypeName(tvpPartName); } @@ -95,6 +97,7 @@ void initTVP(TVPType type, ISQLServerDataRecord tvpRecord) throws SQLServerException { initTVP(TVPType.ISQLServerDataRecord, tvpPartName); sourceRecord = tvpRecord; + columnNames = new HashSet<>(); // Populate TVP metdata from ISQLServerDataRecord. populateMetadataFromDataRecord(); @@ -112,7 +115,14 @@ Object[] getRowData() throws SQLServerException { Object[] rowData = new Object[colCount]; for (int i = 0; i < colCount; i++) { try { - rowData[i] = sourceResultSet.getObject(i + 1); + // for Time types, getting Timestamp instead of Time, because this value will be converted to String later on. If the value is a + // time object, the millisecond would be removed. + if (java.sql.Types.TIME == sourceResultSet.getMetaData().getColumnType(i + 1)) { + rowData[i] = sourceResultSet.getTimestamp(i + 1); + } + else { + rowData[i] = sourceResultSet.getObject(i + 1); + } } catch (SQLException e) { throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), e); @@ -151,9 +161,7 @@ void populateMetadataFromDataTable() throws SQLServerException { if (null == dataTableMetaData || dataTableMetaData.isEmpty()) { throw new SQLServerException(SQLServerException.getErrString("R_TVPEmptyMetadata"), null); } - Iterator> columnsIterator = dataTableMetaData.entrySet().iterator(); - while (columnsIterator.hasNext()) { - Map.Entry pair = columnsIterator.next(); + for (Entry pair : dataTableMetaData.entrySet()) { // duplicate column names for the dataTable will be checked in the SQLServerDataTable. columnMetadata.put(pair.getKey(), new SQLServerMetaData(pair.getValue().columnName, pair.getValue().javaSqlType, pair.getValue().precision, pair.getValue().scale)); @@ -181,8 +189,9 @@ void populateMetadataFromDataRecord() throws SQLServerException { throw new SQLServerException(SQLServerException.getErrString("R_TVPEmptyMetadata"), null); } for (int i = 0; i < sourceRecord.getColumnCount(); i++) { + Util.checkDuplicateColumnName(sourceRecord.getColumnMetaData(i + 1).columnName, columnNames); + // Make a copy here as we do not want to change user's metadata. - Util.checkDuplicateColumnName(sourceRecord.getColumnMetaData(i + 1).columnName, columnMetadata); SQLServerMetaData metaData = new SQLServerMetaData(sourceRecord.getColumnMetaData(i + 1)); columnMetadata.put(i, metaData); } @@ -194,9 +203,7 @@ void validateOrderProperty() throws SQLServerException { int maxSortOrdinal = -1; int sortCount = 0; - Iterator> columnsIterator = columnMetadata.entrySet().iterator(); - while (columnsIterator.hasNext()) { - Map.Entry columnPair = columnsIterator.next(); + for (Entry columnPair : columnMetadata.entrySet()) { SQLServerSortOrder columnSortOrder = columnPair.getValue().sortOrder; int columnSortOrdinal = columnPair.getValue().sortOrdinal; @@ -204,13 +211,13 @@ void validateOrderProperty() throws SQLServerException { // check if there's no way sort order could be monotonically increasing if (columnCount <= columnSortOrdinal) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_TVPSortOrdinalGreaterThanFieldCount")); - throw new SQLServerException(form.format(new Object[] {columnSortOrdinal, columnPair.getKey()}), null, 0, null); + throw new SQLServerException(form.format(new Object[]{columnSortOrdinal, columnPair.getKey()}), null, 0, null); } // Check to make sure we haven't seen this ordinal before if (sortOrdinalSpecified[columnSortOrdinal]) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_TVPDuplicateSortOrdinal")); - throw new SQLServerException(form.format(new Object[] {columnSortOrdinal}), null, 0, null); + throw new SQLServerException(form.format(new Object[]{columnSortOrdinal}), null, 0, null); } sortOrdinalSpecified[columnSortOrdinal] = true; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ThreePartName.java b/src/main/java/com/microsoft/sqlserver/jdbc/ThreePartName.java new file mode 100644 index 000000000..6882de47d --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ThreePartName.java @@ -0,0 +1,71 @@ +package com.microsoft.sqlserver.jdbc; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class ThreePartName { + /* + * Three part names parsing For metdata calls we parse the procedure name into parts so we can use it in sp_sproc_columns sp_sproc_columns + * [[@procedure_name =] 'name'] [,[@procedure_owner =] 'owner'] [,[@procedure_qualifier =] 'qualifier'] + * + */ + private static final Pattern THREE_PART_NAME = Pattern.compile(JDBCSyntaxTranslator.getSQLIdentifierWithGroups()); + + private final String databasePart; + private final String ownerPart; + private final String procedurePart; + + private ThreePartName(String databasePart, String ownerPart, String procedurePart) { + this.databasePart = databasePart; + this.ownerPart = ownerPart; + this.procedurePart = procedurePart; + } + + String getDatabasePart() { + return databasePart; + } + + String getOwnerPart() { + return ownerPart; + } + + String getProcedurePart() { + return procedurePart; + } + + static ThreePartName parse(String theProcName) { + String procedurePart = null; + String ownerPart = null; + String databasePart = null; + Matcher matcher; + if (null != theProcName) { + matcher = THREE_PART_NAME.matcher(theProcName); + if (matcher.matches()) { + if (matcher.group(2) != null) { + databasePart = matcher.group(1); + + // if we have two parts look to see if the last part can be broken even more + matcher = THREE_PART_NAME.matcher(matcher.group(2)); + if (matcher.matches()) { + if (null != matcher.group(2)) { + ownerPart = matcher.group(1); + procedurePart = matcher.group(2); + } + else { + ownerPart = databasePart; + databasePart = null; + procedurePart = matcher.group(1); + } + } + } + else { + procedurePart = matcher.group(1); + } + } + else { + procedurePart = theProcName; + } + } + return new ThreePartName(databasePart, ownerPart, procedurePart); + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java index 5218c9ea5..3c09f9519 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Properties; +import java.util.Set; import java.util.UUID; import java.util.logging.Level; import java.util.logging.LogManager; @@ -249,12 +250,13 @@ static BigDecimal readBigDecimal(byte valueBytes[], String result = ""; String name = ""; String value = ""; + StringBuilder builder; if (!tmpUrl.startsWith(sPrefix)) return null; tmpUrl = tmpUrl.substring(sPrefix.length()); - int i = 0; + int i; // Simple finite state machine. // always look at one char at a time @@ -279,7 +281,10 @@ static BigDecimal readBigDecimal(byte valueBytes[], state = inName; } else { - result = result + ch; + builder = new StringBuilder(); + builder.append(result); + builder.append(ch); + result = builder.toString(); state = inServerName; } break; @@ -305,7 +310,10 @@ else if (ch == ':') state = inInstanceName; } else { - result = result + ch; + builder = new StringBuilder(); + builder.append(result); + builder.append(ch); + result = builder.toString(); // same state } break; @@ -322,7 +330,10 @@ else if (ch == ':') state = inName; } else { - result = result + ch; + builder = new StringBuilder(); + builder.append(result); + builder.append(ch); + result = builder.toString(); // same state } break; @@ -343,7 +354,10 @@ else if (ch == ':') state = inPort; } else { - result = result + ch; + builder = new StringBuilder(); + builder.append(result); + builder.append(ch); + result = builder.toString(); // same state } break; @@ -367,7 +381,10 @@ else if (ch == ':') // same state } else { - name = name + ch; + builder = new StringBuilder(); + builder.append(name); + builder.append(ch); + name = builder.toString(); // same state } break; @@ -379,9 +396,14 @@ else if (ch == ':') name = SQLServerDriver.getNormalizedPropertyName(name, logger); if (null != name) { if (logger.isLoggable(Level.FINE)) { - if ((false == name.equals(SQLServerDriverStringProperty.USER.toString())) - && (false == name.equals(SQLServerDriverStringProperty.PASSWORD.toString()))) - logger.fine("Property:" + name + " Value:" + value); + if (false == name.equals(SQLServerDriverStringProperty.USER.toString())) { + if (!name.toLowerCase(Locale.ENGLISH).contains("password")) { + logger.fine("Property:" + name + " Value:" + value); + } + else { + logger.fine("Property:" + name); + } + } } p.put(name, value); } @@ -399,7 +421,10 @@ else if (ch == '{') { } } else { - value = value + ch; + builder = new StringBuilder(); + builder.append(value); + builder.append(ch); + value = builder.toString(); // same state } break; @@ -424,7 +449,10 @@ else if (ch == '{') { state = inEscapedValueEnd; } else { - value = value + ch; + builder = new StringBuilder(); + builder.append(value); + builder.append(ch); + value = builder.toString(); // same state } break; @@ -536,28 +564,22 @@ static String escapeSQLId(String inID) { outID.append(']'); return outID.toString(); } - + + /** + * Checks if duplicate columns exists, in O(n) time. + * + * @param columnName + * the name of the column + * @throws SQLServerException + * when a duplicate column exists + */ static void checkDuplicateColumnName(String columnName, - Map columnMetadata) throws SQLServerException { - if (columnMetadata.get(0) instanceof SQLServerMetaData) { - for (Entry entry : columnMetadata.entrySet()) { - SQLServerMetaData value = (SQLServerMetaData) entry.getValue(); - if (value.columnName.equals(columnName)) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_TVPDuplicateColumnName")); - Object[] msgArgs = {columnName}; - throw new SQLServerException(null, form.format(msgArgs), null, 0, false); - } - } - } - else if (columnMetadata.get(0) instanceof SQLServerDataColumn) { - for (Entry entry : columnMetadata.entrySet()) { - SQLServerDataColumn value = (SQLServerDataColumn) entry.getValue(); - if (value.columnName.equals(columnName)) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_TVPDuplicateColumnName")); - Object[] msgArgs = {columnName}; - throw new SQLServerException(null, form.format(msgArgs), null, 0, false); - } - } + Set columnNames) throws SQLServerException { + //columnList.add will return false if the same column name already exists + if (!columnNames.add(columnName)) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_TVPDuplicateColumnName")); + Object[] msgArgs = {columnName}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, false); } } @@ -584,9 +606,9 @@ static String readUnicodeString(byte[] b, catch (IndexOutOfBoundsException ex) { String txtMsg = SQLServerException.checkAndAppendClientConnId(SQLServerException.getErrString("R_stringReadError"), conn); MessageFormat form = new MessageFormat(txtMsg); - Object[] msgArgs = {new Integer(offset)}; + Object[] msgArgs = {offset}; // Re-throw SQLServerException if conversion fails. - throw new SQLServerException(null, form.format(msgArgs), null, 0, true); + throw new SQLServerException(form.format(msgArgs), null, 0, ex); } } @@ -605,8 +627,8 @@ static String byteToHexDisplayString(byte[] b) { int hexVal; StringBuilder sb = new StringBuilder(b.length * 2 + 2); sb.append("0x"); - for (int i = 0; i < b.length; i++) { - hexVal = b[i] & 0xFF; + for (byte aB : b) { + hexVal = aB & 0xFF; sb.append(hexChars[(hexVal & 0xF0) >> 4]); sb.append(hexChars[(hexVal & 0x0F)]); } @@ -693,6 +715,46 @@ static final byte[] asGuidByteArray(UUID aId) { return buffer; } + static final UUID readGUIDtoUUID(byte[] inputGUID) throws SQLServerException { + if (inputGUID.length != 16) { + throw new SQLServerException("guid length must be 16", null); + } + + // For the first three fields, UUID uses network byte order, + // Guid uses native byte order. So we need to reverse + // the first three fields before creating a UUID. + + byte tmpByte; + + // Reverse the first 4 bytes + tmpByte = inputGUID[0]; + inputGUID[0] = inputGUID[3]; + inputGUID[3] = tmpByte; + tmpByte = inputGUID[1]; + inputGUID[1] = inputGUID[2]; + inputGUID[2] = tmpByte; + + // Reverse the 5th and the 6th + tmpByte = inputGUID[4]; + inputGUID[4] = inputGUID[5]; + inputGUID[5] = tmpByte; + + // Reverse the 7th and the 8th + tmpByte = inputGUID[6]; + inputGUID[6] = inputGUID[7]; + inputGUID[7] = tmpByte; + + long msb = 0L; + for (int i = 0; i < 8; i++) { + msb = msb << 8 | ((long) inputGUID[i] & 0xFFL); + } + long lsb = 0L; + for (int i = 8; i < 16; i++) { + lsb = lsb << 8 | ((long) inputGUID[i] & 0xFFL); + } + return new UUID(msb, lsb); + } + static final String readGUID(byte[] inputGUID) throws SQLServerException { String guidTemplate = "NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN"; byte guid[] = inputGUID; @@ -729,7 +791,7 @@ static final String readGUID(byte[] inputGUID) throws SQLServerException { static boolean IsActivityTraceOn() { LogManager lm = LogManager.getLogManager(); String activityTrace = lm.getProperty(ActivityIdTraceProperty); - if (null != activityTrace && activityTrace.equalsIgnoreCase("on")) + if ("on".equalsIgnoreCase(activityTrace)) return true; else return false; @@ -747,7 +809,6 @@ static boolean shouldHonorAEForRead(SQLServerStatementColumnEncryptionSetting st case Disabled: return false; case Enabled: - return true; case ResultSetOnly: return true; default: @@ -767,11 +828,10 @@ static boolean shouldHonorAEForParameters(SQLServerStatementColumnEncryptionSett // Command leve setting trumps all switch (stmtColumnEncryptionSetting) { case Disabled: + case ResultSetOnly: return false; case Enabled: return true; - case ResultSetOnly: - return false; default: // Check connection level setting! assert SQLServerStatementColumnEncryptionSetting.UseConnectionSetting == stmtColumnEncryptionSetting : "Unexpected value for command level override"; @@ -850,7 +910,7 @@ else if (JDBCType.BINARY == jdbcType || JDBCType.VARBINARY == jdbcType) { return ((null == value) ? 0 : ((byte[]) value).length); case BIGDECIMAL: - int length = -1; + int length; if (null == precision) { if (null == value) { @@ -859,8 +919,14 @@ else if (JDBCType.BINARY == jdbcType || JDBCType.VARBINARY == jdbcType) { else { if (0 == ((BigDecimal) value).intValue()) { String s = "" + value; - s = s.replaceAll("\\.", ""); s = s.replaceAll("\\-", ""); + if (s.startsWith("0.")) { + // remove the leading zero, eg., for 0.32, the precision should be 2 and not 3 + s = s.replaceAll("0\\.", ""); + } + else { + s = s.replaceAll("\\.", ""); + } length = s.length(); } // if the value is in scientific notation format @@ -886,13 +952,12 @@ else if (("" + value).contains("E")) { case TIME: case DATETIMEOFFSET: return ((null == scale) ? TDS.MAX_FRACTIONAL_SECONDS_SCALE : scale); - case READER: - return ((null == value) ? 0 : DataTypes.NTEXT_MAX_CHARS); case CLOB: return ((null == value) ? 0 : (DataTypes.NTEXT_MAX_CHARS * 2)); case NCLOB: + case READER: return ((null == value) ? 0 : DataTypes.NTEXT_MAX_CHARS); } return 0; @@ -928,10 +993,9 @@ static synchronized boolean checkIfNeedNewAccessToken(SQLServerConnection connec return false; } - // if driver is for JDBC 42 and jvm version is 8 or higher, then always return as SQLServerPreparedStatement42, - // otherwise return SQLServerPreparedStatement - static boolean use42Wrapper() { + static final boolean use42Wrapper; + static { boolean supportJDBC42 = true; try { DriverJDBCVersion.checkSupportsJDBC42(); @@ -942,7 +1006,13 @@ static boolean use42Wrapper() { double jvmVersion = Double.parseDouble(Util.SYSTEM_SPEC_VERSION); - return supportJDBC42 && (1.8 <= jvmVersion); + use42Wrapper = supportJDBC42 && (1.8 <= jvmVersion); + } + + // if driver is for JDBC 42 and jvm version is 8 or higher, then always return as SQLServerPreparedStatement42, + // otherwise return SQLServerPreparedStatement + static boolean use42Wrapper() { + return use42Wrapper; } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dns/DNSKerberosLocator.java b/src/main/java/com/microsoft/sqlserver/jdbc/dns/DNSKerberosLocator.java new file mode 100644 index 000000000..77bb67b0f --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dns/DNSKerberosLocator.java @@ -0,0 +1,44 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.dns; + +import java.util.Set; + +import javax.naming.NameNotFoundException; +import javax.naming.NamingException; + +public final class DNSKerberosLocator { + + private DNSKerberosLocator() { + } + + /** + * Tells whether a realm is valid. + * + * @param realmName + * the realm to test + * @return true if realm is valid, false otherwise + * @throws NamingException + * if DNS failed, so realm existence cannot be determined + */ + public static boolean isRealmValid(String realmName) throws NamingException { + if (realmName == null || realmName.length() < 2) { + return false; + } + if (realmName.startsWith(".")) { + realmName = realmName.substring(1); + } + try { + Set records = DNSUtilities.findSrvRecords("_kerberos._udp." + realmName); + return !records.isEmpty(); + } + catch (NameNotFoundException wrongDomainException) { + return false; + } + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dns/DNSRecordSRV.java b/src/main/java/com/microsoft/sqlserver/jdbc/dns/DNSRecordSRV.java new file mode 100644 index 000000000..5ced8ca3a --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dns/DNSRecordSRV.java @@ -0,0 +1,170 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.dns; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Describe an DNS SRV Record. + */ +public class DNSRecordSRV implements Comparable { + + private static final Pattern PATTERN = Pattern.compile("^([0-9]+) ([0-9]+) ([0-9]+) (.+)$"); + + private final int priority; + + /** + * Parse a DNS SRC Record from a DNS String record. + * + * @param record + * the record to parse + * @return a not null DNS Record + * @throws IllegalArgumentException + * if record is not correct and cannot be parsed + */ + public static DNSRecordSRV parseFromDNSRecord(String record) throws IllegalArgumentException { + Matcher m = PATTERN.matcher(record); + if (!m.matches()) { + throw new IllegalArgumentException("record '" + record + "' cannot be matched as a valid DNS SRV Record"); + } + try { + int priority = Integer.parseInt(m.group(1)); + int weight = Integer.parseInt(m.group(2)); + int port = Integer.parseInt(m.group(3)); + String serverName = m.group(4); + // Avoid issues with Kerberos SPN when fully qualified records ends with '.' + if (serverName.endsWith(".")) { + serverName = serverName.substring(0, serverName.length() - 1); + } + return new DNSRecordSRV(priority, weight, port, serverName); + } + catch (IllegalArgumentException err) { + throw err; + } + catch (Exception err) { + throw new IllegalArgumentException("Failed to parse DNS SRV record '" + record + "'", err); + } + } + + @Override + public String toString() { + return String.format("DNS.SRV[pri=%d w=%d port=%d h='%s']", priority, weight, port, serverName); + } + + /** + * Constructor. + * + * @param priority + * is lowest + * @param weight + * 1 at minimum + * @param port + * the port of service + * @param serverName + * the host + * @throws IllegalArgumentException + * if priority {@literal <} 0 or weight {@literal <=} 1 + */ + public DNSRecordSRV(int priority, + int weight, + int port, + String serverName) throws IllegalArgumentException { + if (priority < 0) { + throw new IllegalArgumentException("priority must be >= 0, but was: " + priority); + } + this.priority = priority; + if (weight < 0) { + // Weight == 0 is OK to disable load balancing, but not below + throw new IllegalArgumentException("weight must be >= 0, but was: " + weight); + } + this.weight = weight; + if (port < 0 || port > 65535) { + throw new IllegalArgumentException("port must be between 0 and 65535, but was: " + port); + } + this.port = port; + if (serverName == null || serverName.trim().isEmpty()) { + throw new IllegalArgumentException("hostname is not supposed to be null or empty in a SRV Record"); + } + this.serverName = serverName; + } + + private final int weight; + private final int port; + private final String serverName; + + @Override + public int hashCode() { + return serverName.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof DNSRecordSRV)) { + return false; + } + + DNSRecordSRV r = (DNSRecordSRV) other; + return port == r.port && weight == r.weight && priority == r.priority && serverName.equals(r.serverName); + } + + @Override + public int compareTo(DNSRecordSRV o) { + if (o == null) { + return 1; + } + int p = Integer.compare(priority, o.priority); + if (p != 0) { + return p; + } + p = Integer.compare(weight, o.weight); + if (p != 0) { + return p; + } + p = Integer.compare(port, o.port); + if (p != 0) { + return p; + } + return serverName.compareTo(o.serverName); + } + + /** + * Get the priority of DNS SRV record. + * @return a positive priority, where lowest values have to be considered first. + */ + public int getPriority() { + return priority; + } + + /** + * Get the weight of DNS record from 0 to 65535. + * @return The weight, higher value means higher probability of selecting the given record for a given priority. + */ + public int getWeight() { + return weight; + } + + /** + * IP port of record. + * @return a value from 1 to 65535. + */ + public int getPort() { + return port; + } + + /** + * The DNS server name. + * @return a not null server name. + */ + public String getServerName() { + return serverName; + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dns/DNSUtilities.java b/src/main/java/com/microsoft/sqlserver/jdbc/dns/DNSUtilities.java new file mode 100644 index 000000000..483ad3635 --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dns/DNSUtilities.java @@ -0,0 +1,68 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.dns; + +import java.util.Hashtable; +import java.util.Set; +import java.util.TreeSet; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; + +public class DNSUtilities { + + private final static Logger LOG = Logger.getLogger(DNSUtilities.class.getName()); + + private static final Level DNS_ERR_LOG_LEVEL = Level.FINE; + + /** + * Find all SRV Record using DNS. + * + * @param dnsSrvRecordToFind + * the DNS record, for instance: _ldap._tcp.dc._msdcs.DOMAIN.COM to find all LDAP servers in DOMAIN.COM + * @return the collection of records with facilities to find the best candidate + * @throws NamingException + * if DNS is not available + */ + public static Set findSrvRecords(final String dnsSrvRecordToFind) throws NamingException { + Hashtable env = new Hashtable<>(); + env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); + env.put("java.naming.provider.url", "dns:"); + DirContext ctx = new InitialDirContext(env); + Attributes attrs = ctx.getAttributes(dnsSrvRecordToFind, new String[] {"SRV"}); + NamingEnumeration allServers = attrs.getAll(); + TreeSet records = new TreeSet<>(); + while (allServers.hasMoreElements()) { + Attribute a = allServers.nextElement(); + NamingEnumeration srvRecord = a.getAll(); + while (srvRecord.hasMore()) { + final String record = String.valueOf(srvRecord.nextElement()); + try { + DNSRecordSRV rec = DNSRecordSRV.parseFromDNSRecord(record); + if (rec != null) { + records.add(rec); + } + } + catch (IllegalArgumentException errorParsingRecord) { + if (LOG.isLoggable(DNS_ERR_LOG_LEVEL)) { + LOG.log(DNS_ERR_LOG_LEVEL, String.format("Failed to parse SRV DNS Record: '%s'", record), errorParsingRecord); + } + } + } + srvRecord.close(); + } + allServers.close(); + return records; + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java index cee390b2e..e0ea30378 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java @@ -139,6 +139,9 @@ abstract void execute(DTV dtv, abstract void execute(DTV dtv, TVP tvpValue) throws SQLServerException; + + abstract void execute(DTV dtv, + SqlVariant sqlVariantValue) throws SQLServerException; } /** @@ -290,6 +293,10 @@ Object getSetterValue() { return impl.getSetterValue(); } + SqlVariant getInternalVariant() { + return impl.getInternalVariant(); + } + /** * Called by DTV implementation instances to change to a different DTV implementation. */ @@ -656,7 +663,7 @@ private void sendTemporal(DTV dtv, throw new SQLServerException(SQLServerException.getErrString("R_zoneOffsetError"), null, // SQLState is null as this error // is generated in the driver 0, // Use 0 instead of DriverError.NOT_SET to use the correct constructor - null); + e); } subSecondNanos = offsetTimeValue.getNano(); @@ -688,7 +695,7 @@ private void sendTemporal(DTV dtv, throw new SQLServerException(SQLServerException.getErrString("R_zoneOffsetError"), null, // SQLState is null as this error // is generated in the driver 0, // Use 0 instead of DriverError.NOT_SET to use the correct constructor - null); + e); } subSecondNanos = offsetDateTimeValue.getNano(); @@ -1070,7 +1077,7 @@ void execute(DTV dtv, * simply the assignment in the Java runtime). So the only way found is to convert the float to a string and init the double with that * string */ - Double doubleValue = (null == floatValue) ? null : new Double(floatValue.floatValue()); + Double doubleValue = (null == floatValue) ? null : (double) floatValue; tdsWriter.writeRPCDouble(name, doubleValue, isOutParam); } } @@ -1206,7 +1213,7 @@ void writeEncryptData(DTV dtv, // the default length for decimal value } - tdsWriter.writeByte((byte) ((0 != outScale) ? outScale : 0)); // send scale + tdsWriter.writeByte((byte) (outScale)); // send scale } else { tdsWriter.writeByte((byte) 0x11); // maximum length @@ -1436,6 +1443,18 @@ void execute(DTV dtv, // Write the reader value as a stream of Unicode characters tdsWriter.writeRPCReaderUnicode(name, readerValue, dtv.getStreamSetterArgs().getLength(), isOutParam, collation); } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.DTVExecuteOp#execute(com.microsoft.sqlserver.jdbc.DTV, microsoft.sql.SqlVariant) + */ + @Override + void execute(DTV dtv, + SqlVariant sqlVariantValue) throws SQLServerException { + tdsWriter.writeRPCSqlVariant(name, sqlVariantValue, isOutParam); + + } } /** @@ -1523,10 +1542,7 @@ final void executeOp(DTVExecuteOp op) throws SQLServerException { case LONGVARCHAR: case CLOB: case GUID: - if (null != cryptoMeta) - op.execute(this, (byte[]) null); - else - op.execute(this, (byte[]) null); + op.execute(this, (byte[]) null); break; case TINYINT: @@ -1580,6 +1596,10 @@ final void executeOp(DTVExecuteOp op) throws SQLServerException { case STRUCT: unsupportedConversion = true; break; + + case SQL_VARIANT: + op.execute(this, (SqlVariant) null); + break; case UNKNOWN: default: @@ -1597,10 +1617,19 @@ final void executeOp(DTVExecuteOp op) throws SQLServerException { switch (javaType) { case STRING: if (JDBCType.GUID == jdbcType) { - if (value instanceof String) - value = UUID.fromString((String) value); - byte[] bArray = Util.asGuidByteArray((UUID) value); - op.execute(this, bArray); + if (null != cryptoMeta) { + if (value instanceof String) { + value = UUID.fromString((String) value); + } + byte[] bArray = Util.asGuidByteArray((UUID) value); + op.execute(this, bArray); + } + else { + op.execute(this, String.valueOf(value)); + } + } + else if (JDBCType.SQL_VARIANT == jdbcType) { + op.execute(this, String.valueOf(value)); } else { if (null != cryptoMeta) { @@ -1959,6 +1988,8 @@ abstract void skipValue(TypeInfo typeInfo, boolean isDiscard) throws SQLServerException; abstract void initFromCompressedNull(); + + abstract SqlVariant getInternalVariant(); } /** @@ -1972,7 +2003,8 @@ final class AppDTVImpl extends DTVImpl { private Calendar cal; private Integer scale; private boolean forceEncrypt; - + private SqlVariant internalVariant; + final void skipValue(TypeInfo typeInfo, TDSReader tdsReader, boolean isDiscard) throws SQLServerException { @@ -2175,8 +2207,8 @@ void execute(DTV dtv, // Rescale the value if necessary if (null != bigDecimalValue) { Integer inScale = dtv.getScale(); - if (null != inScale && inScale.intValue() != bigDecimalValue.scale()) - bigDecimalValue = bigDecimalValue.setScale(inScale.intValue(), BigDecimal.ROUND_DOWN); + if (null != inScale && inScale != bigDecimalValue.scale()) + bigDecimalValue = bigDecimalValue.setScale(inScale, BigDecimal.ROUND_DOWN); } dtv.setValue(bigDecimalValue, JavaType.BIGDECIMAL); @@ -2230,7 +2262,7 @@ void execute(DTV dtv, readerValue = new InputStreamReader(inputStreamValue, "US-ASCII"); } catch (UnsupportedEncodingException ex) { - throw new SQLServerException(null, ex.getMessage(), null, 0, true); + throw new SQLServerException(ex.getMessage(), null, 0, ex); } dtv.setValue(readerValue, JavaType.READER); @@ -2264,7 +2296,7 @@ void execute(DTV dtv, // the actual stream length did not match then cancel the request. if (DataTypes.UNKNOWN_STREAM_LENGTH != readerLength && stringValue.length() != readerLength) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_mismatchedStreamLength")); - Object[] msgArgs = {Long.valueOf(readerLength), Integer.valueOf(stringValue.length())}; + Object[] msgArgs = {readerLength, stringValue.length()}; SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), "", true); } @@ -2284,6 +2316,17 @@ else if (null != collation execute(dtv, streamValue); } } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.DTVExecuteOp#execute(com.microsoft.sqlserver.jdbc.DTV, microsoft.sql.SqlVariant) + */ + @Override + void execute(DTV dtv, + SqlVariant SqlVariantValue) throws SQLServerException { + } + } void setValue(DTV dtv, @@ -2374,6 +2417,26 @@ Object getValue(DTV dtv, Object getSetterValue() { return value; } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.DTVImpl#getInternalVariant() + */ + @Override + SqlVariant getInternalVariant() { + return this.internalVariant; + } + + /** + * Sets the internal datatype of variant type + * + * @param type + * sql_variant internal type + */ + void setInternalVariant(SqlVariant type) { + this.internalVariant = type; + } } /** @@ -2401,10 +2464,18 @@ final class TypeInfo { SSType getSSType() { return ssType; } + + void setSSType(SSType ssType) { + this.ssType = ssType; + } SSLenType getSSLenType() { return ssLenType; } + + void setSSLenType(SSLenType ssLenType){ + this.ssLenType = ssLenType; + } String getSSTypeName() { return (SSType.UDT == ssType) ? udtTypeName : ssType.toString(); @@ -2413,14 +2484,25 @@ String getSSTypeName() { int getMaxLength() { return maxLength; } - + + void setMaxLength(int maxLength) { + this.maxLength = maxLength; + } int getPrecision() { return precision; } + + void setPrecision(int precision) { + this.precision = precision; + } int getDisplaySize() { return displaySize; } + + void setDisplaySize(int displaySize){ + this.displaySize = displaySize; + } int getScale() { return scale; @@ -2437,6 +2519,10 @@ void setSQLCollation(SQLCollation collation) { Charset getCharset() { return charset; } + + void setCharset(Charset charset){ + this.charset = charset; + } boolean isNullable() { return 0x0001 == (flags & 0x0001); @@ -2480,6 +2566,10 @@ short getFlagsAsShort() { void setFlags(Short flags) { this.flags = flags; } + + void setScale(int scale){ + this.scale = scale; + } //TypeInfo Builder enum defines a set of builders used to construct TypeInfo instances //for the various data types. Each builder builds a TypeInfo instance using a builder Strategy. @@ -3014,37 +3104,10 @@ public void apply(TypeInfo typeInfo, */ public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException { - try { - SQLServerException.makeFromDriverError(tdsReader.getConnection(), null, SQLServerException.getErrString("R_variantNotSupported"), - null, false); - } - finally { - /* - * As the driver doesn't know how to process or skip the VARIANT type in TDS token stream, we send an interrupt Signal to server, - * and skips all the data received while waiting for the interrupt acknowledgment. - */ - int remainingPackets = 0; - - // Skip the current buffered packet - remainingPackets = tdsReader.availableCurrentPacket(); - tdsReader.skip(remainingPackets); - - // send interrupt to server - tdsReader.getCommand().interrupt(SQLServerException.getErrString("R_variantNotSupported")); - - /* - * Skip all data only if waiting for attention ack and until interrupt acknowledgment is received. - * - * Interrupt acknowledgment is a DONE token with the DONE_ATTN(0x0020) bit set. - */ - while (tdsReader.getCommand().attentionPending() && (TDS.TDS_DONE != tdsReader.peekTokenType()) - && (0 != (tdsReader.peekStatusFlag() & 0x0020))) { - remainingPackets = tdsReader.availableCurrentPacket(); - tdsReader.skip(remainingPackets); - } - tdsReader.getCommand().close(); + typeInfo.ssLenType = SSLenType.LONGLENTYPE; //sql_variant type should be LONGLENTYPE length. + typeInfo.maxLength = tdsReader.readInt(); + typeInfo.ssType = SSType.SQL_VARIANT; } - } }); private final TDSType tdsType; @@ -3271,7 +3334,7 @@ boolean supportsFastAsciiConversion() { } } - private static final Map builderMap = new EnumMap(TDSType.class); + private static final Map builderMap = new EnumMap<>(TDSType.class); static { for (Builder builder : Builder.values()) @@ -3315,7 +3378,7 @@ final class ServerDTVImpl extends DTVImpl { private int valueLength; private TDSReaderMark valueMark; private boolean isNull; - + private SqlVariant internalVariant; /** * Sets the value of the DTV to an app-specified Java type. * @@ -3497,9 +3560,11 @@ private void getValuePrep(TypeInfo typeInfo, valueLength = tdsReader.readInt(); } } - else { + + else if (SSType.SQL_VARIANT == typeInfo.getSSType()) { valueLength = tdsReader.readInt(); isNull = (0 == valueLength); + typeInfo.setSSType(SSType.SQL_VARIANT); } break; } @@ -3540,7 +3605,7 @@ Object denormalizedValue(byte[] decryptedValue, (null == baseTypeInfo.getCharset()) ? con.getDatabaseCollation().getCharset() : baseTypeInfo.getCharset()); if ((SSType.CHAR == baseSSType) || (SSType.NCHAR == baseSSType)) { // Right pad the string for CHAR types. - StringBuffer sb = new StringBuffer(strVal); + StringBuilder sb = new StringBuilder(strVal); int padLength = baseTypeInfo.getPrecision() - strVal.length(); for (int i = 0; i < padLength; i++) { sb.append(' '); @@ -3556,7 +3621,7 @@ Object denormalizedValue(byte[] decryptedValue, catch (UnsupportedEncodingException e) { // Important: we should not pass the exception here as it displays the data. MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unsupportedEncoding")); - throw new SQLServerException(form.format(new Object[] {baseTypeInfo.getCharset()}), null, 0, null); + throw new SQLServerException(form.format(new Object[] {baseTypeInfo.getCharset()}), null, 0, e); } } @@ -3738,7 +3803,7 @@ Object getValue(DTV dtv, TDSReader tdsReader) throws SQLServerException { SQLServerConnection con = tdsReader.getConnection(); Object convertedValue = null; - byte[] decryptedValue = null; + byte[] decryptedValue; boolean encrypted = false; SSType baseSSType = typeInfo.getSSType(); @@ -3765,14 +3830,12 @@ Object getValue(DTV dtv, // or valueMark should be null and isNull should be set to true(NBCROW case) assert ((valueMark != null) || (valueMark == null && isNull)); - boolean isAdaptive = false; - if (null != streamGetterArgs) { if (!streamGetterArgs.streamType.convertsFrom(typeInfo)) DataTypes.throwConversionError(typeInfo.getSSType().toString(), streamGetterArgs.streamType.toString()); } else { - if (!baseSSType.convertsTo(jdbcType)) { + if (!baseSSType.convertsTo(jdbcType) && !isNull) { // if the baseSSType is Character or NCharacter and jdbcType is Longvarbinary, // does not throw type conversion error, which allows getObject() on Long Character types. if (encrypted) { @@ -3939,7 +4002,26 @@ Object getValue(DTV dtv, case GUID: convertedValue = tdsReader.readGUID(valueLength, jdbcType, streamGetterArgs.streamType); break; + + case SQL_VARIANT: + /** + * SQL_Variant has the following structure: + * 1- basetype: the underlying type + * 2- probByte: holds count of property bytes expected for a sql_variant structure + * 3- properties: For example VARCHAR type has 5 byte collation and 2 byte max length + * 4- dataValue: the data value + */ + int baseType = tdsReader.readUnsignedByte(); + int cbPropsActual = tdsReader.readUnsignedByte(); + // don't create new one, if we have already created an internalVariant object. For example, in bulkcopy + // when we are reading time column, we update the same internalvarianttype's JDBC to be timestamp + if (null == internalVariant) { + internalVariant = new SqlVariant(baseType); + } + convertedValue = readSqlVariant(baseType, cbPropsActual, valueLength, tdsReader, baseSSType, typeInfo, jdbcType, streamGetterArgs, + cal); + break; // Unknown SSType should have already been rejected by TypeInfo.setFromTDS() default: assert false : "Unexpected SSType " + typeInfo.getSSType(); @@ -3951,6 +4033,265 @@ Object getValue(DTV dtv, assert isNull || null != convertedValue; return convertedValue; } + + SqlVariant getInternalVariant() { + return internalVariant; + } + + /** + * Read the value inside sqlVariant. The reading differs based on what the internal baseType is. + * + * @return sql_variant value + * @since 6.3.0 + * @throws SQLServerException + */ + private Object readSqlVariant(int intbaseType, + int cbPropsActual, + int valueLength, + TDSReader tdsReader, + SSType baseSSType, + TypeInfo typeInfo, + JDBCType jdbcType, + InputStreamGetterArgs streamGetterArgs, + Calendar cal) throws SQLServerException { + Object convertedValue = null; + int lengthConsumed = 2 + cbPropsActual; // We have already read 2bytes for baseType earlier. + int expectedValueLength = valueLength - lengthConsumed; + SQLCollation collation = null; + int precision; + int scale; + int maxLength; + TDSType baseType = TDSType.valueOf(intbaseType); + switch (baseType) { + case INT8: + jdbcType = JDBCType.BIGINT; + convertedValue = DDC.convertLongToObject(tdsReader.readLong(), jdbcType, baseSSType, streamGetterArgs.streamType); + break; + + case INT4: + jdbcType = JDBCType.INTEGER; + convertedValue = DDC.convertIntegerToObject(tdsReader.readInt(), valueLength, jdbcType, streamGetterArgs.streamType); + break; + + case INT2: + jdbcType = JDBCType.SMALLINT; + convertedValue = DDC.convertIntegerToObject(tdsReader.readShort(), valueLength, jdbcType, streamGetterArgs.streamType); + break; + + case INT1: + jdbcType = JDBCType.TINYINT; + convertedValue = DDC.convertIntegerToObject(tdsReader.readUnsignedByte(), valueLength, jdbcType, streamGetterArgs.streamType); + break; + + case DECIMALN: + case NUMERICN: + if (TDSType.DECIMALN == baseType) + jdbcType = JDBCType.DECIMAL; + else if (TDSType.NUMERICN == baseType) + jdbcType = JDBCType.NUMERIC; + if (cbPropsActual != sqlVariantProbBytes.DECIMALN.getIntValue()) { // Numeric and decimal have the same probbytes value + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidProbbytes")); + throw new SQLServerException(form.format(new Object[] {baseType}), null, 0, null); + } + jdbcType = JDBCType.DECIMAL; + precision = tdsReader.readUnsignedByte(); + scale = tdsReader.readUnsignedByte(); + typeInfo.setScale(scale); // typeInfo needs to be updated. typeInfo is usually set when reading columnMetaData, but for sql_variant + // type the actual columnMetaData is is set when reading the data rows. + internalVariant.setPrecision(precision); + internalVariant.setScale(scale); + convertedValue = tdsReader.readDecimal(expectedValueLength, typeInfo, jdbcType, streamGetterArgs.streamType); + break; + + case FLOAT4: + jdbcType = JDBCType.REAL; + convertedValue = tdsReader.readReal(expectedValueLength, jdbcType, streamGetterArgs.streamType); + break; + + case FLOAT8: + jdbcType = JDBCType.FLOAT; + convertedValue = tdsReader.readFloat(expectedValueLength, jdbcType, streamGetterArgs.streamType); + break; + + case MONEY4: + jdbcType = JDBCType.SMALLMONEY; + precision = Long.toString(Long.MAX_VALUE).length(); + typeInfo.setPrecision(precision); + scale = 4; + typeInfo.setDisplaySize(("-" + "." + Integer.toString(Integer.MAX_VALUE)).length()); + typeInfo.setScale(scale); + internalVariant.setPrecision(precision); + internalVariant.setScale(scale); + convertedValue = tdsReader.readMoney(expectedValueLength, jdbcType, streamGetterArgs.streamType); + break; + + case MONEY8: + jdbcType = JDBCType.MONEY; + precision = Long.toString(Long.MAX_VALUE).length(); + scale = 4; + typeInfo.setPrecision(precision); + typeInfo.setDisplaySize(("-" + "." + Integer.toString(Integer.MAX_VALUE)).length()); + typeInfo.setScale(scale); + internalVariant.setPrecision(precision); + internalVariant.setScale(scale); + convertedValue = tdsReader.readMoney(expectedValueLength, jdbcType, streamGetterArgs.streamType); + break; + + case BIT1: + case BITN: + jdbcType = JDBCType.BIT; + switch (expectedValueLength) { + case 8: + convertedValue = DDC.convertLongToObject(tdsReader.readLong(), jdbcType, baseSSType, streamGetterArgs.streamType); + break; + + case 4: + convertedValue = DDC.convertIntegerToObject(tdsReader.readInt(), expectedValueLength, jdbcType, streamGetterArgs.streamType); + break; + + case 2: + convertedValue = DDC.convertIntegerToObject(tdsReader.readShort(), expectedValueLength, jdbcType, + streamGetterArgs.streamType); + break; + + case 1: + convertedValue = DDC.convertIntegerToObject(tdsReader.readUnsignedByte(), expectedValueLength, jdbcType, + streamGetterArgs.streamType); + break; + + default: + assert false : "Unexpected valueLength" + expectedValueLength; + break; + } + break; + + case BIGVARCHAR: + case BIGCHAR: + if (cbPropsActual != sqlVariantProbBytes.BIGCHAR.getIntValue()) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidProbbytes")); + throw new SQLServerException(form.format(new Object[] {baseType}), null, 0, null); + } + if (TDSType.BIGVARCHAR == baseType) + jdbcType = JDBCType.VARCHAR; + else if (TDSType.BIGCHAR == baseType) + jdbcType = JDBCType.CHAR; + collation = tdsReader.readCollation(); + typeInfo.setSQLCollation(collation); + maxLength = tdsReader.readUnsignedShort(); + if (maxLength > DataTypes.SHORT_VARTYPE_MAX_BYTES) + tdsReader.throwInvalidTDS(); + typeInfo.setDisplaySize(maxLength); + typeInfo.setPrecision(maxLength); + internalVariant.setPrecision(maxLength); + internalVariant.setCollation(collation); + typeInfo.setCharset(collation.getCharset()); + convertedValue = DDC.convertStreamToObject(new SimpleInputStream(tdsReader, expectedValueLength, streamGetterArgs, this), typeInfo, + jdbcType, streamGetterArgs); + break; + + case NCHAR: + case NVARCHAR: + if (cbPropsActual != sqlVariantProbBytes.NCHAR.getIntValue()) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidProbbytes")); + throw new SQLServerException(form.format(new Object[] {baseType}), null, 0, null); + } + if (TDSType.NCHAR == baseType) + jdbcType = JDBCType.NCHAR; + else if (TDSType.NVARCHAR == baseType) + jdbcType = JDBCType.NVARCHAR; + collation = tdsReader.readCollation(); + typeInfo.setSQLCollation(collation); + maxLength = tdsReader.readUnsignedShort(); + if (maxLength > DataTypes.SHORT_VARTYPE_MAX_BYTES || 0 != maxLength % 2) + tdsReader.throwInvalidTDS(); + typeInfo.setDisplaySize(maxLength / 2); + typeInfo.setPrecision(maxLength / 2); + internalVariant.setPrecision(maxLength / 2); + internalVariant.setCollation(collation); + typeInfo.setCharset(Encoding.UNICODE.charset()); + convertedValue = DDC.convertStreamToObject(new SimpleInputStream(tdsReader, expectedValueLength, streamGetterArgs, this), typeInfo, + jdbcType, streamGetterArgs); + break; + + case DATETIME8: + jdbcType = JDBCType.DATETIME; + convertedValue = tdsReader.readDateTime(expectedValueLength, cal, jdbcType, streamGetterArgs.streamType); + break; + + case DATETIME4: + jdbcType = JDBCType.SMALLDATETIME; + convertedValue = tdsReader.readDateTime(expectedValueLength, cal, jdbcType, streamGetterArgs.streamType); + break; + + case DATEN: + jdbcType = JDBCType.DATE; + convertedValue = tdsReader.readDate(expectedValueLength, cal, jdbcType); + break; + + case TIMEN: + if (cbPropsActual != sqlVariantProbBytes.TIMEN.getIntValue()) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidProbbytes")); + throw new SQLServerException(form.format(new Object[] {baseType}), null, 0, null); + } + if (internalVariant.isBaseTypeTimeValue()) { + jdbcType = JDBCType.TIMESTAMP; + } + scale = tdsReader.readUnsignedByte(); + typeInfo.setScale(scale); + internalVariant.setScale(scale); + convertedValue = tdsReader.readTime(expectedValueLength, typeInfo, cal, jdbcType); + break; + + case DATETIME2N: + if (cbPropsActual != sqlVariantProbBytes.DATETIME2N.getIntValue()) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidProbbytes")); + throw new SQLServerException(form.format(new Object[] {baseType}), null, 0, null); + } + jdbcType = JDBCType.TIMESTAMP; + scale = tdsReader.readUnsignedByte(); + typeInfo.setScale(scale); + internalVariant.setScale(scale); + convertedValue = tdsReader.readDateTime2(expectedValueLength, typeInfo, cal, jdbcType); + break; + + case BIGBINARY: // e.g binary20, binary 512, binary 8000 -> reads as bigbinary + case BIGVARBINARY: + if (cbPropsActual != sqlVariantProbBytes.BIGBINARY.getIntValue()) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidProbbytes")); + throw new SQLServerException(form.format(new Object[] {baseType}), null, 0, null); + } + if (TDSType.BIGBINARY == baseType) + jdbcType = JDBCType.BINARY;// LONGVARCHAR; + else if (TDSType.BIGVARBINARY == baseType) + jdbcType = JDBCType.VARBINARY; + maxLength = tdsReader.readUnsignedShort(); + internalVariant.setMaxLength(maxLength); + if (maxLength > DataTypes.SHORT_VARTYPE_MAX_BYTES) + tdsReader.throwInvalidTDS(); + typeInfo.setDisplaySize(2 * maxLength); + typeInfo.setPrecision(maxLength); + convertedValue = DDC.convertStreamToObject(new SimpleInputStream(tdsReader, expectedValueLength, streamGetterArgs, this), typeInfo, + jdbcType, streamGetterArgs); + break; + + case GUID: + jdbcType = JDBCType.GUID; + internalVariant.setBaseType(intbaseType); + internalVariant.setBaseJDBCType(jdbcType); + typeInfo.setDisplaySize("NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN".length()); + lengthConsumed = 2 + cbPropsActual; + convertedValue = tdsReader.readGUID(expectedValueLength, jdbcType, streamGetterArgs.streamType); + break; + + // Unsupported TdsType should throw error message + default: { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidDataTypeSupportForSQLVariant")); + throw new SQLServerException(form.format(new Object[] {baseType}), null, 0, null); + } + + } + return convertedValue; + } Object getSetterValue() { // This function is never called, but must be implemented; it's abstract in DTVImpl. diff --git a/src/main/java/microsoft/sql/DateTimeOffset.java b/src/main/java/microsoft/sql/DateTimeOffset.java index f905e2737..c91a38086 100644 --- a/src/main/java/microsoft/sql/DateTimeOffset.java +++ b/src/main/java/microsoft/sql/DateTimeOffset.java @@ -18,7 +18,7 @@ * The DateTimeOffset class represents a java.sql.Timestamp, including fractional seconds, plus an integer representing the number of minutes offset * from GMT. */ -public final class DateTimeOffset extends Object implements java.io.Serializable, java.lang.Comparable { +public final class DateTimeOffset implements java.io.Serializable, java.lang.Comparable { private static final long serialVersionUID = 541973748553014280L; private final long utcMillis; diff --git a/src/main/java/microsoft/sql/Types.java b/src/main/java/microsoft/sql/Types.java index 0068fda08..9f510c2ec 100644 --- a/src/main/java/microsoft/sql/Types.java +++ b/src/main/java/microsoft/sql/Types.java @@ -13,43 +13,48 @@ * * This class is never instantiated. */ -public final class Types extends Object { +public final class Types { private Types() { // not reached } - /* + /** * The constant in the Java programming language, sometimes referred to as a type code, that identifies the Microsoft SQL type DATETIMEOFFSET. */ public static final int DATETIMEOFFSET = -155; - /* + /** * The constant in the Java programming language, sometimes referred to as a type code, that identifies the Microsoft SQL type STRUCTURED. */ public static final int STRUCTURED = -153; - /* + /** * The constant in the Java programming language, sometimes referred to as a type code, that identifies the Microsoft SQL type DATETIME. */ public static final int DATETIME = -151; - /* + /** * The constant in the Java programming language, sometimes referred to as a type code, that identifies the Microsoft SQL type SMALLDATETIME. */ public static final int SMALLDATETIME = -150; - /* + /** * The constant in the Java programming language, sometimes referred to as a type code, that identifies the Microsoft SQL type MONEY. */ public static final int MONEY = -148; - /* + /** * The constant in the Java programming language, sometimes referred to as a type code, that identifies the Microsoft SQL type SMALLMONEY. */ public static final int SMALLMONEY = -146; - /* + /** * The constant in the Java programming language, sometimes referred to as a type code, that identifies the Microsoft SQL type GUID. */ public static final int GUID = -145; + + /** + * The constant in the Java programming language, sometimes referred to as a type code, that identifies the Microsoft SQL type SQL_VARIANT. + */ + public static final int SQL_VARIANT = -156; } diff --git a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/ConcurrentLinkedHashMap.java b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/ConcurrentLinkedHashMap.java new file mode 100644 index 000000000..a52c70e7b --- /dev/null +++ b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/ConcurrentLinkedHashMap.java @@ -0,0 +1,1582 @@ +/* + * Copyright 2010 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package mssql.googlecode.concurrentlinkedhashmap; + +import static mssql.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap.DrainStatus.IDLE; +import static mssql.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap.DrainStatus.PROCESSING; +import static mssql.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap.DrainStatus.REQUIRED; +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableMap; +import static java.util.Collections.unmodifiableSet; + +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractQueue; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * A hash table supporting full concurrency of retrievals, adjustable expected + * concurrency for updates, and a maximum capacity to bound the map by. This + * implementation differs from {@link ConcurrentHashMap} in that it maintains a + * page replacement algorithm that is used to evict an entry when the map has + * exceeded its capacity. Unlike the Java Collections Framework, this + * map does not have a publicly visible constructor and instances are created + * through a {@link Builder}. + *

    + * An entry is evicted from the map when the weighted capacity exceeds + * its maximum weighted capacity threshold. A {@link EntryWeigher} + * determines how many units of capacity that an entry consumes. The default + * weigher assigns each value a weight of 1 to bound the map by the + * total number of key-value pairs. A map that holds collections may choose to + * weigh values by the number of elements in the collection and bound the map + * by the total number of elements that it contains. A change to a value that + * modifies its weight requires that an update operation is performed on the + * map. + *

    + * An {@link EvictionListener} may be supplied for notification when an entry + * is evicted from the map. This listener is invoked on a caller's thread and + * will not block other threads from operating on the map. An implementation + * should be aware that the caller's thread will not expect long execution + * times or failures as a side effect of the listener being notified. Execution + * safety and a fast turn around time can be achieved by performing the + * operation asynchronously, such as by submitting a task to an + * {@link java.util.concurrent.ExecutorService}. + *

    + * The concurrency level determines the number of threads that can + * concurrently modify the table. Using a significantly higher or lower value + * than needed can waste space or lead to thread contention, but an estimate + * within an order of magnitude of the ideal value does not usually have a + * noticeable impact. Because placement in hash tables is essentially random, + * the actual concurrency will vary. + *

    + * This class and its views and iterators implement all of the + * optional methods of the {@link Map} and {@link Iterator} + * interfaces. + *

    + * Like {@link java.util.Hashtable} but unlike {@link HashMap}, this class + * does not allow null to be used as a key or value. Unlike + * {@link java.util.LinkedHashMap}, this class does not provide + * predictable iteration order. A snapshot of the keys and entries may be + * obtained in ascending and descending order of retention. + * + * @author ben.manes@gmail.com (Ben Manes) + * @param the type of keys maintained by this map + * @param the type of mapped values + * @see + * http://code.google.com/p/concurrentlinkedhashmap/ + */ +public final class ConcurrentLinkedHashMap extends AbstractMap + implements ConcurrentMap, Serializable { + + /* + * This class performs a best-effort bounding of a ConcurrentHashMap using a + * page-replacement algorithm to determine which entries to evict when the + * capacity is exceeded. + * + * The page replacement algorithm's data structures are kept eventually + * consistent with the map. An update to the map and recording of reads may + * not be immediately reflected on the algorithm's data structures. These + * structures are guarded by a lock and operations are applied in batches to + * avoid lock contention. The penalty of applying the batches is spread across + * threads so that the amortized cost is slightly higher than performing just + * the ConcurrentHashMap operation. + * + * A memento of the reads and writes that were performed on the map are + * recorded in buffers. These buffers are drained at the first opportunity + * after a write or when the read buffer exceeds a threshold size. The reads + * are recorded in a lossy buffer, allowing the reordering operations to be + * discarded if the draining process cannot keep up. Due to the concurrent + * nature of the read and write operations a strict policy ordering is not + * possible, but is observably strict when single threaded. + * + * Due to a lack of a strict ordering guarantee, a task can be executed + * out-of-order, such as a removal followed by its addition. The state of the + * entry is encoded within the value's weight. + * + * Alive: The entry is in both the hash-table and the page replacement policy. + * This is represented by a positive weight. + * + * Retired: The entry is not in the hash-table and is pending removal from the + * page replacement policy. This is represented by a negative weight. + * + * Dead: The entry is not in the hash-table and is not in the page replacement + * policy. This is represented by a weight of zero. + * + * The Least Recently Used page replacement algorithm was chosen due to its + * simplicity, high hit rate, and ability to be implemented with O(1) time + * complexity. + */ + + /** The number of CPUs */ + static final int NCPU = Runtime.getRuntime().availableProcessors(); + + /** The maximum weighted capacity of the map. */ + static final long MAXIMUM_CAPACITY = Long.MAX_VALUE - Integer.MAX_VALUE; + + /** The number of read buffers to use. */ + static final int NUMBER_OF_READ_BUFFERS = ceilingNextPowerOfTwo(NCPU); + + /** Mask value for indexing into the read buffers. */ + static final int READ_BUFFERS_MASK = NUMBER_OF_READ_BUFFERS - 1; + + /** The number of pending read operations before attempting to drain. */ + static final int READ_BUFFER_THRESHOLD = 32; + + /** The maximum number of read operations to perform per amortized drain. */ + static final int READ_BUFFER_DRAIN_THRESHOLD = 2 * READ_BUFFER_THRESHOLD; + + /** The maximum number of pending reads per buffer. */ + static final int READ_BUFFER_SIZE = 2 * READ_BUFFER_DRAIN_THRESHOLD; + + /** Mask value for indexing into the read buffer. */ + static final int READ_BUFFER_INDEX_MASK = READ_BUFFER_SIZE - 1; + + /** The maximum number of write operations to perform per amortized drain. */ + static final int WRITE_BUFFER_DRAIN_THRESHOLD = 16; + + /** A queue that discards all entries. */ + static final Queue DISCARDING_QUEUE = new DiscardingQueue(); + + static int ceilingNextPowerOfTwo(int x) { + // From Hacker's Delight, Chapter 3, Harry S. Warren Jr. + return 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(x - 1)); + } + + // The backing data store holding the key-value associations + final ConcurrentMap> data; + final int concurrencyLevel; + + // These fields provide support to bound the map by a maximum capacity + final long[] readBufferReadCount; + final LinkedDeque> evictionDeque; + + final AtomicLong weightedSize; + final AtomicLong capacity; + + final Lock evictionLock; + final Queue writeBuffer; + final AtomicLong[] readBufferWriteCount; + final AtomicLong[] readBufferDrainAtWriteCount; + final AtomicReference>[][] readBuffers; + + final AtomicReference drainStatus; + final EntryWeigher weigher; + + // These fields provide support for notifying a listener. + final Queue> pendingNotifications; + final EvictionListener listener; + + transient Set keySet; + transient Collection values; + transient Set> entrySet; + + /** + * Creates an instance based on the builder's configuration. + */ + @SuppressWarnings({"unchecked", "cast"}) + private ConcurrentLinkedHashMap(Builder builder) { + // The data store and its maximum capacity + concurrencyLevel = builder.concurrencyLevel; + capacity = new AtomicLong(Math.min(builder.capacity, MAXIMUM_CAPACITY)); + data = new ConcurrentHashMap>(builder.initialCapacity, 0.75f, concurrencyLevel); + + // The eviction support + weigher = builder.weigher; + evictionLock = new ReentrantLock(); + weightedSize = new AtomicLong(); + evictionDeque = new LinkedDeque>(); + writeBuffer = new ConcurrentLinkedQueue(); + drainStatus = new AtomicReference(IDLE); + + readBufferReadCount = new long[NUMBER_OF_READ_BUFFERS]; + readBufferWriteCount = new AtomicLong[NUMBER_OF_READ_BUFFERS]; + readBufferDrainAtWriteCount = new AtomicLong[NUMBER_OF_READ_BUFFERS]; + readBuffers = new AtomicReference[NUMBER_OF_READ_BUFFERS][READ_BUFFER_SIZE]; + for (int i = 0; i < NUMBER_OF_READ_BUFFERS; i++) { + readBufferWriteCount[i] = new AtomicLong(); + readBufferDrainAtWriteCount[i] = new AtomicLong(); + readBuffers[i] = new AtomicReference[READ_BUFFER_SIZE]; + for (int j = 0; j < READ_BUFFER_SIZE; j++) { + readBuffers[i][j] = new AtomicReference>(); + } + } + + // The notification queue and listener + listener = builder.listener; + pendingNotifications = (listener == DiscardingListener.INSTANCE) + ? (Queue>) DISCARDING_QUEUE + : new ConcurrentLinkedQueue>(); + } + + /** Ensures that the object is not null. */ + static void checkNotNull(Object o) { + if (o == null) { + throw new NullPointerException(); + } + } + + /** Ensures that the argument expression is true. */ + static void checkArgument(boolean expression) { + if (!expression) { + throw new IllegalArgumentException(); + } + } + + /** Ensures that the state expression is true. */ + static void checkState(boolean expression) { + if (!expression) { + throw new IllegalStateException(); + } + } + + /* ---------------- Eviction Support -------------- */ + + /** + * Retrieves the maximum weighted capacity of the map. + * + * @return the maximum weighted capacity + */ + public long capacity() { + return capacity.get(); + } + + /** + * Sets the maximum weighted capacity of the map and eagerly evicts entries + * until it shrinks to the appropriate size. + * + * @param capacity the maximum weighted capacity of the map + * @throws IllegalArgumentException if the capacity is negative + */ + public void setCapacity(long capacity) { + checkArgument(capacity >= 0); + evictionLock.lock(); + try { + this.capacity.lazySet(Math.min(capacity, MAXIMUM_CAPACITY)); + drainBuffers(); + evict(); + } finally { + evictionLock.unlock(); + } + notifyListener(); + } + + /** Determines whether the map has exceeded its capacity. */ + boolean hasOverflowed() { + return weightedSize.get() > capacity.get(); + } + + /** + * Evicts entries from the map while it exceeds the capacity and appends + * evicted entries to the notification queue for processing. + */ + void evict() { + // Attempts to evict entries from the map if it exceeds the maximum + // capacity. If the eviction fails due to a concurrent removal of the + // victim, that removal may cancel out the addition that triggered this + // eviction. The victim is eagerly unlinked before the removal task so + // that if an eviction is still required then a new victim will be chosen + // for removal. + while (hasOverflowed()) { + final Node node = evictionDeque.poll(); + + // If weighted values are used, then the pending operations will adjust + // the size to reflect the correct weight + if (node == null) { + return; + } + + // Notify the listener only if the entry was evicted + if (data.remove(node.key, node)) { + pendingNotifications.add(node); + } + + makeDead(node); + } + } + + /** + * Performs the post-processing work required after a read. + * + * @param node the entry in the page replacement policy + */ + void afterRead(Node node) { + final int bufferIndex = readBufferIndex(); + final long writeCount = recordRead(bufferIndex, node); + drainOnReadIfNeeded(bufferIndex, writeCount); + notifyListener(); + } + + /** Returns the index to the read buffer to record into. */ + static int readBufferIndex() { + // A buffer is chosen by the thread's id so that tasks are distributed in a + // pseudo evenly manner. This helps avoid hot entries causing contention + // due to other threads trying to append to the same buffer. + return ((int) Thread.currentThread().getId()) & READ_BUFFERS_MASK; + } + + /** + * Records a read in the buffer and return its write count. + * + * @param bufferIndex the index to the chosen read buffer + * @param node the entry in the page replacement policy + * @return the number of writes on the chosen read buffer + */ + long recordRead(int bufferIndex, Node node) { + // The location in the buffer is chosen in a racy fashion as the increment + // is not atomic with the insertion. This means that concurrent reads can + // overlap and overwrite one another, resulting in a lossy buffer. + final AtomicLong counter = readBufferWriteCount[bufferIndex]; + final long writeCount = counter.get(); + counter.lazySet(writeCount + 1); + + final int index = (int) (writeCount & READ_BUFFER_INDEX_MASK); + readBuffers[bufferIndex][index].lazySet(node); + + return writeCount; + } + + /** + * Attempts to drain the buffers if it is determined to be needed when + * post-processing a read. + * + * @param bufferIndex the index to the chosen read buffer + * @param writeCount the number of writes on the chosen read buffer + */ + void drainOnReadIfNeeded(int bufferIndex, long writeCount) { + final long pending = (writeCount - readBufferDrainAtWriteCount[bufferIndex].get()); + final boolean delayable = (pending < READ_BUFFER_THRESHOLD); + final DrainStatus status = drainStatus.get(); + if (status.shouldDrainBuffers(delayable)) { + tryToDrainBuffers(); + } + } + + /** + * Performs the post-processing work required after a write. + * + * @param task the pending operation to be applied + */ + void afterWrite(Runnable task) { + writeBuffer.add(task); + drainStatus.lazySet(REQUIRED); + tryToDrainBuffers(); + notifyListener(); + } + + /** + * Attempts to acquire the eviction lock and apply the pending operations, up + * to the amortized threshold, to the page replacement policy. + */ + void tryToDrainBuffers() { + if (evictionLock.tryLock()) { + try { + drainStatus.lazySet(PROCESSING); + drainBuffers(); + } finally { + drainStatus.compareAndSet(PROCESSING, IDLE); + evictionLock.unlock(); + } + } + } + + /** Drains the read and write buffers up to an amortized threshold. */ + void drainBuffers() { + drainReadBuffers(); + drainWriteBuffer(); + } + + /** Drains the read buffers, each up to an amortized threshold. */ + void drainReadBuffers() { + final int start = (int) Thread.currentThread().getId(); + final int end = start + NUMBER_OF_READ_BUFFERS; + for (int i = start; i < end; i++) { + drainReadBuffer(i & READ_BUFFERS_MASK); + } + } + + /** Drains the read buffer up to an amortized threshold. */ + void drainReadBuffer(int bufferIndex) { + final long writeCount = readBufferWriteCount[bufferIndex].get(); + for (int i = 0; i < READ_BUFFER_DRAIN_THRESHOLD; i++) { + final int index = (int) (readBufferReadCount[bufferIndex] & READ_BUFFER_INDEX_MASK); + final AtomicReference> slot = readBuffers[bufferIndex][index]; + final Node node = slot.get(); + if (node == null) { + break; + } + + slot.lazySet(null); + applyRead(node); + readBufferReadCount[bufferIndex]++; + } + readBufferDrainAtWriteCount[bufferIndex].lazySet(writeCount); + } + + /** Updates the node's location in the page replacement policy. */ + void applyRead(Node node) { + // An entry may be scheduled for reordering despite having been removed. + // This can occur when the entry was concurrently read while a writer was + // removing it. If the entry is no longer linked then it does not need to + // be processed. + if (evictionDeque.contains(node)) { + evictionDeque.moveToBack(node); + } + } + + /** Drains the read buffer up to an amortized threshold. */ + void drainWriteBuffer() { + for (int i = 0; i < WRITE_BUFFER_DRAIN_THRESHOLD; i++) { + final Runnable task = writeBuffer.poll(); + if (task == null) { + break; + } + task.run(); + } + } + + /** + * Attempts to transition the node from the alive state to the + * retired state. + * + * @param node the entry in the page replacement policy + * @param expect the expected weighted value + * @return if successful + */ + boolean tryToRetire(Node node, WeightedValue expect) { + if (expect.isAlive()) { + final WeightedValue retired = new WeightedValue(expect.value, -expect.weight); + return node.compareAndSet(expect, retired); + } + return false; + } + + /** + * Atomically transitions the node from the alive state to the + * retired state, if a valid transition. + * + * @param node the entry in the page replacement policy + */ + void makeRetired(Node node) { + for (;;) { + final WeightedValue current = node.get(); + if (!current.isAlive()) { + return; + } + final WeightedValue retired = new WeightedValue(current.value, -current.weight); + if (node.compareAndSet(current, retired)) { + return; + } + } + } + + /** + * Atomically transitions the node to the dead state and decrements + * the weightedSize. + * + * @param node the entry in the page replacement policy + */ + void makeDead(Node node) { + for (;;) { + WeightedValue current = node.get(); + WeightedValue dead = new WeightedValue(current.value, 0); + if (node.compareAndSet(current, dead)) { + weightedSize.lazySet(weightedSize.get() - Math.abs(current.weight)); + return; + } + } + } + + /** Notifies the listener of entries that were evicted. */ + void notifyListener() { + Node node; + while ((node = pendingNotifications.poll()) != null) { + listener.onEviction(node.key, node.getValue()); + } + } + + /** Adds the node to the page replacement policy. */ + final class AddTask implements Runnable { + final Node node; + final int weight; + + AddTask(Node node, int weight) { + this.weight = weight; + this.node = node; + } + + @Override + public void run() { + weightedSize.lazySet(weightedSize.get() + weight); + + // ignore out-of-order write operations + if (node.get().isAlive()) { + evictionDeque.add(node); + evict(); + } + } + } + + /** Removes a node from the page replacement policy. */ + final class RemovalTask implements Runnable { + final Node node; + + RemovalTask(Node node) { + this.node = node; + } + + @Override + public void run() { + // add may not have been processed yet + evictionDeque.remove(node); + makeDead(node); + } + } + + /** Updates the weighted size and evicts an entry on overflow. */ + final class UpdateTask implements Runnable { + final int weightDifference; + final Node node; + + public UpdateTask(Node node, int weightDifference) { + this.weightDifference = weightDifference; + this.node = node; + } + + @Override + public void run() { + weightedSize.lazySet(weightedSize.get() + weightDifference); + applyRead(node); + evict(); + } + } + + /* ---------------- Concurrent Map Support -------------- */ + + @Override + public boolean isEmpty() { + return data.isEmpty(); + } + + @Override + public int size() { + return data.size(); + } + + /** + * Returns the weighted size of this map. + * + * @return the combined weight of the values in this map + */ + public long weightedSize() { + return Math.max(0, weightedSize.get()); + } + + @Override + public void clear() { + evictionLock.lock(); + try { + // Discard all entries + Node node; + while ((node = evictionDeque.poll()) != null) { + data.remove(node.key, node); + makeDead(node); + } + + // Discard all pending reads + for (AtomicReference>[] buffer : readBuffers) { + for (AtomicReference> slot : buffer) { + slot.lazySet(null); + } + } + + // Apply all pending writes + Runnable task; + while ((task = writeBuffer.poll()) != null) { + task.run(); + } + } finally { + evictionLock.unlock(); + } + } + + @Override + public boolean containsKey(Object key) { + return data.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + checkNotNull(value); + + for (Node node : data.values()) { + if (node.getValue().equals(value)) { + return true; + } + } + return false; + } + + @Override + public V get(Object key) { + final Node node = data.get(key); + if (node == null) { + return null; + } + afterRead(node); + return node.getValue(); + } + + /** + * Returns the value to which the specified key is mapped, or {@code null} + * if this map contains no mapping for the key. This method differs from + * {@link #get(Object)} in that it does not record the operation with the + * page replacement policy. + * + * @param key the key whose associated value is to be returned + * @return the value to which the specified key is mapped, or + * {@code null} if this map contains no mapping for the key + * @throws NullPointerException if the specified key is null + */ + public V getQuietly(Object key) { + final Node node = data.get(key); + return (node == null) ? null : node.getValue(); + } + + @Override + public V put(K key, V value) { + return put(key, value, false); + } + + @Override + public V putIfAbsent(K key, V value) { + return put(key, value, true); + } + + /** + * Adds a node to the list and the data store. If an existing node is found, + * then its value is updated if allowed. + * + * @param key key with which the specified value is to be associated + * @param value value to be associated with the specified key + * @param onlyIfAbsent a write is performed only if the key is not already + * associated with a value + * @return the prior value in the data store or null if no mapping was found + */ + V put(K key, V value, boolean onlyIfAbsent) { + checkNotNull(key); + checkNotNull(value); + + final int weight = weigher.weightOf(key, value); + final WeightedValue weightedValue = new WeightedValue(value, weight); + final Node node = new Node(key, weightedValue); + + for (;;) { + final Node prior = data.putIfAbsent(node.key, node); + if (prior == null) { + afterWrite(new AddTask(node, weight)); + return null; + } else if (onlyIfAbsent) { + afterRead(prior); + return prior.getValue(); + } + for (;;) { + final WeightedValue oldWeightedValue = prior.get(); + if (!oldWeightedValue.isAlive()) { + break; + } + + if (prior.compareAndSet(oldWeightedValue, weightedValue)) { + final int weightedDifference = weight - oldWeightedValue.weight; + if (weightedDifference == 0) { + afterRead(prior); + } else { + afterWrite(new UpdateTask(prior, weightedDifference)); + } + return oldWeightedValue.value; + } + } + } + } + + @Override + public V remove(Object key) { + final Node node = data.remove(key); + if (node == null) { + return null; + } + + makeRetired(node); + afterWrite(new RemovalTask(node)); + return node.getValue(); + } + + @Override + public boolean remove(Object key, Object value) { + final Node node = data.get(key); + if ((node == null) || (value == null)) { + return false; + } + + WeightedValue weightedValue = node.get(); + for (;;) { + if (weightedValue.contains(value)) { + if (tryToRetire(node, weightedValue)) { + if (data.remove(key, node)) { + afterWrite(new RemovalTask(node)); + return true; + } + } else { + weightedValue = node.get(); + if (weightedValue.isAlive()) { + // retry as an intermediate update may have replaced the value with + // an equal instance that has a different reference identity + continue; + } + } + } + return false; + } + } + + @Override + public V replace(K key, V value) { + checkNotNull(key); + checkNotNull(value); + + final int weight = weigher.weightOf(key, value); + final WeightedValue weightedValue = new WeightedValue(value, weight); + + final Node node = data.get(key); + if (node == null) { + return null; + } + for (;;) { + final WeightedValue oldWeightedValue = node.get(); + if (!oldWeightedValue.isAlive()) { + return null; + } + if (node.compareAndSet(oldWeightedValue, weightedValue)) { + final int weightedDifference = weight - oldWeightedValue.weight; + if (weightedDifference == 0) { + afterRead(node); + } else { + afterWrite(new UpdateTask(node, weightedDifference)); + } + return oldWeightedValue.value; + } + } + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + checkNotNull(key); + checkNotNull(oldValue); + checkNotNull(newValue); + + final int weight = weigher.weightOf(key, newValue); + final WeightedValue newWeightedValue = new WeightedValue(newValue, weight); + + final Node node = data.get(key); + if (node == null) { + return false; + } + for (;;) { + final WeightedValue weightedValue = node.get(); + if (!weightedValue.isAlive() || !weightedValue.contains(oldValue)) { + return false; + } + if (node.compareAndSet(weightedValue, newWeightedValue)) { + final int weightedDifference = weight - weightedValue.weight; + if (weightedDifference == 0) { + afterRead(node); + } else { + afterWrite(new UpdateTask(node, weightedDifference)); + } + return true; + } + } + } + + @Override + public Set keySet() { + final Set ks = keySet; + return (ks == null) ? (keySet = new KeySet()) : ks; + } + + /** + * Returns a unmodifiable snapshot {@link Set} view of the keys contained in + * this map. The set's iterator returns the keys whose order of iteration is + * the ascending order in which its entries are considered eligible for + * retention, from the least-likely to be retained to the most-likely. + *

    + * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT + * a constant-time operation. Because of the asynchronous nature of the page + * replacement policy, determining the retention ordering requires a traversal + * of the keys. + * + * @return an ascending snapshot view of the keys in this map + */ + public Set ascendingKeySet() { + return ascendingKeySetWithLimit(Integer.MAX_VALUE); + } + + /** + * Returns an unmodifiable snapshot {@link Set} view of the keys contained in + * this map. The set's iterator returns the keys whose order of iteration is + * the ascending order in which its entries are considered eligible for + * retention, from the least-likely to be retained to the most-likely. + *

    + * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT + * a constant-time operation. Because of the asynchronous nature of the page + * replacement policy, determining the retention ordering requires a traversal + * of the keys. + * + * @param limit the maximum size of the returned set + * @return a ascending snapshot view of the keys in this map + * @throws IllegalArgumentException if the limit is negative + */ + public Set ascendingKeySetWithLimit(int limit) { + return orderedKeySet(true, limit); + } + + /** + * Returns an unmodifiable snapshot {@link Set} view of the keys contained in + * this map. The set's iterator returns the keys whose order of iteration is + * the descending order in which its entries are considered eligible for + * retention, from the most-likely to be retained to the least-likely. + *

    + * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT + * a constant-time operation. Because of the asynchronous nature of the page + * replacement policy, determining the retention ordering requires a traversal + * of the keys. + * + * @return a descending snapshot view of the keys in this map + */ + public Set descendingKeySet() { + return descendingKeySetWithLimit(Integer.MAX_VALUE); + } + + /** + * Returns an unmodifiable snapshot {@link Set} view of the keys contained in + * this map. The set's iterator returns the keys whose order of iteration is + * the descending order in which its entries are considered eligible for + * retention, from the most-likely to be retained to the least-likely. + *

    + * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT + * a constant-time operation. Because of the asynchronous nature of the page + * replacement policy, determining the retention ordering requires a traversal + * of the keys. + * + * @param limit the maximum size of the returned set + * @return a descending snapshot view of the keys in this map + * @throws IllegalArgumentException if the limit is negative + */ + public Set descendingKeySetWithLimit(int limit) { + return orderedKeySet(false, limit); + } + + Set orderedKeySet(boolean ascending, int limit) { + checkArgument(limit >= 0); + evictionLock.lock(); + try { + drainBuffers(); + + final int initialCapacity = (weigher == Weighers.entrySingleton()) + ? Math.min(limit, (int) weightedSize()) + : 16; + final Set keys = new LinkedHashSet(initialCapacity); + final Iterator> iterator = ascending + ? evictionDeque.iterator() + : evictionDeque.descendingIterator(); + while (iterator.hasNext() && (limit > keys.size())) { + keys.add(iterator.next().key); + } + return unmodifiableSet(keys); + } finally { + evictionLock.unlock(); + } + } + + @Override + public Collection values() { + final Collection vs = values; + return (vs == null) ? (values = new Values()) : vs; + } + + @Override + public Set> entrySet() { + final Set> es = entrySet; + return (es == null) ? (entrySet = new EntrySet()) : es; + } + + /** + * Returns an unmodifiable snapshot {@link Map} view of the mappings contained + * in this map. The map's collections return the mappings whose order of + * iteration is the ascending order in which its entries are considered + * eligible for retention, from the least-likely to be retained to the + * most-likely. + *

    + * Beware that obtaining the mappings is NOT a constant-time + * operation. Because of the asynchronous nature of the page replacement + * policy, determining the retention ordering requires a traversal of the + * entries. + * + * @return a ascending snapshot view of this map + */ + public Map ascendingMap() { + return ascendingMapWithLimit(Integer.MAX_VALUE); + } + + /** + * Returns an unmodifiable snapshot {@link Map} view of the mappings contained + * in this map. The map's collections return the mappings whose order of + * iteration is the ascending order in which its entries are considered + * eligible for retention, from the least-likely to be retained to the + * most-likely. + *

    + * Beware that obtaining the mappings is NOT a constant-time + * operation. Because of the asynchronous nature of the page replacement + * policy, determining the retention ordering requires a traversal of the + * entries. + * + * @param limit the maximum size of the returned map + * @return a ascending snapshot view of this map + * @throws IllegalArgumentException if the limit is negative + */ + public Map ascendingMapWithLimit(int limit) { + return orderedMap(true, limit); + } + + /** + * Returns an unmodifiable snapshot {@link Map} view of the mappings contained + * in this map. The map's collections return the mappings whose order of + * iteration is the descending order in which its entries are considered + * eligible for retention, from the most-likely to be retained to the + * least-likely. + *

    + * Beware that obtaining the mappings is NOT a constant-time + * operation. Because of the asynchronous nature of the page replacement + * policy, determining the retention ordering requires a traversal of the + * entries. + * + * @return a descending snapshot view of this map + */ + public Map descendingMap() { + return descendingMapWithLimit(Integer.MAX_VALUE); + } + + /** + * Returns an unmodifiable snapshot {@link Map} view of the mappings contained + * in this map. The map's collections return the mappings whose order of + * iteration is the descending order in which its entries are considered + * eligible for retention, from the most-likely to be retained to the + * least-likely. + *

    + * Beware that obtaining the mappings is NOT a constant-time + * operation. Because of the asynchronous nature of the page replacement + * policy, determining the retention ordering requires a traversal of the + * entries. + * + * @param limit the maximum size of the returned map + * @return a descending snapshot view of this map + * @throws IllegalArgumentException if the limit is negative + */ + public Map descendingMapWithLimit(int limit) { + return orderedMap(false, limit); + } + + Map orderedMap(boolean ascending, int limit) { + checkArgument(limit >= 0); + evictionLock.lock(); + try { + drainBuffers(); + + final int initialCapacity = (weigher == Weighers.entrySingleton()) + ? Math.min(limit, (int) weightedSize()) + : 16; + final Map map = new LinkedHashMap(initialCapacity); + final Iterator> iterator = ascending + ? evictionDeque.iterator() + : evictionDeque.descendingIterator(); + while (iterator.hasNext() && (limit > map.size())) { + Node node = iterator.next(); + map.put(node.key, node.getValue()); + } + return unmodifiableMap(map); + } finally { + evictionLock.unlock(); + } + } + + /** The draining status of the buffers. */ + enum DrainStatus { + + /** A drain is not taking place. */ + IDLE { + @Override boolean shouldDrainBuffers(boolean delayable) { + return !delayable; + } + }, + + /** A drain is required due to a pending write modification. */ + REQUIRED { + @Override boolean shouldDrainBuffers(boolean delayable) { + return true; + } + }, + + /** A drain is in progress. */ + PROCESSING { + @Override boolean shouldDrainBuffers(boolean delayable) { + return false; + } + }; + + /** + * Determines whether the buffers should be drained. + * + * @param delayable if a drain should be delayed until required + * @return if a drain should be attempted + */ + abstract boolean shouldDrainBuffers(boolean delayable); + } + + /** A value, its weight, and the entry's status. */ + static final class WeightedValue { + final int weight; + final V value; + + WeightedValue(V value, int weight) { + this.weight = weight; + this.value = value; + } + + boolean contains(Object o) { + return (o == value) || value.equals(o); + } + + /** + * If the entry is available in the hash-table and page replacement policy. + */ + boolean isAlive() { + return weight > 0; + } + + /** + * If the entry was removed from the hash-table and is awaiting removal from + * the page replacement policy. + */ + boolean isRetired() { + return weight < 0; + } + + /** + * If the entry was removed from the hash-table and the page replacement + * policy. + */ + boolean isDead() { + return weight == 0; + } + } + + /** + * A node contains the key, the weighted value, and the linkage pointers on + * the page-replacement algorithm's data structures. + */ + @SuppressWarnings("serial") + static final class Node extends AtomicReference> + implements Linked> { + final K key; + Node prev; + Node next; + + /** Creates a new, unlinked node. */ + Node(K key, WeightedValue weightedValue) { + super(weightedValue); + this.key = key; + } + + @Override + public Node getPrevious() { + return prev; + } + + @Override + public void setPrevious(Node prev) { + this.prev = prev; + } + + @Override + public Node getNext() { + return next; + } + + @Override + public void setNext(Node next) { + this.next = next; + } + + /** Retrieves the value held by the current WeightedValue. */ + V getValue() { + return get().value; + } + } + + /** An adapter to safely externalize the keys. */ + final class KeySet extends AbstractSet { + final ConcurrentLinkedHashMap map = ConcurrentLinkedHashMap.this; + + @Override + public int size() { + return map.size(); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public Iterator iterator() { + return new KeyIterator(); + } + + @Override + public boolean contains(Object obj) { + return containsKey(obj); + } + + @Override + public boolean remove(Object obj) { + return (map.remove(obj) != null); + } + + @Override + public Object[] toArray() { + return map.data.keySet().toArray(); + } + + @Override + public T[] toArray(T[] array) { + return map.data.keySet().toArray(array); + } + } + + /** An adapter to safely externalize the key iterator. */ + final class KeyIterator implements Iterator { + final Iterator iterator = data.keySet().iterator(); + K current; + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public K next() { + current = iterator.next(); + return current; + } + + @Override + public void remove() { + checkState(current != null); + ConcurrentLinkedHashMap.this.remove(current); + current = null; + } + } + + /** An adapter to safely externalize the values. */ + final class Values extends AbstractCollection { + + @Override + public int size() { + return ConcurrentLinkedHashMap.this.size(); + } + + @Override + public void clear() { + ConcurrentLinkedHashMap.this.clear(); + } + + @Override + public Iterator iterator() { + return new ValueIterator(); + } + + @Override + public boolean contains(Object o) { + return containsValue(o); + } + } + + /** An adapter to safely externalize the value iterator. */ + final class ValueIterator implements Iterator { + final Iterator> iterator = data.values().iterator(); + Node current; + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public V next() { + current = iterator.next(); + return current.getValue(); + } + + @Override + public void remove() { + checkState(current != null); + ConcurrentLinkedHashMap.this.remove(current.key); + current = null; + } + } + + /** An adapter to safely externalize the entries. */ + final class EntrySet extends AbstractSet> { + final ConcurrentLinkedHashMap map = ConcurrentLinkedHashMap.this; + + @Override + public int size() { + return map.size(); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public Iterator> iterator() { + return new EntryIterator(); + } + + @Override + public boolean contains(Object obj) { + if (!(obj instanceof Entry)) { + return false; + } + Entry entry = (Entry) obj; + Node node = map.data.get(entry.getKey()); + return (node != null) && (node.getValue().equals(entry.getValue())); + } + + @Override + public boolean add(Entry entry) { + return (map.putIfAbsent(entry.getKey(), entry.getValue()) == null); + } + + @Override + public boolean remove(Object obj) { + if (!(obj instanceof Entry)) { + return false; + } + Entry entry = (Entry) obj; + return map.remove(entry.getKey(), entry.getValue()); + } + } + + /** An adapter to safely externalize the entry iterator. */ + final class EntryIterator implements Iterator> { + final Iterator> iterator = data.values().iterator(); + Node current; + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Entry next() { + current = iterator.next(); + return new WriteThroughEntry(current); + } + + @Override + public void remove() { + checkState(current != null); + ConcurrentLinkedHashMap.this.remove(current.key); + current = null; + } + } + + /** An entry that allows updates to write through to the map. */ + final class WriteThroughEntry extends SimpleEntry { + static final long serialVersionUID = 1; + + WriteThroughEntry(Node node) { + super(node.key, node.getValue()); + } + + @Override + public V setValue(V value) { + put(getKey(), value); + return super.setValue(value); + } + + Object writeReplace() { + return new SimpleEntry(this); + } + } + + /** A weigher that enforces that the weight falls within a valid range. */ + static final class BoundedEntryWeigher implements EntryWeigher, Serializable { + static final long serialVersionUID = 1; + final EntryWeigher weigher; + + BoundedEntryWeigher(EntryWeigher weigher) { + checkNotNull(weigher); + this.weigher = weigher; + } + + @Override + public int weightOf(K key, V value) { + int weight = weigher.weightOf(key, value); + checkArgument(weight >= 1); + return weight; + } + + Object writeReplace() { + return weigher; + } + } + + /** A queue that discards all additions and is always empty. */ + static final class DiscardingQueue extends AbstractQueue { + @Override public boolean add(Object e) { return true; } + @Override public boolean offer(Object e) { return true; } + @Override public Object poll() { return null; } + @Override public Object peek() { return null; } + @Override public int size() { return 0; } + @Override public Iterator iterator() { return emptyList().iterator(); } + } + + /** A listener that ignores all notifications. */ + enum DiscardingListener implements EvictionListener { + INSTANCE; + + @Override public void onEviction(Object key, Object value) {} + } + + /* ---------------- Serialization Support -------------- */ + + static final long serialVersionUID = 1; + + Object writeReplace() { + return new SerializationProxy(this); + } + + private void readObject(ObjectInputStream stream) throws InvalidObjectException { + throw new InvalidObjectException("Proxy required"); + } + + /** + * A proxy that is serialized instead of the map. The page-replacement + * algorithm's data structures are not serialized so the deserialized + * instance contains only the entries. This is acceptable as caches hold + * transient data that is recomputable and serialization would tend to be + * used as a fast warm-up process. + */ + static final class SerializationProxy implements Serializable { + final EntryWeigher weigher; + final EvictionListener listener; + final int concurrencyLevel; + final Map data; + final long capacity; + + SerializationProxy(ConcurrentLinkedHashMap map) { + concurrencyLevel = map.concurrencyLevel; + data = new HashMap(map); + capacity = map.capacity.get(); + listener = map.listener; + weigher = map.weigher; + } + + Object readResolve() { + ConcurrentLinkedHashMap map = new Builder() + .concurrencyLevel(concurrencyLevel) + .maximumWeightedCapacity(capacity) + .listener(listener) + .weigher(weigher) + .build(); + map.putAll(data); + return map; + } + + static final long serialVersionUID = 1; + } + + /* ---------------- Builder -------------- */ + + /** + * A builder that creates {@link ConcurrentLinkedHashMap} instances. It + * provides a flexible approach for constructing customized instances with + * a named parameter syntax. It can be used in the following manner: + *
    {@code
    +   * ConcurrentMap> graph = new Builder>()
    +   *     .maximumWeightedCapacity(5000)
    +   *     .weigher(Weighers.set())
    +   *     .build();
    +   * }
    + */ + public static final class Builder { + static final int DEFAULT_CONCURRENCY_LEVEL = 16; + static final int DEFAULT_INITIAL_CAPACITY = 16; + + EvictionListener listener; + EntryWeigher weigher; + + int concurrencyLevel; + int initialCapacity; + long capacity; + + @SuppressWarnings("unchecked") + public Builder() { + capacity = -1; + weigher = Weighers.entrySingleton(); + initialCapacity = DEFAULT_INITIAL_CAPACITY; + concurrencyLevel = DEFAULT_CONCURRENCY_LEVEL; + listener = (EvictionListener) DiscardingListener.INSTANCE; + } + + /** + * Specifies the initial capacity of the hash table (default 16). + * This is the number of key-value pairs that the hash table can hold + * before a resize operation is required. + * + * @param initialCapacity the initial capacity used to size the hash table + * to accommodate this many entries. + * + * @return Builder + * @throws IllegalArgumentException if the initialCapacity is negative + */ + public Builder initialCapacity(int initialCapacity) { + checkArgument(initialCapacity >= 0); + this.initialCapacity = initialCapacity; + return this; + } + + /** + * Specifies the maximum weighted capacity to coerce the map to and may + * exceed it temporarily. + * + * @param capacity the weighted threshold to bound the map by + * @return Builder + * @throws IllegalArgumentException if the maximumWeightedCapacity is + * negative + */ + public Builder maximumWeightedCapacity(long capacity) { + checkArgument(capacity >= 0); + this.capacity = capacity; + return this; + } + + /** + * Specifies the estimated number of concurrently updating threads. The + * implementation performs internal sizing to try to accommodate this many + * threads (default 16). + * + * @param concurrencyLevel the estimated number of concurrently updating + * threads + * @return Builder + * @throws IllegalArgumentException if the concurrencyLevel is less than or + * equal to zero + */ + public Builder concurrencyLevel(int concurrencyLevel) { + checkArgument(concurrencyLevel > 0); + this.concurrencyLevel = concurrencyLevel; + return this; + } + + /** + * Specifies an optional listener that is registered for notification when + * an entry is evicted. + * + * @param listener the object to forward evicted entries to + * @return Builder + * @throws NullPointerException if the listener is null + */ + public Builder listener(EvictionListener listener) { + checkNotNull(listener); + this.listener = listener; + return this; + } + + /** + * Specifies an algorithm to determine how many the units of capacity a + * value consumes. The default algorithm bounds the map by the number of + * key-value pairs by giving each entry a weight of 1. + * + * @param weigher the algorithm to determine a value's weight + * @return Builder + * @throws NullPointerException if the weigher is null + */ + public Builder weigher(Weigher weigher) { + this.weigher = (weigher == Weighers.singleton()) + ? Weighers.entrySingleton() + : new BoundedEntryWeigher(Weighers.asEntryWeigher(weigher)); + return this; + } + + /** + * Specifies an algorithm to determine how many the units of capacity an + * entry consumes. The default algorithm bounds the map by the number of + * key-value pairs by giving each entry a weight of 1. + * + * @param weigher the algorithm to determine a entry's weight + * @return Builder + * @throws NullPointerException if the weigher is null + */ + public Builder weigher(EntryWeigher weigher) { + this.weigher = (weigher == Weighers.entrySingleton()) + ? Weighers.entrySingleton() + : new BoundedEntryWeigher(weigher); + return this; + } + + /** + * Creates a new {@link ConcurrentLinkedHashMap} instance. + * + * @return ConcurrentLinkedHashMap + * @throws IllegalStateException if the maximum weighted capacity was + * not set + */ + public ConcurrentLinkedHashMap build() { + checkState(capacity >= 0); + return new ConcurrentLinkedHashMap(this); + } + } +} diff --git a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/EntryWeigher.java b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/EntryWeigher.java new file mode 100644 index 000000000..9bf2a22b0 --- /dev/null +++ b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/EntryWeigher.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package mssql.googlecode.concurrentlinkedhashmap; + +/** + * A class that can determine the weight of an entry. The total weight threshold + * is used to determine when an eviction is required. + * + * @author ben.manes@gmail.com (Ben Manes) + * @see + * http://code.google.com/p/concurrentlinkedhashmap/ + */ +public interface EntryWeigher { + + /** + * Measures an entry's weight to determine how many units of capacity that + * the key and value consumes. An entry must consume a minimum of one unit. + * + * @param key the key to weigh + * @param value the value to weigh + * @return the entry's weight + */ + int weightOf(K key, V value); +} diff --git a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/EvictionListener.java b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/EvictionListener.java new file mode 100644 index 000000000..65488587c --- /dev/null +++ b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/EvictionListener.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package mssql.googlecode.concurrentlinkedhashmap; + +/** + * A listener registered for notification when an entry is evicted. An instance + * may be called concurrently by multiple threads to process entries. An + * implementation should avoid performing blocking calls or synchronizing on + * shared resources. + *

    + * The listener is invoked by {@link ConcurrentLinkedHashMap} on a caller's + * thread and will not block other threads from operating on the map. An + * implementation should be aware that the caller's thread will not expect + * long execution times or failures as a side effect of the listener being + * notified. Execution safety and a fast turn around time can be achieved by + * performing the operation asynchronously, such as by submitting a task to an + * {@link java.util.concurrent.ExecutorService}. + * + * @author ben.manes@gmail.com (Ben Manes) + * @see + * http://code.google.com/p/concurrentlinkedhashmap/ + */ +public interface EvictionListener { + + /** + * A call-back notification that the entry was evicted. + * + * @param key the entry's key + * @param value the entry's value + */ + void onEviction(K key, V value); +} diff --git a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/LICENSE b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/LinkedDeque.java b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/LinkedDeque.java new file mode 100644 index 000000000..2bb23ea78 --- /dev/null +++ b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/LinkedDeque.java @@ -0,0 +1,460 @@ +/* + * Copyright 2011 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package mssql.googlecode.concurrentlinkedhashmap; + +import java.util.AbstractCollection; +import java.util.Collection; +import java.util.Deque; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Linked list implementation of the {@link Deque} interface where the link + * pointers are tightly integrated with the element. Linked deques have no + * capacity restrictions; they grow as necessary to support usage. They are not + * thread-safe; in the absence of external synchronization, they do not support + * concurrent access by multiple threads. Null elements are prohibited. + *

    + * Most LinkedDeque operations run in constant time by assuming that + * the {@link Linked} parameter is associated with the deque instance. Any usage + * that violates this assumption will result in non-deterministic behavior. + *

    + * The iterators returned by this class are not fail-fast: If + * the deque is modified at any time after the iterator is created, the iterator + * will be in an unknown state. Thus, in the face of concurrent modification, + * the iterator risks arbitrary, non-deterministic behavior at an undetermined + * time in the future. + * + * @author ben.manes@gmail.com (Ben Manes) + * @param the type of elements held in this collection + * @see + * http://code.google.com/p/concurrentlinkedhashmap/ + */ +final class LinkedDeque> extends AbstractCollection implements Deque { + + // This class provides a doubly-linked list that is optimized for the virtual + // machine. The first and last elements are manipulated instead of a slightly + // more convenient sentinel element to avoid the insertion of null checks with + // NullPointerException throws in the byte code. The links to a removed + // element are cleared to help a generational garbage collector if the + // discarded elements inhabit more than one generation. + + /** + * Pointer to first node. + * Invariant: (first == null && last == null) || + * (first.prev == null) + */ + E first; + + /** + * Pointer to last node. + * Invariant: (first == null && last == null) || + * (last.next == null) + */ + E last; + + /** + * Links the element to the front of the deque so that it becomes the first + * element. + * + * @param e the unlinked element + */ + void linkFirst(final E e) { + final E f = first; + first = e; + + if (f == null) { + last = e; + } else { + f.setPrevious(e); + e.setNext(f); + } + } + + /** + * Links the element to the back of the deque so that it becomes the last + * element. + * + * @param e the unlinked element + */ + void linkLast(final E e) { + final E l = last; + last = e; + + if (l == null) { + first = e; + } else { + l.setNext(e); + e.setPrevious(l); + } + } + + /** Unlinks the non-null first element. */ + E unlinkFirst() { + final E f = first; + final E next = f.getNext(); + f.setNext(null); + + first = next; + if (next == null) { + last = null; + } else { + next.setPrevious(null); + } + return f; + } + + /** Unlinks the non-null last element. */ + E unlinkLast() { + final E l = last; + final E prev = l.getPrevious(); + l.setPrevious(null); + last = prev; + if (prev == null) { + first = null; + } else { + prev.setNext(null); + } + return l; + } + + /** Unlinks the non-null element. */ + void unlink(E e) { + final E prev = e.getPrevious(); + final E next = e.getNext(); + + if (prev == null) { + first = next; + } else { + prev.setNext(next); + e.setPrevious(null); + } + + if (next == null) { + last = prev; + } else { + next.setPrevious(prev); + e.setNext(null); + } + } + + @Override + public boolean isEmpty() { + return (first == null); + } + + void checkNotEmpty() { + if (isEmpty()) { + throw new NoSuchElementException(); + } + } + + /** + * {@inheritDoc} + *

    + * Beware that, unlike in most collections, this method is NOT a + * constant-time operation. + */ + @Override + public int size() { + int size = 0; + for (E e = first; e != null; e = e.getNext()) { + size++; + } + return size; + } + + @Override + public void clear() { + for (E e = first; e != null;) { + E next = e.getNext(); + e.setPrevious(null); + e.setNext(null); + e = next; + } + first = last = null; + } + + @Override + public boolean contains(Object o) { + return (o instanceof Linked) && contains((Linked) o); + } + + // A fast-path containment check + boolean contains(Linked e) { + return (e.getPrevious() != null) + || (e.getNext() != null) + || (e == first); + } + + /** + * Moves the element to the front of the deque so that it becomes the first + * element. + * + * @param e the linked element + */ + public void moveToFront(E e) { + if (e != first) { + unlink(e); + linkFirst(e); + } + } + + /** + * Moves the element to the back of the deque so that it becomes the last + * element. + * + * @param e the linked element + */ + public void moveToBack(E e) { + if (e != last) { + unlink(e); + linkLast(e); + } + } + + @Override + public E peek() { + return peekFirst(); + } + + @Override + public E peekFirst() { + return first; + } + + @Override + public E peekLast() { + return last; + } + + @Override + public E getFirst() { + checkNotEmpty(); + return peekFirst(); + } + + @Override + public E getLast() { + checkNotEmpty(); + return peekLast(); + } + + @Override + public E element() { + return getFirst(); + } + + @Override + public boolean offer(E e) { + return offerLast(e); + } + + @Override + public boolean offerFirst(E e) { + if (contains(e)) { + return false; + } + linkFirst(e); + return true; + } + + @Override + public boolean offerLast(E e) { + if (contains(e)) { + return false; + } + linkLast(e); + return true; + } + + @Override + public boolean add(E e) { + return offerLast(e); + } + + + @Override + public void addFirst(E e) { + if (!offerFirst(e)) { + throw new IllegalArgumentException(); + } + } + + @Override + public void addLast(E e) { + if (!offerLast(e)) { + throw new IllegalArgumentException(); + } + } + + @Override + public E poll() { + return pollFirst(); + } + + @Override + public E pollFirst() { + return isEmpty() ? null : unlinkFirst(); + } + + @Override + public E pollLast() { + return isEmpty() ? null : unlinkLast(); + } + + @Override + public E remove() { + return removeFirst(); + } + + @Override + @SuppressWarnings("unchecked") + public boolean remove(Object o) { + return (o instanceof Linked) && remove((E) o); + } + + // A fast-path removal + boolean remove(E e) { + if (contains(e)) { + unlink(e); + return true; + } + return false; + } + + @Override + public E removeFirst() { + checkNotEmpty(); + return pollFirst(); + } + + @Override + public boolean removeFirstOccurrence(Object o) { + return remove(o); + } + + @Override + public E removeLast() { + checkNotEmpty(); + return pollLast(); + } + + @Override + public boolean removeLastOccurrence(Object o) { + return remove(o); + } + + @Override + public boolean removeAll(Collection c) { + boolean modified = false; + for (Object o : c) { + modified |= remove(o); + } + return modified; + } + + @Override + public void push(E e) { + addFirst(e); + } + + @Override + public E pop() { + return removeFirst(); + } + + @Override + public Iterator iterator() { + return new AbstractLinkedIterator(first) { + @Override E computeNext() { + return cursor.getNext(); + } + }; + } + + @Override + public Iterator descendingIterator() { + return new AbstractLinkedIterator(last) { + @Override E computeNext() { + return cursor.getPrevious(); + } + }; + } + + abstract class AbstractLinkedIterator implements Iterator { + E cursor; + + /** + * Creates an iterator that can can traverse the deque. + * + * @param start the initial element to begin traversal from + */ + AbstractLinkedIterator(E start) { + cursor = start; + } + + @Override + public boolean hasNext() { + return (cursor != null); + } + + @Override + public E next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + E e = cursor; + cursor = computeNext(); + return e; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + /** + * Retrieves the next element to traverse to or null if there are + * no more elements. + */ + abstract E computeNext(); + } +} + +/** + * An element that is linked on the {@link Deque}. + */ +interface Linked> { + + /** + * Retrieves the previous element or null if either the element is + * unlinked or the first element on the deque. + */ + T getPrevious(); + + /** Sets the previous element or null if there is no link. */ + void setPrevious(T prev); + + /** + * Retrieves the next element or null if either the element is + * unlinked or the last element on the deque. + */ + T getNext(); + + /** Sets the next element or null if there is no link. */ + void setNext(T next); +} diff --git a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/NOTICE b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/NOTICE new file mode 100644 index 000000000..e1cedae49 --- /dev/null +++ b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/NOTICE @@ -0,0 +1,7 @@ +ConcurrentLinkedHashMap +Copyright 2008, Ben Manes +Copyright 2010, Google Inc. + +Some alternate data structures provided by JSR-166e +from http://gee.cs.oswego.edu/dl/concurrency-interest/. +Written by Doug Lea and released as Public Domain. diff --git a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/Weigher.java b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/Weigher.java new file mode 100644 index 000000000..529622c8e --- /dev/null +++ b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/Weigher.java @@ -0,0 +1,36 @@ +/* + * Copyright 2010 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package mssql.googlecode.concurrentlinkedhashmap; + +/** + * A class that can determine the weight of a value. The total weight threshold + * is used to determine when an eviction is required. + * + * @author ben.manes@gmail.com (Ben Manes) + * @see + * http://code.google.com/p/concurrentlinkedhashmap/ + */ +public interface Weigher { + + /** + * Measures an object's weight to determine how many units of capacity that + * the value consumes. A value must consume a minimum of one unit. + * + * @param value the object to weigh + * @return the object's weight + */ + int weightOf(V value); +} diff --git a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/Weighers.java b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/Weighers.java new file mode 100644 index 000000000..2dd4531d0 --- /dev/null +++ b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/Weighers.java @@ -0,0 +1,292 @@ +/* + * Copyright 2010 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package mssql.googlecode.concurrentlinkedhashmap; + +import static mssql.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap.checkNotNull; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A common set of {@link Weigher} and {@link EntryWeigher} implementations. + * + * @author ben.manes@gmail.com (Ben Manes) + * @see + * http://code.google.com/p/concurrentlinkedhashmap/ + */ +public final class Weighers { + + private Weighers() { + throw new AssertionError(); + } + + /** + * A entry weigher backed by the specified weigher. The weight of the value + * determines the weight of the entry. + * + * @param K + * @param V + * @param weigher the weigher to be "wrapped" in a entry weigher. + * @return A entry weigher view of the specified weigher. + */ + public static EntryWeigher asEntryWeigher( + final Weigher weigher) { + return (weigher == singleton()) + ? Weighers.entrySingleton() + : new EntryWeigherView(weigher); + } + + /** + * A weigher where an entry has a weight of 1. A map bounded with + * this weigher will evict when the number of key-value pairs exceeds the + * capacity. + * + * @param K + * @param V + * @return A weigher where a value takes one unit of capacity. + */ + @SuppressWarnings({"cast", "unchecked"}) + public static EntryWeigher entrySingleton() { + return (EntryWeigher) SingletonEntryWeigher.INSTANCE; + } + + /** + * A weigher where a value has a weight of 1. A map bounded with + * this weigher will evict when the number of key-value pairs exceeds the + * capacity. + * + * @param V + * @return A weigher where a value takes one unit of capacity. + */ + @SuppressWarnings({"cast", "unchecked"}) + public static Weigher singleton() { + return (Weigher) SingletonWeigher.INSTANCE; + } + + /** + * A weigher where the value is a byte array and its weight is the number of + * bytes. A map bounded with this weigher will evict when the number of bytes + * exceeds the capacity rather than the number of key-value pairs in the map. + * This allows for restricting the capacity based on the memory-consumption + * and is primarily for usage by dedicated caching servers that hold the + * serialized data. + *

    + * A value with a weight of 0 will be rejected by the map. If a value + * with this weight can occur then the caller should eagerly evaluate the + * value and treat it as a removal operation. Alternatively, a custom weigher + * may be specified on the map to assign an empty value a positive weight. + * + * @return A weigher where each byte takes one unit of capacity. + */ + public static Weigher byteArray() { + return ByteArrayWeigher.INSTANCE; + } + + /** + * A weigher where the value is a {@link Iterable} and its weight is the + * number of elements. This weigher only should be used when the alternative + * {@link #collection()} weigher cannot be, as evaluation takes O(n) time. A + * map bounded with this weigher will evict when the total number of elements + * exceeds the capacity rather than the number of key-value pairs in the map. + *

    + * A value with a weight of 0 will be rejected by the map. If a value + * with this weight can occur then the caller should eagerly evaluate the + * value and treat it as a removal operation. Alternatively, a custom weigher + * may be specified on the map to assign an empty value a positive weight. + * + * @param E + * @return A weigher where each element takes one unit of capacity. + */ + @SuppressWarnings({"cast", "unchecked"}) + public static Weigher> iterable() { + return (Weigher>) (Weigher) IterableWeigher.INSTANCE; + } + + /** + * A weigher where the value is a {@link Collection} and its weight is the + * number of elements. A map bounded with this weigher will evict when the + * total number of elements exceeds the capacity rather than the number of + * key-value pairs in the map. + *

    + * A value with a weight of 0 will be rejected by the map. If a value + * with this weight can occur then the caller should eagerly evaluate the + * value and treat it as a removal operation. Alternatively, a custom weigher + * may be specified on the map to assign an empty value a positive weight. + * + * @param E + * @return A weigher where each element takes one unit of capacity. + */ + @SuppressWarnings({"cast", "unchecked"}) + public static Weigher> collection() { + return (Weigher>) (Weigher) CollectionWeigher.INSTANCE; + } + + /** + * A weigher where the value is a {@link List} and its weight is the number + * of elements. A map bounded with this weigher will evict when the total + * number of elements exceeds the capacity rather than the number of + * key-value pairs in the map. + *

    + * A value with a weight of 0 will be rejected by the map. If a value + * with this weight can occur then the caller should eagerly evaluate the + * value and treat it as a removal operation. Alternatively, a custom weigher + * may be specified on the map to assign an empty value a positive weight. + * + * @param E + * @return A weigher where each element takes one unit of capacity. + */ + @SuppressWarnings({"cast", "unchecked"}) + public static Weigher> list() { + return (Weigher>) (Weigher) ListWeigher.INSTANCE; + } + + /** + * A weigher where the value is a {@link Set} and its weight is the number + * of elements. A map bounded with this weigher will evict when the total + * number of elements exceeds the capacity rather than the number of + * key-value pairs in the map. + *

    + * A value with a weight of 0 will be rejected by the map. If a value + * with this weight can occur then the caller should eagerly evaluate the + * value and treat it as a removal operation. Alternatively, a custom weigher + * may be specified on the map to assign an empty value a positive weight. + * + * @param E + * @return A weigher where each element takes one unit of capacity. + */ + @SuppressWarnings({"cast", "unchecked"}) + public static Weigher> set() { + return (Weigher>) (Weigher) SetWeigher.INSTANCE; + } + + /** + * A weigher where the value is a {@link Map} and its weight is the number of + * entries. A map bounded with this weigher will evict when the total number of + * entries across all values exceeds the capacity rather than the number of + * key-value pairs in the map. + *

    + * A value with a weight of 0 will be rejected by the map. If a value + * with this weight can occur then the caller should eagerly evaluate the + * value and treat it as a removal operation. Alternatively, a custom weigher + * may be specified on the map to assign an empty value a positive weight. + * + * @param A + * @param B + * @return A weigher where each entry takes one unit of capacity. + */ + @SuppressWarnings({"cast", "unchecked"}) + public static Weigher> map() { + return (Weigher>) (Weigher) MapWeigher.INSTANCE; + } + + static final class EntryWeigherView implements EntryWeigher, Serializable { + static final long serialVersionUID = 1; + final Weigher weigher; + + EntryWeigherView(Weigher weigher) { + checkNotNull(weigher); + this.weigher = weigher; + } + + @Override + public int weightOf(K key, V value) { + return weigher.weightOf(value); + } + } + + enum SingletonEntryWeigher implements EntryWeigher { + INSTANCE; + + @Override + public int weightOf(Object key, Object value) { + return 1; + } + } + + enum SingletonWeigher implements Weigher { + INSTANCE; + + @Override + public int weightOf(Object value) { + return 1; + } + } + + enum ByteArrayWeigher implements Weigher { + INSTANCE; + + @Override + public int weightOf(byte[] value) { + return value.length; + } + } + + enum IterableWeigher implements Weigher> { + INSTANCE; + + @Override + public int weightOf(Iterable values) { + if (values instanceof Collection) { + return ((Collection) values).size(); + } + int size = 0; + for (Object value : values) { + size++; + } + return size; + } + } + + enum CollectionWeigher implements Weigher> { + INSTANCE; + + @Override + public int weightOf(Collection values) { + return values.size(); + } + } + + enum ListWeigher implements Weigher> { + INSTANCE; + + @Override + public int weightOf(List values) { + return values.size(); + } + } + + enum SetWeigher implements Weigher> { + INSTANCE; + + @Override + public int weightOf(Set values) { + return values.size(); + } + } + + enum MapWeigher implements Weigher> { + INSTANCE; + + @Override + public int weightOf(Map values) { + return values.size(); + } + } +} diff --git a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/package-info.java b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/package-info.java new file mode 100644 index 000000000..ad0fd0026 --- /dev/null +++ b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright 2011 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * This package contains an implementation of a bounded {@link java.util.concurrent.ConcurrentMap} data structure. + *

    + * {@link Weigher} is a simple interface for determining how many units of capacity an entry consumes. Depending on which concrete Weigher class is + * used, an entry may consume a different amount of space within the cache. The {@link Weighers} class provides utility methods for obtaining the most + * common kinds of implementations. + *

    + * {@link EvictionListener} provides the ability to be notified when an entry is evicted from the map. An eviction occurs when the entry was + * automatically removed due to the map exceeding a capacity threshold. It is not called when an entry was explicitly removed. + *

    + * The {@link ConcurrentLinkedHashMap} class supplies an efficient, scalable, thread-safe, bounded map. As with the + * Java Collections Framework the "Concurrent" prefix is used to indicate that the map is not governed by a single exclusion lock. + * + * @see http://code.google.com/p/concurrentlinkedhashmap/ + */ +package mssql.googlecode.concurrentlinkedhashmap; diff --git a/src/samples/README.md b/src/samples/README.md index e39c8c246..2ea757d28 100644 --- a/src/samples/README.md +++ b/src/samples/README.md @@ -31,6 +31,9 @@ The following samples are available: 7. sparse * **SparseColumns** - how to detect column sets. It also shows a technique for parsing a column set's XML output, to get data from the sparse columns. + +8. constrained + * **ConstrainedSample** - how to connect with Kerberos constrained delegation using an impersonated credential. ## Running Samples diff --git a/src/samples/constrained/pom.xml b/src/samples/constrained/pom.xml new file mode 100644 index 000000000..e9964b64e --- /dev/null +++ b/src/samples/constrained/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + com.microsoft.sqlserver.jdbc + constrained + 0.0.1 + + jar + + ${project.artifactId} + https://github.com/Microsoft/mssql-jdbc/tree/master/src/samples + + + UTF-8 + + + + + com.microsoft.sqlserver + mssql-jdbc + 6.1.5.jre8-preview + + + + + + ConstrainedSample + + ConstrainedSample + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + ConstrainedSample + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.0 + + 1.8 + 1.8 + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.0.2 + + + + + diff --git a/src/samples/constrained/src/main/java/ConstrainedSample.java b/src/samples/constrained/src/main/java/ConstrainedSample.java new file mode 100644 index 000000000..a8e36454e --- /dev/null +++ b/src/samples/constrained/src/main/java/ConstrainedSample.java @@ -0,0 +1,161 @@ +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.sql.Connection; +import java.sql.DriverManager; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import javax.security.auth.Subject; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; + +import com.sun.security.jgss.ExtendedGSSCredential; + +/** + * + * Sample of constrained delegation connection. + * + * An intermediate service is necessary to impersonate the client. This service needs to be configured with the + * options: + * "Trust this user for delegation to specified services only" + * "Use any authentication protocol" + * + */ +public class ConstrainedSample { + + // Connection properties + private static final String DRIVER_CLASS_NAME ="com.microsoft.sqlserver.jdbc.SQLServerDriver"; + private static final String CONNECTION_URI = "jdbc:sqlserver:// URI of the SQLServer"; + + private static final String TARGET_USER_NAME = "User to be impersonated"; + + // Impersonation service properties + private static final String SERVICE_PRINCIPAL = "SPN"; + private static final String KEYTAB_ROUTE = "Route to the keytab file"; + + private static final Properties driverProperties; + private static Oid krb5Oid; + + private static Subject serviceSubject; + + static { + + driverProperties = new Properties(); + driverProperties.setProperty("integratedSecurity", "true"); + driverProperties.setProperty("authenticationScheme", "JavaKerberos"); + + try { + krb5Oid = new Oid("1.2.840.113554.1.2.2"); + } catch (GSSException e) { + System.out.println("Error creating Oid: " + e); + System.exit(-1); + } + } + + public static void main(String... args) throws Exception { + + Class.forName(DRIVER_CLASS_NAME).getConstructor().newInstance(); + System.out.println("Service subject: " + doInitialLogin()); + + // Get impersonated user credentials thanks S4U2self mechanism + GSSCredential impersonatedUserCreds = impersonate(); + System.out.println("Credentials for " + TARGET_USER_NAME + ": " + impersonatedUserCreds); + + // Create a connection for target service thanks S4U2proxy mechanism + try (Connection con = createConnection(impersonatedUserCreds)) { + System.out.println("Connection succesfully: " + con); + } + + } + + /** + * + * Authenticate the intermediate server that is going to impersonate the client + * + * @return a subject for the intermediate server with the keytab credentials + * @throws PrivilegedActionException in case of failure + */ + private static Subject doInitialLogin() throws PrivilegedActionException { + serviceSubject = new Subject(); + + LoginModule krb5Module; + try { + krb5Module = (LoginModule) Class.forName("com.sun.security.auth.module.Krb5LoginModule").getConstructor() + .newInstance(); + } catch (Exception e) { + System.out.print("Error loading Krb5LoginModule module: " + e); + throw new PrivilegedActionException(e); + } + + System.setProperty("sun.security.krb5.debug", String.valueOf(true)); + + Map options = new HashMap<>(); + options.put("useKeyTab", "true"); + options.put("storeKey", "true"); + options.put("doNotPrompt", "true"); + options.put("keyTab", KEYTAB_ROUTE); + options.put("principal", SERVICE_PRINCIPAL); + options.put("debug", "true"); + options.put("isInitiator", "true"); + + Map sharedState = new HashMap<>(0); + + krb5Module.initialize(serviceSubject, null, sharedState, options); + try { + krb5Module.login(); + krb5Module.commit(); + } catch (LoginException e) { + System.out.print("Error authenticating with Kerberos: " + e); + try { + krb5Module.abort(); + } catch (LoginException e1) { + System.out.print("Error aborting Kerberos authentication: " + e1); + throw new PrivilegedActionException(e); + } + throw new PrivilegedActionException(e); + } + + return serviceSubject; + } + + /** + * Generate the impersonated user credentials thanks to the S4U2self mechanism + * + * @return the client impersonated GSSCredential + * @throws PrivilegedActionException in case of failure + */ + private static GSSCredential impersonate() throws PrivilegedActionException { + return Subject.doAs(serviceSubject, (PrivilegedExceptionAction) () -> { + GSSManager manager = GSSManager.getInstance(); + + GSSCredential self = manager.createCredential(null, GSSCredential.DEFAULT_LIFETIME, krb5Oid, + GSSCredential.INITIATE_ONLY); + GSSName user = manager.createName(TARGET_USER_NAME, GSSName.NT_USER_NAME); + return ((ExtendedGSSCredential) self).impersonate(user); + }); + } + + /** + * Obtains a connection using an impersonated credential + * + * @param impersonatedUserCredential impersonated user credentials + * @return a connection to the SQL Server opened using the given impersonated credential + * @throws PrivilegedActionException in case of failure + */ + private static Connection createConnection(final GSSCredential impersonatedUserCredential) + throws PrivilegedActionException { + + return Subject.doAs(new Subject(), (PrivilegedExceptionAction) () -> { + driverProperties.put("gsscredential", impersonatedUserCredential); + return DriverManager.getConnection(CONNECTION_URI, driverProperties); + }); + } + +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/AESetup.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/AESetup.java new file mode 100644 index 000000000..9ae2445c1 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/AESetup.java @@ -0,0 +1,2092 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.AlwaysEncrypted; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.math.BigDecimal; +import java.sql.Date; +import java.sql.DriverManager; +import java.sql.JDBCType; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.LinkedList; +import java.util.Properties; + +import javax.xml.bind.DatatypeConverter; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; +import org.opentest4j.TestAbortedException; + +import com.microsoft.sqlserver.jdbc.SQLServerColumnEncryptionJavaKeyStoreProvider; +import com.microsoft.sqlserver.jdbc.SQLServerColumnEncryptionKeyStoreProvider; +import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.jdbc.SQLServerStatement; +import com.microsoft.sqlserver.jdbc.SQLServerStatementColumnEncryptionSetting; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.DBConnection; +import com.microsoft.sqlserver.testframework.Utils; +import com.microsoft.sqlserver.testframework.util.RandomData; +import com.microsoft.sqlserver.testframework.util.Util; + +import microsoft.sql.DateTimeOffset; + +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Setup for Always Encrypted test This test will work on Appveyor and Travis-ci as java key store gets created from the .yml scripts. Users on their + * local machine should create the keystore manually and save the alias name in JavaKeyStore.txt file. For local test purposes, put this in the + * target/test-classes directory + * + */ +@RunWith(JUnitPlatform.class) +public class AESetup extends AbstractTest { + + static final String javaKeyStoreInputFile = "JavaKeyStore.txt"; + static final String keyStoreName = "MSSQL_JAVA_KEYSTORE"; + static final String jksName = "clientcert.jks"; + static final String cmkName = "JDBC_CMK"; + static final String cekName = "JDBC_CEK"; + static final String secretstrJks = "password"; + static final String charTable = "JDBCEncryptedCharTable"; + static final String binaryTable = "JDBCEncryptedBinaryTable"; + static final String dateTable = "JDBCEncryptedDateTable"; + static final String numericTable = "JDBCEncryptedNumericTable"; + static final String scaleDateTable = "JDBCEncryptedScaleDateTable"; + + static final String uid = "171fbe25-4331-4765-a838-b2e3eea3e7ea"; + + static String filePath = null; + static String thumbprint = null; + static SQLServerConnection con = null; + static SQLServerStatement stmt = null; + static String keyPath = null; + static String javaKeyAliases = null; + static String OS = System.getProperty("os.name").toLowerCase(); + static SQLServerColumnEncryptionKeyStoreProvider storeProvider = null; + static SQLServerStatementColumnEncryptionSetting stmtColEncSetting = null; + + /** + * Create connection, statement and generate path of resource file + * + * @throws Exception + * @throws TestAbortedException + */ + @BeforeAll + static void setUpConnection() throws TestAbortedException, Exception { + assumeTrue(13 <= new DBConnection(connectionString).getServerVersion(), + "Aborting test case as SQL Server version is not compatible with Always encrypted "); + + String AETestConenctionString = connectionString + ";sendTimeAsDateTime=false"; + readFromFile(javaKeyStoreInputFile, "Alias name"); + + try(SQLServerConnection con = (SQLServerConnection) DriverManager.getConnection(AETestConenctionString); + SQLServerStatement stmt = (SQLServerStatement) con.createStatement()) { + dropCEK(stmt); + dropCMK(stmt); + } + + keyPath = Utils.getCurrentClassPath() + jksName; + storeProvider = new SQLServerColumnEncryptionJavaKeyStoreProvider(keyPath, secretstrJks.toCharArray()); + stmtColEncSetting = SQLServerStatementColumnEncryptionSetting.Enabled; + + Properties info = new Properties(); + info.setProperty("ColumnEncryptionSetting", "Enabled"); + info.setProperty("keyStoreAuthentication", "JavaKeyStorePassword"); + info.setProperty("keyStoreLocation", keyPath); + info.setProperty("keyStoreSecret", secretstrJks); + + con = (SQLServerConnection) DriverManager.getConnection(AETestConenctionString, info); + stmt = (SQLServerStatement) con.createStatement(); + createCMK(keyStoreName, javaKeyAliases); + createCEK(storeProvider); + } + + /** + * Dropping all CMKs and CEKs and any open resources. Technically, dropAll depends on the state of the class so it shouldn't be static, but the + * AfterAll annotation requires it to be static. + * + * @throws SQLServerException + * @throws SQLException + */ + @AfterAll + private static void dropAll() throws SQLServerException, SQLException { + dropTables(stmt); + dropCEK(stmt); + dropCMK(stmt); + Util.close(null, stmt, con); + } + + /** + * Read the alias from file which is created during creating jks If the jks and alias name in JavaKeyStore.txt does not exists, will not run! + * + * @param inputFile + * @param lookupValue + * @throws IOException + */ + private static void readFromFile(String inputFile, + String lookupValue) throws IOException { + filePath = Utils.getCurrentClassPath(); + try { + File f = new File(filePath + inputFile); + assumeTrue(f.exists(), "Aborting test case since no java key store and alias name exists!"); + try(BufferedReader buffer = new BufferedReader(new FileReader(f))) { + String readLine = ""; + String[] linecontents; + + while ((readLine = buffer.readLine()) != null) { + if (readLine.trim().contains(lookupValue)) { + linecontents = readLine.split(" "); + javaKeyAliases = linecontents[2]; + break; + } + } + } + } + catch (IOException e) { + fail(e.toString()); + } + } + + /** + * Create encrypted table for Binary + * + * @throws SQLException + */ + protected static void createBinaryTable() throws SQLException { + String sql = "create table " + binaryTable + " (" + "PlainBinary binary(20) null," + + "RandomizedBinary binary(20) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicBinary binary(20) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainVarbinary varbinary(50) null," + + "RandomizedVarbinary varbinary(50) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicVarbinary varbinary(50) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainVarbinaryMax varbinary(max) null," + + "RandomizedVarbinaryMax varbinary(max) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicVarbinaryMax varbinary(max) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainBinary512 binary(512) null," + + "RandomizedBinary512 binary(512) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicBinary512 binary(512) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainBinary8000 varbinary(8000) null," + + "RandomizedBinary8000 varbinary(8000) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicBinary8000 varbinary(8000) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + ");"; + + try { + stmt.execute(sql); + stmt.execute("DBCC FREEPROCCACHE"); + } + catch (SQLException e) { + fail(e.toString()); + } + } + + /** + * Create encrypted table for Char + * + * @throws SQLException + */ + protected static void createCharTable() throws SQLException { + String sql = "create table " + charTable + " (" + "PlainChar char(20) null," + + "RandomizedChar char(20) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicChar char(20) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainVarchar varchar(50) null," + + "RandomizedVarchar varchar(50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicVarchar varchar(50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainVarcharMax varchar(max) null," + + "RandomizedVarcharMax varchar(max) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicVarcharMax varchar(max) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainNchar nchar(30) null," + + "RandomizedNchar nchar(30) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicNchar nchar(30) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainNvarchar nvarchar(60) null," + + "RandomizedNvarchar nvarchar(60) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicNvarchar nvarchar(60) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainNvarcharMax nvarchar(max) null," + + "RandomizedNvarcharMax nvarchar(max) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicNvarcharMax nvarchar(max) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainUniqueidentifier uniqueidentifier null," + + "RandomizedUniqueidentifier uniqueidentifier ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicUniqueidentifier uniqueidentifier ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainVarchar8000 varchar(8000) null," + + "RandomizedVarchar8000 varchar(8000) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicVarchar8000 varchar(8000) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainNvarchar4000 nvarchar(4000) null," + + "RandomizedNvarchar4000 nvarchar(4000) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicNvarchar4000 nvarchar(4000) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + ");"; + + try { + stmt.execute(sql); + stmt.execute("DBCC FREEPROCCACHE"); + } + catch (SQLException e) { + fail(e.toString()); + } + } + + /** + * Create encrypted table for Date + * + * @throws SQLException + */ + protected void createDateTable() throws SQLException { + String sql = "create table " + dateTable + " (" + "PlainDate date null," + + "RandomizedDate date ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDate date ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainDatetime2Default datetime2 null," + + "RandomizedDatetime2Default datetime2 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDatetime2Default datetime2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainDatetimeoffsetDefault datetimeoffset null," + + "RandomizedDatetimeoffsetDefault datetimeoffset ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDatetimeoffsetDefault datetimeoffset ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainTimeDefault time null," + + "RandomizedTimeDefault time ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicTimeDefault time ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainDatetime datetime null," + + "RandomizedDatetime datetime ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDatetime datetime ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainSmalldatetime smalldatetime null," + + "RandomizedSmalldatetime smalldatetime ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicSmalldatetime smalldatetime ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + ");"; + + try { + stmt.execute(sql); + stmt.execute("DBCC FREEPROCCACHE"); + } + catch (SQLException e) { + fail(e.toString()); + } + } + + /** + * Create encrypted table for Date with precision + * + * @throws SQLException + */ + protected void createDatePrecisionTable(int scale) throws SQLException { + String sql = "create table " + dateTable + " (" + // 1 + + "PlainDatetime2 datetime2(" + scale + ") null," + "RandomizedDatetime2 datetime2(" + scale + + ") ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + cekName + + ") NULL," + "DeterministicDatetime2 datetime2(" + scale + + ") ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + cekName + + ") NULL," + + // 4 + + "PlainDatetime2Default datetime2 null," + + "RandomizedDatetime2Default datetime2 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDatetime2Default datetime2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + // 7 + + "PlainDatetimeoffsetDefault datetimeoffset null," + + "RandomizedDatetimeoffsetDefault datetimeoffset ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDatetimeoffsetDefault datetimeoffset ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + // 10 + + "PlainTimeDefault time null," + + "RandomizedTimeDefault time ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicTimeDefault time ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + // 13 + + "PlainTime time(" + scale + ") null," + "RandomizedTime time(" + scale + + ") ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + cekName + + ") NULL," + "DeterministicTime time(" + scale + + ") ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + cekName + + ") NULL," + + // 16 + + "PlainDatetimeoffset datetimeoffset(" + scale + ") null," + "RandomizedDatetimeoffset datetimeoffset(" + scale + + ") ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + cekName + + ") NULL," + "DeterministicDatetimeoffset datetimeoffset(" + scale + + ") ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + cekName + + ") NULL," + + + ");"; + + try { + stmt.execute(sql); + stmt.execute("DBCC FREEPROCCACHE"); + } + catch (SQLException e) { + fail(e.toString()); + } + } + + /** + * Create encrypted table for Date with scale + * + * @throws SQLException + */ + protected static void createDateScaleTable() throws SQLException { + String sql = "create table " + scaleDateTable + " (" + + + "PlainDatetime2 datetime2(2) null," + + "RandomizedDatetime2 datetime2(2) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDatetime2 datetime2(2) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainTime time(2) null," + + "RandomizedTime time(2) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicTime time(2) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainDatetimeoffset datetimeoffset(2) null," + + "RandomizedDatetimeoffset datetimeoffset(2) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDatetimeoffset datetimeoffset(2) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + ");"; + + try { + stmt.execute(sql); + stmt.execute("DBCC FREEPROCCACHE"); + } + catch (SQLException e) { + fail(e.toString()); + } + } + + /** + * Create encrypted table for Numeric + * + * @throws SQLException + */ + protected static void createNumericTable() throws SQLException { + String sql = "create table " + numericTable + " (" + "PlainBit bit null," + + "RandomizedBit bit ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicBit bit ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainTinyint tinyint null," + + "RandomizedTinyint tinyint ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicTinyint tinyint ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainSmallint smallint null," + + "RandomizedSmallint smallint ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicSmallint smallint ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainInt int null," + + "RandomizedInt int ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicInt int ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainBigint bigint null," + + "RandomizedBigint bigint ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicBigint bigint ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainFloatDefault float null," + + "RandomizedFloatDefault float ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicFloatDefault float ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainFloat float(30) null," + + "RandomizedFloat float(30) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicFloat float(30) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainReal real null," + + "RandomizedReal real ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicReal real ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainDecimalDefault decimal null," + + "RandomizedDecimalDefault decimal ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDecimalDefault decimal ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainDecimal decimal(10,5) null," + + "RandomizedDecimal decimal(10,5) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDecimal decimal(10,5) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainNumericDefault numeric null," + + "RandomizedNumericDefault numeric ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicNumericDefault numeric ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainNumeric numeric(8,2) null," + + "RandomizedNumeric numeric(8,2) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicNumeric numeric(8,2) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainSmallMoney smallmoney null," + + "RandomizedSmallMoney smallmoney ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicSmallMoney smallmoney ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainMoney money null," + + "RandomizedMoney money ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicMoney money ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainDecimal2 decimal(28,4) null," + + "RandomizedDecimal2 decimal(28,4) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDecimal2 decimal(28,4) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainNumeric2 numeric(28,4) null," + + "RandomizedNumeric2 numeric(28,4) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicNumeric2 numeric(28,4) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + ");"; + + try { + stmt.execute(sql); + stmt.execute("DBCC FREEPROCCACHE"); + } + catch (SQLException e) { + fail(e.toString()); + } + } + + /** + * Create encrypted table for Numeric with precision + * + * @throws SQLException + */ + protected void createNumericPrecisionTable(int floatPrecision, + int precision, + int scale) throws SQLException { + String sql = "create table " + numericTable + " (" + "PlainFloat float(" + floatPrecision + ") null," + "RandomizedFloat float(" + + floatPrecision + + ") ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + cekName + + ") NULL," + "DeterministicFloat float(" + floatPrecision + + ") ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + cekName + + ") NULL," + + + "PlainDecimal decimal(" + precision + "," + scale + ") null," + "RandomizedDecimal decimal(" + precision + "," + scale + + ") ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + cekName + + ") NULL," + "DeterministicDecimal decimal(" + precision + "," + scale + + ") ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + cekName + + ") NULL," + + + "PlainNumeric numeric(" + precision + "," + scale + ") null," + "RandomizedNumeric numeric(" + precision + "," + scale + + ") ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + cekName + + ") NULL," + "DeterministicNumeric numeric(" + precision + "," + scale + + ") ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + cekName + + ") NULL" + + + ");"; + + try { + stmt.execute(sql); + stmt.execute("DBCC FREEPROCCACHE"); + } + catch (SQLException e) { + fail(e.toString()); + } + } + + /** + * Create a list of binary values + * + * @param nullable + */ + protected static LinkedList createbinaryValues(boolean nullable) { + + boolean encrypted = true; + RandomData.returnNull = nullable; + + byte[] binary20 = RandomData.generateBinaryTypes("20", nullable, encrypted); + byte[] varbinary50 = RandomData.generateBinaryTypes("50", nullable, encrypted); + byte[] varbinarymax = RandomData.generateBinaryTypes("max", nullable, encrypted); + byte[] binary512 = RandomData.generateBinaryTypes("512", nullable, encrypted); + byte[] varbinary8000 = RandomData.generateBinaryTypes("8000", nullable, encrypted); + + LinkedList list = new LinkedList<>(); + list.add(binary20); + list.add(varbinary50); + list.add(varbinarymax); + list.add(binary512); + list.add(varbinary8000); + + return list; + } + + /** + * Create a list of char values + * + * @param nullable + */ + protected static String[] createCharValues(boolean nullable) { + + boolean encrypted = true; + String char20 = RandomData.generateCharTypes("20", nullable, encrypted); + String varchar50 = RandomData.generateCharTypes("50", nullable, encrypted); + String varcharmax = RandomData.generateCharTypes("max", nullable, encrypted); + String nchar30 = RandomData.generateNCharTypes("30", nullable, encrypted); + String nvarchar60 = RandomData.generateNCharTypes("60", nullable, encrypted); + String nvarcharmax = RandomData.generateNCharTypes("max", nullable, encrypted); + String varchar8000 = RandomData.generateCharTypes("8000", nullable, encrypted); + String nvarchar4000 = RandomData.generateNCharTypes("4000", nullable, encrypted); + + String[] values = {char20.trim(), varchar50, varcharmax, nchar30, nvarchar60, nvarcharmax, uid, varchar8000, nvarchar4000}; + + return values; + } + + /** + * Create a list of numeric values + * + * @param nullable + */ + protected static String[] createNumericValues(boolean nullable) { + + Boolean boolValue = RandomData.generateBoolean(nullable); + Short tinyIntValue = RandomData.generateTinyint(nullable); + Short smallIntValue = RandomData.generateSmallint(nullable); + Integer intValue = RandomData.generateInt(nullable); + Long bigintValue = RandomData.generateLong(nullable); + Double floatValue = RandomData.generateFloat(24, nullable); + Double floatValuewithPrecision = RandomData.generateFloat(53, nullable); + Float realValue = RandomData.generateReal(nullable); + BigDecimal decimal = RandomData.generateDecimalNumeric(18, 0, nullable); + BigDecimal decimalPrecisionScale = RandomData.generateDecimalNumeric(10, 5, nullable); + BigDecimal numeric = RandomData.generateDecimalNumeric(18, 0, nullable); + BigDecimal numericPrecisionScale = RandomData.generateDecimalNumeric(8, 2, nullable); + BigDecimal smallMoney = RandomData.generateSmallMoney(nullable); + BigDecimal money = RandomData.generateMoney(nullable); + BigDecimal decimalPrecisionScale2 = RandomData.generateDecimalNumeric(28, 4, nullable); + BigDecimal numericPrecisionScale2 = RandomData.generateDecimalNumeric(28, 4, nullable); + + String[] numericValues = {"" + boolValue, "" + tinyIntValue, "" + smallIntValue, "" + intValue, "" + bigintValue, "" + floatValue, + "" + floatValuewithPrecision, "" + realValue, "" + decimal, "" + decimalPrecisionScale, "" + numeric, "" + numericPrecisionScale, + "" + smallMoney, "" + money, "" + decimalPrecisionScale2, "" + numericPrecisionScale2}; + + return numericValues; + } + + /** + * Create a list of temporal values + * + * @param nullable + */ + protected static LinkedList createTemporalTypes(boolean nullable) { + + Date date = RandomData.generateDate(nullable); + Timestamp datetime2 = RandomData.generateDatetime2(7, nullable); + DateTimeOffset datetimeoffset = RandomData.generateDatetimeoffset(7, nullable); + Time time = RandomData.generateTime(7, nullable); + Timestamp datetime = RandomData.generateDatetime(nullable); + Timestamp smalldatetime = RandomData.generateSmalldatetime(nullable); + + LinkedList list = new LinkedList<>(); + list.add(date); + list.add(datetime2); + list.add(datetimeoffset); + list.add(time); + list.add(datetime); + list.add(smalldatetime); + + return list; + } + + /** + * Create column master key + * + * @param keyStoreName + * @param keyPath + * @throws SQLException + */ + private static void createCMK(String keyStoreName, + String keyPath) throws SQLException { + String sql = " if not exists (SELECT name from sys.column_master_keys where name='" + cmkName + "')" + " begin" + " CREATE COLUMN MASTER KEY " + + cmkName + " WITH (KEY_STORE_PROVIDER_NAME = '" + keyStoreName + "', KEY_PATH = '" + keyPath + "')" + " end"; + stmt.execute(sql); + } + + /** + * Create column encryption key + * + * @param storeProvider + * @param certStore + * @throws SQLException + */ + private static void createCEK(SQLServerColumnEncryptionKeyStoreProvider storeProvider) throws SQLException { + String letters = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + byte[] valuesDefault = letters.getBytes(); + String cekSql = null; + byte[] key = storeProvider.encryptColumnEncryptionKey(javaKeyAliases, "RSA_OAEP", valuesDefault); + cekSql = "CREATE COLUMN ENCRYPTION KEY " + cekName + " WITH VALUES " + "(COLUMN_MASTER_KEY = " + cmkName + + ", ALGORITHM = 'RSA_OAEP', ENCRYPTED_VALUE = 0x" + DatatypeConverter.printHexBinary(key) + ")" + ";"; + stmt.execute(cekSql); + } + + /** + * Drop all tables that are in use by AE + * + * @throws SQLException + */ + protected static void dropTables(SQLServerStatement statement) throws SQLException { + Utils.dropTableIfExists(numericTable, statement); + Utils.dropTableIfExists(charTable, statement); + Utils.dropTableIfExists(binaryTable, statement); + Utils.dropTableIfExists(dateTable, statement); + } + + /** + * Populate binary data. + * + * @param byteValues + * @throws SQLException + */ + protected static void populateBinaryNormalCase(LinkedList byteValues) throws SQLException { + String sql = "insert into " + binaryTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // binary20 + for (int i = 1; i <= 3; i++) { + if (null == byteValues) { + pstmt.setBytes(i, null); + } + else { + pstmt.setBytes(i, byteValues.get(0)); + } + } + + // varbinary50 + for (int i = 4; i <= 6; i++) { + if (null == byteValues) { + pstmt.setBytes(i, null); + } + else { + pstmt.setBytes(i, byteValues.get(1)); + } + } + + // varbinary(max) + for (int i = 7; i <= 9; i++) { + if (null == byteValues) { + pstmt.setBytes(i, null); + } + else { + pstmt.setBytes(i, byteValues.get(2)); + } + } + + // binary(512) + for (int i = 10; i <= 12; i++) { + if (null == byteValues) { + pstmt.setBytes(i, null); + } + else { + pstmt.setBytes(i, byteValues.get(3)); + } + } + + // varbinary(8000) + for (int i = 13; i <= 15; i++) { + if (null == byteValues) { + pstmt.setBytes(i, null); + } + else { + pstmt.setBytes(i, byteValues.get(4)); + } + } + + pstmt.execute(); + } + } + + /** + * Populate binary data using set object. + * + * @param byteValues + * @throws SQLException + */ + protected static void populateBinarySetObject(LinkedList byteValues) throws SQLException { + String sql = "insert into " + binaryTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // binary(20) + for (int i = 1; i <= 3; i++) { + if (null == byteValues) { + pstmt.setObject(i, null, java.sql.Types.BINARY); + } + else { + pstmt.setObject(i, byteValues.get(0)); + } + } + + // varbinary(50) + for (int i = 4; i <= 6; i++) { + if (null == byteValues) { + pstmt.setObject(i, null, java.sql.Types.BINARY); + } + else { + pstmt.setObject(i, byteValues.get(1)); + } + } + + // varbinary(max) + for (int i = 7; i <= 9; i++) { + if (null == byteValues) { + pstmt.setObject(i, null, java.sql.Types.BINARY); + } + else { + pstmt.setObject(i, byteValues.get(2)); + } + } + + // binary(512) + for (int i = 10; i <= 12; i++) { + if (null == byteValues) { + pstmt.setObject(i, null, java.sql.Types.BINARY); + } + else { + pstmt.setObject(i, byteValues.get(3)); + } + } + + // varbinary(8000) + for (int i = 13; i <= 15; i++) { + if (null == byteValues) { + pstmt.setObject(i, null, java.sql.Types.BINARY); + } + else { + pstmt.setObject(i, byteValues.get(4)); + } + } + + pstmt.execute(); + } + } + + /** + * Populate binary data using set object with JDBC type. + * + * @param byteValues + * @throws SQLException + */ + protected static void populateBinarySetObjectWithJDBCType(LinkedList byteValues) throws SQLException { + String sql = "insert into " + binaryTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // binary(20) + for (int i = 1; i <= 3; i++) { + if (null == byteValues) { + pstmt.setObject(i, null, JDBCType.BINARY); + } + else { + pstmt.setObject(i, byteValues.get(0), JDBCType.BINARY); + } + } + + // varbinary(50) + for (int i = 4; i <= 6; i++) { + if (null == byteValues) { + pstmt.setObject(i, null, JDBCType.VARBINARY); + } + else { + pstmt.setObject(i, byteValues.get(1), JDBCType.VARBINARY); + } + } + + // varbinary(max) + for (int i = 7; i <= 9; i++) { + if (null == byteValues) { + pstmt.setObject(i, null, JDBCType.VARBINARY); + } + else { + pstmt.setObject(i, byteValues.get(2), JDBCType.VARBINARY); + } + } + + // binary(512) + for (int i = 10; i <= 12; i++) { + if (null == byteValues) { + pstmt.setObject(i, null, JDBCType.BINARY); + } + else { + pstmt.setObject(i, byteValues.get(3), JDBCType.BINARY); + } + } + + // varbinary(8000) + for (int i = 13; i <= 15; i++) { + if (null == byteValues) { + pstmt.setObject(i, null, JDBCType.VARBINARY); + } + else { + pstmt.setObject(i, byteValues.get(4), JDBCType.VARBINARY); + } + } + + pstmt.execute(); + } + } + + /** + * Populate binary data using set null. + * + * @throws SQLException + */ + protected static void populateBinaryNullCase() throws SQLException { + String sql = "insert into " + binaryTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // binary + for (int i = 1; i <= 3; i++) { + pstmt.setNull(i, java.sql.Types.BINARY); + } + + // varbinary, varbinary(max) + for (int i = 4; i <= 9; i++) { + pstmt.setNull(i, java.sql.Types.VARBINARY); + } + + // binary512 + for (int i = 10; i <= 12; i++) { + pstmt.setNull(i, java.sql.Types.BINARY); + } + + // varbinary(8000) + for (int i = 13; i <= 15; i++) { + pstmt.setNull(i, java.sql.Types.VARBINARY); + } + + pstmt.execute(); + } + } + + /** + * Populate char data. + * + * @param charValues + * @throws SQLException + */ + protected static void populateCharNormalCase(String[] charValues) throws SQLException { + String sql = "insert into " + charTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // char + for (int i = 1; i <= 3; i++) { + pstmt.setString(i, charValues[0]); + } + + // varchar + for (int i = 4; i <= 6; i++) { + pstmt.setString(i, charValues[1]); + } + + // varchar(max) + for (int i = 7; i <= 9; i++) { + pstmt.setString(i, charValues[2]); + } + + // nchar + for (int i = 10; i <= 12; i++) { + pstmt.setNString(i, charValues[3]); + } + + // nvarchar + for (int i = 13; i <= 15; i++) { + pstmt.setNString(i, charValues[4]); + } + + // varchar(max) + for (int i = 16; i <= 18; i++) { + pstmt.setNString(i, charValues[5]); + } + + // uniqueidentifier + for (int i = 19; i <= 21; i++) { + if (null == charValues[6]) { + pstmt.setUniqueIdentifier(i, null); + } + else { + pstmt.setUniqueIdentifier(i, uid); + } + } + + // varchar8000 + for (int i = 22; i <= 24; i++) { + pstmt.setString(i, charValues[7]); + } + + // nvarchar4000 + for (int i = 25; i <= 27; i++) { + pstmt.setNString(i, charValues[8]); + } + + pstmt.execute(); + } + } + + /** + * Populate char data using set object. + * + * @param charValues + * @throws SQLException + */ + protected static void populateCharSetObject(String[] charValues) throws SQLException { + String sql = "insert into " + charTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // char + for (int i = 1; i <= 3; i++) { + pstmt.setObject(i, charValues[0]); + } + + // varchar + for (int i = 4; i <= 6; i++) { + pstmt.setObject(i, charValues[1]); + } + + // varchar(max) + for (int i = 7; i <= 9; i++) { + pstmt.setObject(i, charValues[2], java.sql.Types.LONGVARCHAR); + } + + // nchar + for (int i = 10; i <= 12; i++) { + pstmt.setObject(i, charValues[3], java.sql.Types.NCHAR); + } + + // nvarchar + for (int i = 13; i <= 15; i++) { + pstmt.setObject(i, charValues[4], java.sql.Types.NCHAR); + } + + // nvarchar(max) + for (int i = 16; i <= 18; i++) { + pstmt.setObject(i, charValues[5], java.sql.Types.LONGNVARCHAR); + } + + // uniqueidentifier + for (int i = 19; i <= 21; i++) { + pstmt.setObject(i, charValues[6], microsoft.sql.Types.GUID); + } + + // varchar8000 + for (int i = 22; i <= 24; i++) { + pstmt.setObject(i, charValues[7]); + } + + // nvarchar4000 + for (int i = 25; i <= 27; i++) { + pstmt.setObject(i, charValues[8], java.sql.Types.NCHAR); + } + + pstmt.execute(); + } + } + + /** + * Populate char data using set object with JDBC types. + * + * @param charValues + * @throws SQLException + */ + protected static void populateCharSetObjectWithJDBCTypes(String[] charValues) throws SQLException { + String sql = "insert into " + charTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // char + for (int i = 1; i <= 3; i++) { + pstmt.setObject(i, charValues[0], JDBCType.CHAR); + } + + // varchar + for (int i = 4; i <= 6; i++) { + pstmt.setObject(i, charValues[1], JDBCType.VARCHAR); + } + + // varchar(max) + for (int i = 7; i <= 9; i++) { + pstmt.setObject(i, charValues[2], JDBCType.LONGVARCHAR); + } + + // nchar + for (int i = 10; i <= 12; i++) { + pstmt.setObject(i, charValues[3], JDBCType.NCHAR); + } + + // nvarchar + for (int i = 13; i <= 15; i++) { + pstmt.setObject(i, charValues[4], JDBCType.NVARCHAR); + } + + // nvarchar(max) + for (int i = 16; i <= 18; i++) { + pstmt.setObject(i, charValues[5], JDBCType.LONGNVARCHAR); + } + + // uniqueidentifier + for (int i = 19; i <= 21; i++) { + pstmt.setObject(i, charValues[6], microsoft.sql.Types.GUID); + } + + // varchar8000 + for (int i = 22; i <= 24; i++) { + pstmt.setObject(i, charValues[7], JDBCType.VARCHAR); + } + + // vnarchar4000 + for (int i = 25; i <= 27; i++) { + pstmt.setObject(i, charValues[8], JDBCType.NVARCHAR); + } + + pstmt.execute(); + } + } + + /** + * Populate char data with set null. + * + * @throws SQLException + */ + protected static void populateCharNullCase() throws SQLException { + String sql = "insert into " + charTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // char + for (int i = 1; i <= 3; i++) { + pstmt.setNull(i, java.sql.Types.CHAR); + } + + // varchar, varchar(max) + for (int i = 4; i <= 9; i++) { + pstmt.setNull(i, java.sql.Types.VARCHAR); + } + + // nchar + for (int i = 10; i <= 12; i++) { + pstmt.setNull(i, java.sql.Types.NCHAR); + } + + // nvarchar, varchar(max) + for (int i = 13; i <= 18; i++) { + pstmt.setNull(i, java.sql.Types.NVARCHAR); + } + + // uniqueidentifier + for (int i = 19; i <= 21; i++) { + pstmt.setNull(i, microsoft.sql.Types.GUID); + + } + + // varchar8000 + for (int i = 22; i <= 24; i++) { + pstmt.setNull(i, java.sql.Types.VARCHAR); + } + + // nvarchar4000 + for (int i = 25; i <= 27; i++) { + pstmt.setNull(i, java.sql.Types.NVARCHAR); + } + + pstmt.execute(); + } + } + + /** + * Populate date data. + * + * @param dateValues + * @throws SQLException + */ + protected static void populateDateNormalCase(LinkedList dateValues) throws SQLException { + String sql = "insert into " + dateTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // date + for (int i = 1; i <= 3; i++) { + pstmt.setDate(i, (Date) dateValues.get(0)); + } + + // datetime2 default + for (int i = 4; i <= 6; i++) { + pstmt.setTimestamp(i, (Timestamp) dateValues.get(1)); + } + + // datetimeoffset default + for (int i = 7; i <= 9; i++) { + pstmt.setDateTimeOffset(i, (DateTimeOffset) dateValues.get(2)); + } + + // time default + for (int i = 10; i <= 12; i++) { + pstmt.setTime(i, (Time) dateValues.get(3)); + } + + // datetime + for (int i = 13; i <= 15; i++) { + pstmt.setDateTime(i, (Timestamp) dateValues.get(4)); + } + + // smalldatetime + for (int i = 16; i <= 18; i++) { + pstmt.setSmallDateTime(i, (Timestamp) dateValues.get(5)); + } + + pstmt.execute(); + } + } + + /** + * Populate date data with scale. + * + * @param dateValues + * @throws SQLException + */ + protected static void populateDateScaleNormalCase(LinkedList dateValues) throws SQLException { + String sql = "insert into " + scaleDateTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // datetime2(2) + for (int i = 1; i <= 3; i++) { + pstmt.setTimestamp(i, (Timestamp) dateValues.get(4), 2); + } + + // time(2) + for (int i = 4; i <= 6; i++) { + pstmt.setTime(i, (Time) dateValues.get(5), 2); + } + + // datetimeoffset(2) + for (int i = 7; i <= 9; i++) { + pstmt.setDateTimeOffset(i, (DateTimeOffset) dateValues.get(6), 2); + } + + pstmt.execute(); + } + } + + /** + * Populate date data using set object. + * + * @param dateValues + * @param setter + * @throws SQLException + */ + protected static void populateDateSetObject(LinkedList dateValues, + String setter) throws SQLException { + if (setter.equalsIgnoreCase("setwithJDBCType")) { + skipTestForJava7(); + } + + String sql = "insert into " + dateTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // date + for (int i = 1; i <= 3; i++) { + if (setter.equalsIgnoreCase("setwithJavaType")) + pstmt.setObject(i, (Date) dateValues.get(0), java.sql.Types.DATE); + else if (setter.equalsIgnoreCase("setwithJDBCType")) + pstmt.setObject(i, (Date) dateValues.get(0), JDBCType.DATE); + else + pstmt.setObject(i, (Date) dateValues.get(0)); + } + + // datetime2 default + for (int i = 4; i <= 6; i++) { + if (setter.equalsIgnoreCase("setwithJavaType")) + pstmt.setObject(i, (Timestamp) dateValues.get(1), java.sql.Types.TIMESTAMP); + else if (setter.equalsIgnoreCase("setwithJDBCType")) + pstmt.setObject(i, (Timestamp) dateValues.get(1), JDBCType.TIMESTAMP); + else + pstmt.setObject(i, (Timestamp) dateValues.get(1)); + } + + // datetimeoffset default + for (int i = 7; i <= 9; i++) { + if (setter.equalsIgnoreCase("setwithJavaType")) + pstmt.setObject(i, (DateTimeOffset) dateValues.get(2), microsoft.sql.Types.DATETIMEOFFSET); + else if (setter.equalsIgnoreCase("setwithJDBCType")) + pstmt.setObject(i, (DateTimeOffset) dateValues.get(2), microsoft.sql.Types.DATETIMEOFFSET); + else + pstmt.setObject(i, (DateTimeOffset) dateValues.get(2)); + } + + // time default + for (int i = 10; i <= 12; i++) { + if (setter.equalsIgnoreCase("setwithJavaType")) + pstmt.setObject(i, (Time) dateValues.get(3), java.sql.Types.TIME); + else if (setter.equalsIgnoreCase("setwithJDBCType")) + pstmt.setObject(i, (Time) dateValues.get(3), JDBCType.TIME); + else + pstmt.setObject(i, (Time) dateValues.get(3)); + } + + // datetime + for (int i = 13; i <= 15; i++) { + pstmt.setObject(i, (Timestamp) dateValues.get(4), microsoft.sql.Types.DATETIME); + } + + // smalldatetime + for (int i = 16; i <= 18; i++) { + pstmt.setObject(i, (Timestamp) dateValues.get(5), microsoft.sql.Types.SMALLDATETIME); + } + + pstmt.execute(); + } + } + + /** + * Populate date data with null data. + * + * @throws SQLException + */ + protected void populateDateSetObjectNull() throws SQLException { + String sql = "insert into " + dateTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // date + for (int i = 1; i <= 3; i++) { + pstmt.setObject(i, null, java.sql.Types.DATE); + } + + // datetime2 default + for (int i = 4; i <= 6; i++) { + pstmt.setObject(i, null, java.sql.Types.TIMESTAMP); + } + + // datetimeoffset default + for (int i = 7; i <= 9; i++) { + pstmt.setObject(i, null, microsoft.sql.Types.DATETIMEOFFSET); + } + + // time default + for (int i = 10; i <= 12; i++) { + pstmt.setObject(i, null, java.sql.Types.TIME); + } + + // datetime + for (int i = 13; i <= 15; i++) { + pstmt.setObject(i, null, microsoft.sql.Types.DATETIME); + } + + // smalldatetime + for (int i = 16; i <= 18; i++) { + pstmt.setObject(i, null, microsoft.sql.Types.SMALLDATETIME); + } + + pstmt.execute(); + } + } + + /** + * Populate date data with set null. + * + * @throws SQLException + */ + protected static void populateDateNullCase() throws SQLException { + String sql = "insert into " + dateTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // date + for (int i = 1; i <= 3; i++) { + pstmt.setNull(i, java.sql.Types.DATE); + } + + // datetime2 default + for (int i = 4; i <= 6; i++) { + pstmt.setNull(i, java.sql.Types.TIMESTAMP); + } + + // datetimeoffset default + for (int i = 7; i <= 9; i++) { + pstmt.setNull(i, microsoft.sql.Types.DATETIMEOFFSET); + } + + // time default + for (int i = 10; i <= 12; i++) { + pstmt.setNull(i, java.sql.Types.TIME); + } + + // datetime + for (int i = 13; i <= 15; i++) { + pstmt.setNull(i, microsoft.sql.Types.DATETIME); + } + + // smalldatetime + for (int i = 16; i <= 18; i++) { + pstmt.setNull(i, microsoft.sql.Types.SMALLDATETIME); + } + + pstmt.execute(); + } + } + + /** + * Populating the table + * + * @param values + * @throws SQLException + */ + protected static void populateNumeric(String[] values) throws SQLException { + String sql = "insert into " + numericTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // bit + for (int i = 1; i <= 3; i++) { + if (values[0].equalsIgnoreCase("true")) { + pstmt.setBoolean(i, true); + } + else { + pstmt.setBoolean(i, false); + } + } + + // tinyint + for (int i = 4; i <= 6; i++) { + pstmt.setShort(i, Short.valueOf(values[1])); + } + + // smallint + for (int i = 7; i <= 9; i++) { + pstmt.setShort(i, Short.valueOf(values[2])); + } + + // int + for (int i = 10; i <= 12; i++) { + pstmt.setInt(i, Integer.valueOf(values[3])); + } + + // bigint + for (int i = 13; i <= 15; i++) { + pstmt.setLong(i, Long.valueOf(values[4])); + } + + // float default + for (int i = 16; i <= 18; i++) { + pstmt.setDouble(i, Double.valueOf(values[5])); + } + + // float(30) + for (int i = 19; i <= 21; i++) { + pstmt.setDouble(i, Double.valueOf(values[6])); + } + + // real + for (int i = 22; i <= 24; i++) { + pstmt.setFloat(i, Float.valueOf(values[7])); + } + + // decimal default + for (int i = 25; i <= 27; i++) { + if (values[8].equalsIgnoreCase("0")) + pstmt.setBigDecimal(i, new BigDecimal(values[8]), 18, 0); + else + pstmt.setBigDecimal(i, new BigDecimal(values[8])); + } + + // decimal(10,5) + for (int i = 28; i <= 30; i++) { + pstmt.setBigDecimal(i, new BigDecimal(values[9]), 10, 5); + } + + // numeric + for (int i = 31; i <= 33; i++) { + if (values[10].equalsIgnoreCase("0")) + pstmt.setBigDecimal(i, new BigDecimal(values[10]), 18, 0); + else + pstmt.setBigDecimal(i, new BigDecimal(values[10])); + } + + // numeric(8,2) + for (int i = 34; i <= 36; i++) { + pstmt.setBigDecimal(i, new BigDecimal(values[11]), 8, 2); + } + + // small money + for (int i = 37; i <= 39; i++) { + pstmt.setSmallMoney(i, new BigDecimal(values[12])); + } + + // money + for (int i = 40; i <= 42; i++) { + pstmt.setMoney(i, new BigDecimal(values[13])); + } + + // decimal(28,4) + for (int i = 43; i <= 45; i++) { + pstmt.setBigDecimal(i, new BigDecimal(values[14]), 28, 4); + } + + // numeric(28,4) + for (int i = 46; i <= 48; i++) { + pstmt.setBigDecimal(i, new BigDecimal(values[15]), 28, 4); + } + + pstmt.execute(); + } + } + + /** + * Populate numeric data with set object. + * + * @param values + * @throws SQLException + */ + protected static void populateNumericSetObject(String[] values) throws SQLException { + String sql = "insert into " + numericTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // bit + for (int i = 1; i <= 3; i++) { + if (values[0].equalsIgnoreCase("true")) { + pstmt.setObject(i, true); + } + else { + pstmt.setObject(i, false); + } + } + + // tinyint + for (int i = 4; i <= 6; i++) { + pstmt.setObject(i, Short.valueOf(values[1])); + } + + // smallint + for (int i = 7; i <= 9; i++) { + pstmt.setObject(i, Short.valueOf(values[2])); + } + + // int + for (int i = 10; i <= 12; i++) { + pstmt.setObject(i, Integer.valueOf(values[3])); + } + + // bigint + for (int i = 13; i <= 15; i++) { + pstmt.setObject(i, Long.valueOf(values[4])); + } + + // float default + for (int i = 16; i <= 18; i++) { + pstmt.setObject(i, Double.valueOf(values[5])); + } + + // float(30) + for (int i = 19; i <= 21; i++) { + pstmt.setObject(i, Double.valueOf(values[6])); + } + + // real + for (int i = 22; i <= 24; i++) { + pstmt.setObject(i, Float.valueOf(values[7])); + } + + // decimal default + for (int i = 25; i <= 27; i++) { + if (RandomData.returnZero) + pstmt.setObject(i, new BigDecimal(values[8]), java.sql.Types.DECIMAL, 18, 0); + else + pstmt.setObject(i, new BigDecimal(values[8])); + } + + // decimal(10,5) + for (int i = 28; i <= 30; i++) { + pstmt.setObject(i, new BigDecimal(values[9]), java.sql.Types.DECIMAL, 10, 5); + } + + // numeric + for (int i = 31; i <= 33; i++) { + if (RandomData.returnZero) + pstmt.setObject(i, new BigDecimal(values[10]), java.sql.Types.NUMERIC, 18, 0); + else + pstmt.setObject(i, new BigDecimal(values[10])); + } + + // numeric(8,2) + for (int i = 34; i <= 36; i++) { + pstmt.setObject(i, new BigDecimal(values[11]), java.sql.Types.NUMERIC, 8, 2); + } + + // small money + for (int i = 37; i <= 39; i++) { + pstmt.setObject(i, new BigDecimal(values[12]), microsoft.sql.Types.SMALLMONEY); + } + + // money + for (int i = 40; i <= 42; i++) { + pstmt.setObject(i, new BigDecimal(values[13]), microsoft.sql.Types.MONEY); + } + + // decimal(28,4) + for (int i = 43; i <= 45; i++) { + pstmt.setObject(i, new BigDecimal(values[14]), java.sql.Types.DECIMAL, 28, 4); + } + + // numeric + for (int i = 46; i <= 48; i++) { + pstmt.setObject(i, new BigDecimal(values[15]), java.sql.Types.NUMERIC, 28, 4); + } + + pstmt.execute(); + } + } + + /** + * Populate numeric data with set object with JDBC type. + * + * @param values + * @throws SQLException + */ + protected static void populateNumericSetObjectWithJDBCTypes(String[] values) throws SQLException { + String sql = "insert into " + numericTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // bit + for (int i = 1; i <= 3; i++) { + if (values[0].equalsIgnoreCase("true")) { + pstmt.setObject(i, true); + } + else { + pstmt.setObject(i, false); + } + } + + // tinyint + for (int i = 4; i <= 6; i++) { + pstmt.setObject(i, Short.valueOf(values[1]), JDBCType.TINYINT); + } + + // smallint + for (int i = 7; i <= 9; i++) { + pstmt.setObject(i, Short.valueOf(values[2]), JDBCType.SMALLINT); + } + + // int + for (int i = 10; i <= 12; i++) { + pstmt.setObject(i, Integer.valueOf(values[3]), JDBCType.INTEGER); + } + + // bigint + for (int i = 13; i <= 15; i++) { + pstmt.setObject(i, Long.valueOf(values[4]), JDBCType.BIGINT); + } + + // float default + for (int i = 16; i <= 18; i++) { + pstmt.setObject(i, Double.valueOf(values[5]), JDBCType.DOUBLE); + } + + // float(30) + for (int i = 19; i <= 21; i++) { + pstmt.setObject(i, Double.valueOf(values[6]), JDBCType.DOUBLE); + } + + // real + for (int i = 22; i <= 24; i++) { + pstmt.setObject(i, Float.valueOf(values[7]), JDBCType.REAL); + } + + // decimal default + for (int i = 25; i <= 27; i++) { + if (RandomData.returnZero) + pstmt.setObject(i, new BigDecimal(values[8]), java.sql.Types.DECIMAL, 18, 0); + else + pstmt.setObject(i, new BigDecimal(values[8])); + } + + // decimal(10,5) + for (int i = 28; i <= 30; i++) { + pstmt.setObject(i, new BigDecimal(values[9]), java.sql.Types.DECIMAL, 10, 5); + } + + // numeric + for (int i = 31; i <= 33; i++) { + if (RandomData.returnZero) + pstmt.setObject(i, new BigDecimal(values[10]), java.sql.Types.NUMERIC, 18, 0); + else + pstmt.setObject(i, new BigDecimal(values[10])); + } + + // numeric(8,2) + for (int i = 34; i <= 36; i++) { + pstmt.setObject(i, new BigDecimal(values[11]), java.sql.Types.NUMERIC, 8, 2); + } + + // small money + for (int i = 37; i <= 39; i++) { + pstmt.setObject(i, new BigDecimal(values[12]), microsoft.sql.Types.SMALLMONEY); + } + + // money + for (int i = 40; i <= 42; i++) { + pstmt.setObject(i, new BigDecimal(values[13]), microsoft.sql.Types.MONEY); + } + + // decimal(28,4) + for (int i = 43; i <= 45; i++) { + pstmt.setObject(i, new BigDecimal(values[14]), java.sql.Types.DECIMAL, 28, 4); + } + + // numeric + for (int i = 46; i <= 48; i++) { + pstmt.setObject(i, new BigDecimal(values[15]), java.sql.Types.NUMERIC, 28, 4); + } + + pstmt.execute(); + } + } + + /** + * Populate numeric data with set object using null data. + * + * @throws SQLException + */ + protected static void populateNumericSetObjectNull() throws SQLException { + String sql = "insert into " + numericTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // bit + for (int i = 1; i <= 3; i++) { + pstmt.setObject(i, null, java.sql.Types.BIT); + } + + // tinyint + for (int i = 4; i <= 6; i++) { + pstmt.setObject(i, null, java.sql.Types.TINYINT); + } + + // smallint + for (int i = 7; i <= 9; i++) { + pstmt.setObject(i, null, java.sql.Types.SMALLINT); + } + + // int + for (int i = 10; i <= 12; i++) { + pstmt.setObject(i, null, java.sql.Types.INTEGER); + } + + // bigint + for (int i = 13; i <= 15; i++) { + pstmt.setObject(i, null, java.sql.Types.BIGINT); + } + + // float default + for (int i = 16; i <= 18; i++) { + pstmt.setObject(i, null, java.sql.Types.DOUBLE); + } + + // float(30) + for (int i = 19; i <= 21; i++) { + pstmt.setObject(i, null, java.sql.Types.DOUBLE); + } + + // real + for (int i = 22; i <= 24; i++) { + pstmt.setObject(i, null, java.sql.Types.REAL); + } + + // decimal default + for (int i = 25; i <= 27; i++) { + pstmt.setObject(i, null, java.sql.Types.DECIMAL); + } + + // decimal(10,5) + for (int i = 28; i <= 30; i++) { + pstmt.setObject(i, null, java.sql.Types.DECIMAL, 10, 5); + } + + // numeric + for (int i = 31; i <= 33; i++) { + pstmt.setObject(i, null, java.sql.Types.NUMERIC); + } + + // numeric(8,2) + for (int i = 34; i <= 36; i++) { + pstmt.setObject(i, null, java.sql.Types.NUMERIC, 8, 2); + } + + // small money + for (int i = 37; i <= 39; i++) { + pstmt.setObject(i, null, microsoft.sql.Types.SMALLMONEY); + } + + // money + for (int i = 40; i <= 42; i++) { + pstmt.setObject(i, null, microsoft.sql.Types.MONEY); + } + + // decimal(28,4) + for (int i = 43; i <= 45; i++) { + pstmt.setObject(i, null, java.sql.Types.DECIMAL, 28, 4); + } + + // numeric + for (int i = 46; i <= 48; i++) { + pstmt.setObject(i, null, java.sql.Types.NUMERIC, 28, 4); + } + + pstmt.execute(); + } + } + + /** + * Populate numeric data with set null. + * + * @param values + * @throws SQLException + */ + protected static void populateNumericNullCase(String[] values) throws SQLException { + String sql = "insert into " + numericTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + + + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // bit + for (int i = 1; i <= 3; i++) { + pstmt.setNull(i, java.sql.Types.BIT); + } + + // tinyint + for (int i = 4; i <= 6; i++) { + pstmt.setNull(i, java.sql.Types.TINYINT); + } + + // smallint + for (int i = 7; i <= 9; i++) { + pstmt.setNull(i, java.sql.Types.SMALLINT); + } + + // int + for (int i = 10; i <= 12; i++) { + pstmt.setNull(i, java.sql.Types.INTEGER); + } + + // bigint + for (int i = 13; i <= 15; i++) { + pstmt.setNull(i, java.sql.Types.BIGINT); + } + + // float default + for (int i = 16; i <= 18; i++) { + pstmt.setNull(i, java.sql.Types.DOUBLE); + } + + // float(30) + for (int i = 19; i <= 21; i++) { + pstmt.setNull(i, java.sql.Types.DOUBLE); + } + + // real + for (int i = 22; i <= 24; i++) { + pstmt.setNull(i, java.sql.Types.REAL); + } + + // decimal default + for (int i = 25; i <= 27; i++) { + pstmt.setBigDecimal(i, null); + } + + // decimal(10,5) + for (int i = 28; i <= 30; i++) { + pstmt.setBigDecimal(i, null, 10, 5); + } + + // numeric + for (int i = 31; i <= 33; i++) { + pstmt.setBigDecimal(i, null); + } + + // numeric(8,2) + for (int i = 34; i <= 36; i++) { + pstmt.setBigDecimal(i, null, 8, 2); + } + + // small money + for (int i = 37; i <= 39; i++) { + pstmt.setSmallMoney(i, null); + } + + // money + for (int i = 40; i <= 42; i++) { + pstmt.setMoney(i, null); + } + + // decimal(28,4) + for (int i = 43; i <= 45; i++) { + pstmt.setBigDecimal(i, null, 28, 4); + } + + // decimal(28,4) + for (int i = 46; i <= 48; i++) { + pstmt.setBigDecimal(i, null, 28, 4); + } + pstmt.execute(); + } + } + + /** + * Populate numeric data. + * + * @param numericValues + * @throws SQLException + */ + protected static void populateNumericNormalCase(String[] numericValues) throws SQLException { + String sql = "insert into " + numericTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + + + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // bit + for (int i = 1; i <= 3; i++) { + if (numericValues[0].equalsIgnoreCase("true")) { + pstmt.setBoolean(i, true); + } + else { + pstmt.setBoolean(i, false); + } + } + + // tinyint + for (int i = 4; i <= 6; i++) { + if (1 == Integer.valueOf(numericValues[1])) { + pstmt.setBoolean(i, true); + } + else { + pstmt.setBoolean(i, false); + } + } + + // smallint + for (int i = 7; i <= 9; i++) { + if (numericValues[2].equalsIgnoreCase("255")) { + pstmt.setByte(i, (byte) 255); + } + else { + pstmt.setByte(i, Byte.valueOf(numericValues[2])); + } + } + + // int + for (int i = 10; i <= 12; i++) { + pstmt.setShort(i, Short.valueOf(numericValues[3])); + } + + // bigint + for (int i = 13; i <= 15; i++) { + pstmt.setInt(i, Integer.valueOf(numericValues[4])); + } + + // float default + for (int i = 16; i <= 18; i++) { + pstmt.setDouble(i, Double.valueOf(numericValues[5])); + } + + // float(30) + for (int i = 19; i <= 21; i++) { + pstmt.setDouble(i, Double.valueOf(numericValues[6])); + } + + // real + for (int i = 22; i <= 24; i++) { + pstmt.setFloat(i, Float.valueOf(numericValues[7])); + } + + // decimal default + for (int i = 25; i <= 27; i++) { + pstmt.setBigDecimal(i, new BigDecimal(numericValues[8])); + } + + // decimal(10,5) + for (int i = 28; i <= 30; i++) { + pstmt.setBigDecimal(i, new BigDecimal(numericValues[9]), 10, 5); + } + + // numeric + for (int i = 31; i <= 33; i++) { + pstmt.setBigDecimal(i, new BigDecimal(numericValues[10])); + } + + // numeric(8,2) + for (int i = 34; i <= 36; i++) { + pstmt.setBigDecimal(i, new BigDecimal(numericValues[11]), 8, 2); + } + + // small money + for (int i = 37; i <= 39; i++) { + pstmt.setSmallMoney(i, new BigDecimal(numericValues[12])); + } + + // money + for (int i = 40; i <= 42; i++) { + pstmt.setSmallMoney(i, new BigDecimal(numericValues[13])); + } + + // decimal(28,4) + for (int i = 43; i <= 45; i++) { + pstmt.setBigDecimal(i, new BigDecimal(numericValues[14]), 28, 4); + } + + // numeric + for (int i = 46; i <= 48; i++) { + pstmt.setBigDecimal(i, new BigDecimal(numericValues[15]), 28, 4); + } + + pstmt.execute(); + } + } + + /** + * Dropping column encryption key + * + * @throws SQLServerException + * @throws SQLException + */ + private static void dropCEK(SQLServerStatement stmt) throws SQLServerException, SQLException { + String cekSql = " if exists (SELECT name from sys.column_encryption_keys where name='" + cekName + "')" + " begin" + + " drop column encryption key " + cekName + " end"; + stmt.execute(cekSql); + } + + /** + * Dropping column master key + * + * @throws SQLServerException + * @throws SQLException + */ + private static void dropCMK(SQLServerStatement stmt) throws SQLServerException, SQLException { + String cekSql = " if exists (SELECT name from sys.column_master_keys where name='" + cmkName + "')" + " begin" + " drop column master key " + + cmkName + " end"; + stmt.execute(cekSql); + } + + /** + * Skip test if the client is using Java 7 or does not support JDBC 4.2. + * + * @throws SQLException + * @throws TestAbortedException + */ + protected static void skipTestForJava7() throws TestAbortedException, SQLException { + assumeTrue(Util.supportJDBC42(con)); // With Java 7, skip tests for JDBCType. + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/CallableStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/CallableStatementTest.java new file mode 100644 index 000000000..cc6a67e29 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/CallableStatementTest.java @@ -0,0 +1,2464 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.AlwaysEncrypted; + +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import java.math.BigDecimal; +import java.sql.Date; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.LinkedList; + +import com.microsoft.sqlserver.jdbc.SQLServerCallableStatement; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.jdbc.SQLServerResultSet; +import com.microsoft.sqlserver.testframework.Utils; +import com.microsoft.sqlserver.testframework.util.RandomData; +import com.microsoft.sqlserver.testframework.util.Util; + +import microsoft.sql.DateTimeOffset; + +/** + * Test cases related to SQLServerCallableStatement. + * + */ +@RunWith(JUnitPlatform.class) +public class CallableStatementTest extends AESetup { + + private static String multiStatementsProcedure = "multiStatementsProcedure"; + + private static String inputProcedure = "inputProcedure"; + private static String inputProcedure2 = "inputProcedure2"; + + private static String outputProcedure = "outputProcedure"; + private static String outputProcedure2 = "outputProcedure2"; + private static String outputProcedure3 = "outputProcedure3"; + private static String outputProcedureChar = "outputProcedureChar"; + private static String outputProcedureNumeric = "outputProcedureNumeric"; + private static String outputProcedureBinary = "outputProcedureBinary"; + private static String outputProcedureDate = "outputProcedureDate"; + private static String MixedProcedureDateScale = "outputProcedureDateScale"; + private static String outputProcedureBatch = "outputProcedureBatch"; + private static String outputProcedure4 = "outputProcedure4"; + + private static String inoutProcedure = "inoutProcedure"; + + private static String mixedProcedure = "mixedProcedure"; + private static String mixedProcedure2 = "mixedProcedure2"; + private static String mixedProcedure3 = "mixedProcedure3"; + private static String mixedProcedureNumericPrcisionScale = "mixedProcedureNumericPrcisionScale"; + + private static String table1 = "StoredProcedureTable1"; + private static String table2 = "StoredProcedureTable2"; + private static String table3 = "StoredProcedureTable3"; + private static String table4 = "StoredProcedureTable4"; + private static String table5 = "StoredProcedureTable5"; + private static String table6 = "StoredProcedureTable6"; + + static final String uid = "171fbe25-4331-4765-a838-b2e3eea3e7ea"; + + private static String[] numericValues; + private static LinkedList byteValues; + private static String[] charValues; + private static LinkedList dateValues; + private static boolean nullable = false; + + /** + * Initialize the tables for this class. This method will execute AFTER the parent class (AESetup) finishes initializing. + * + * @throws SQLServerException + * @throws SQLException + */ + @BeforeAll + public static void initCallableStatementTest() throws SQLException { + dropTables(); + + numericValues = createNumericValues(nullable); + byteValues = createbinaryValues(nullable); + dateValues = createTemporalTypesCallableStatement(nullable); + charValues = createCharValues(nullable); + + createTables(); + populateTable3(); + populateTable4(); + + createCharTable(); + createNumericTable(); + createBinaryTable(); + createDateTableCallableStatement(); + populateCharNormalCase(charValues); + populateNumericSetObject(numericValues); + populateBinaryNormalCase(byteValues); + populateDateNormalCase(); + + createDateScaleTable(); + populateDateScaleNormalCase(dateValues); + } + + @AfterAll + private static void dropAll() throws SQLServerException, SQLException { + dropTables(); + } + + @Test + public void testMultiInsertionSelection() throws SQLException { + createMultiInsertionSelection(); + MultiInsertionSelection(); + } + + @Test + public void testInputProcedureNumeric() throws SQLException { + createInputProcedure(); + testInputProcedure("{call " + inputProcedure + "(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)}", numericValues); + } + + @Test + public void testInputProcedureChar() throws SQLException { + createInputProcedure2(); + testInputProcedure2("{call " + inputProcedure2 + "(?,?,?,?,?,?,?,?)}"); + } + + @Test + public void testEncryptedOutputNumericParams() throws SQLException { + createOutputProcedure(); + testOutputProcedureRandomOrder("{call " + outputProcedure + "(?,?,?,?,?,?,?)}", numericValues); + testOutputProcedureInorder("{call " + outputProcedure + "(?,?,?,?,?,?,?)}", numericValues); + testOutputProcedureReverseOrder("{call " + outputProcedure + "(?,?,?,?,?,?,?)}", numericValues); + testOutputProcedureRandomOrder("exec " + outputProcedure + " ?,?,?,?,?,?,?", numericValues); + } + + @Test + public void testUnencryptedAndEncryptedNumericOutputParams() throws SQLException { + createOutputProcedure2(); + testOutputProcedure2RandomOrder("{call " + outputProcedure2 + "(?,?,?,?,?,?,?,?,?,?)}", numericValues); + testOutputProcedure2Inorder("{call " + outputProcedure2 + "(?,?,?,?,?,?,?,?,?,?)}", numericValues); + testOutputProcedure2ReverseOrder("{call " + outputProcedure2 + "(?,?,?,?,?,?,?,?,?,?)}", numericValues); + } + + @Test + public void testEncryptedOutputParamsFromDifferentTables() throws SQLException { + createOutputProcedure3(); + testOutputProcedure3RandomOrder("{call " + outputProcedure3 + "(?,?)}"); + testOutputProcedure3Inorder("{call " + outputProcedure3 + "(?,?)}"); + testOutputProcedure3ReverseOrder("{call " + outputProcedure3 + "(?,?)}"); + } + + @Test + public void testInOutProcedure() throws SQLException { + createInOutProcedure(); + testInOutProcedure("{call " + inoutProcedure + "(?)}"); + testInOutProcedure("exec " + inoutProcedure + " ?"); + } + + @Test + public void testMixedProcedure() throws SQLException { + createMixedProcedure(); + testMixedProcedure("{ ? = call " + mixedProcedure + "(?,?,?)}"); + } + + @Test + public void testUnencryptedAndEncryptedIOParams() throws SQLException { + // unencrypted input and output parameter + // encrypted input and output parameter + createMixedProcedure2(); + testMixedProcedure2RandomOrder("{call " + mixedProcedure2 + "(?,?,?,?)}"); + testMixedProcedure2Inorder("{call " + mixedProcedure2 + "(?,?,?,?)}"); + } + + @Test + public void testUnencryptedIOParams() throws SQLException { + createMixedProcedure3(); + testMixedProcedure3RandomOrder("{call " + mixedProcedure3 + "(?,?,?,?)}"); + testMixedProcedure3Inorder("{call " + mixedProcedure3 + "(?,?,?,?)}"); + testMixedProcedure3ReverseOrder("{call " + mixedProcedure3 + "(?,?,?,?)}"); + } + + @Test + public void testVariousIOParams() throws SQLException { + createMixedProcedureNumericPrcisionScale(); + testMixedProcedureNumericPrcisionScaleInorder("{call " + mixedProcedureNumericPrcisionScale + "(?,?,?,?)}"); + testMixedProcedureNumericPrcisionScaleParameterName("{call " + mixedProcedureNumericPrcisionScale + "(?,?,?,?)}"); + } + + @Test + public void testOutputProcedureChar() throws SQLException { + createOutputProcedureChar(); + testOutputProcedureCharInorder("{call " + outputProcedureChar + "(?,?,?,?,?,?,?,?,?)}"); + testOutputProcedureCharInorderObject("{call " + outputProcedureChar + "(?,?,?,?,?,?,?,?,?)}"); + } + + @Test + public void testOutputProcedureNumeric() throws SQLException { + createOutputProcedureNumeric(); + testOutputProcedureNumericInorder("{call " + outputProcedureNumeric + "(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)}"); + testcoerctionsOutputProcedureNumericInorder("{call " + outputProcedureNumeric + "(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)}"); + } + + @Test + public void testOutputProcedureBinary() throws SQLException { + createOutputProcedureBinary(); + testOutputProcedureBinaryInorder("{call " + outputProcedureBinary + "(?,?,?,?,?)}"); + testOutputProcedureBinaryInorderObject("{call " + outputProcedureBinary + "(?,?,?,?,?)}"); + testOutputProcedureBinaryInorderString("{call " + outputProcedureBinary + "(?,?,?,?,?)}"); + } + + @Test + public void testOutputProcedureDate() throws SQLException { + createOutputProcedureDate(); + testOutputProcedureDateInorder("{call " + outputProcedureDate + "(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)}"); + testOutputProcedureDateInorderObject("{call " + outputProcedureDate + "(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)}"); + } + + @Test + public void testMixedProcedureDateScale() throws SQLException { + createMixedProcedureDateScale(); + testMixedProcedureDateScaleInorder("{call " + MixedProcedureDateScale + "(?,?,?,?,?,?)}"); + testMixedProcedureDateScaleWithParameterName("{call " + MixedProcedureDateScale + "(?,?,?,?,?,?)}"); + } + + @Test + public void testOutputProcedureBatch() throws SQLException { + createOutputProcedureBatch(); + testOutputProcedureBatchInorder("{call " + outputProcedureBatch + "(?,?,?,?)}"); + } + + @Test + public void testOutputProcedure4() throws SQLException { + createOutputProcedure4(); + } + + private static void dropTables() throws SQLException { + Utils.dropTableIfExists(table1, stmt); + + Utils.dropTableIfExists(table2, stmt); + + Utils.dropTableIfExists(table3, stmt); + + Utils.dropTableIfExists(table4, stmt); + + Utils.dropTableIfExists(charTable, stmt); + + Utils.dropTableIfExists(numericTable, stmt); + + Utils.dropTableIfExists(binaryTable, stmt); + + Utils.dropTableIfExists(dateTable, stmt); + + Utils.dropTableIfExists(table5, stmt); + + Utils.dropTableIfExists(table6, stmt); + + Utils.dropTableIfExists(scaleDateTable, stmt); + } + + private static void createTables() throws SQLException { + String sql = "create table " + table1 + " (" + "PlainChar char(20) null," + + "RandomizedChar char(20) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicChar char(20) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainVarchar varchar(50) null," + + "RandomizedVarchar varchar(50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicVarchar varchar(50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL" + ");"; + + try { + stmt.execute(sql); + } + catch (SQLException e) { + fail(e.toString()); + } + + sql = "create table " + table2 + " (" + "PlainChar char(20) null," + + "RandomizedChar char(20) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicChar char(20) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainVarchar varchar(50) null," + + "RandomizedVarchar varchar(50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicVarchar varchar(50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL" + + + ");"; + + try { + stmt.execute(sql); + } + catch (SQLException e) { + fail(e.toString()); + } + + sql = "create table " + table3 + " (" + "PlainBit bit null," + + "RandomizedBit bit ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicBit bit ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainTinyint tinyint null," + + "RandomizedTinyint tinyint ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicTinyint tinyint ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainSmallint smallint null," + + "RandomizedSmallint smallint ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicSmallint smallint ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainInt int null," + + "RandomizedInt int ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicInt int ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainBigint bigint null," + + "RandomizedBigint bigint ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicBigint bigint ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainFloatDefault float null," + + "RandomizedFloatDefault float ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicFloatDefault float ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainFloat float(30) null," + + "RandomizedFloat float(30) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicFloat float(30) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainReal real null," + + "RandomizedReal real ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicReal real ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainDecimalDefault decimal(18,0) null," + + "RandomizedDecimalDefault decimal(18,0) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDecimalDefault decimal(18,0) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainDecimal decimal(10,5) null," + + "RandomizedDecimal decimal(10,5) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDecimal decimal(10,5) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainNumericDefault numeric(18,0) null," + + "RandomizedNumericDefault numeric(18,0) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicNumericDefault numeric(18,0) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainNumeric numeric(8,2) null," + + "RandomizedNumeric numeric(8,2) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicNumeric numeric(8,2) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainInt2 int null," + + "RandomizedInt2 int ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicInt2 int ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainSmallMoney smallmoney null," + + "RandomizedSmallMoney smallmoney ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicSmallMoney smallmoney ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainMoney money null," + + "RandomizedMoney money ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicMoney money ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainDecimal2 decimal(28,4) null," + + "RandomizedDecimal2 decimal(28,4) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDecimal2 decimal(28,4) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainNumeric2 numeric(28,4) null," + + "RandomizedNumeric2 numeric(28,4) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicNumeric2 numeric(28,4) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + ");"; + + try { + stmt.execute(sql); + } + catch (SQLException e) { + fail(e.toString()); + } + + sql = "create table " + table4 + " (" + "PlainInt int null," + + "RandomizedInt int ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicInt int ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + ");"; + + try { + stmt.execute(sql); + } + catch (SQLException e) { + fail(e.toString()); + } + + sql = "create table " + table5 + " (" + + "c1 int ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "c2 smallint ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "c3 bigint ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + ");"; + + try { + stmt.execute(sql); + } + catch (SQLException e) { + fail(e.toString()); + } + + sql = "create table " + table6 + " (" + + "c1 int ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "c2 smallint ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "c3 bigint ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + ");"; + + try { + stmt.execute(sql); + } + catch (SQLException e) { + fail(e.toString()); + } + } + + private static void populateTable4() throws SQLException { + String sql = "insert into " + table4 + " values( " + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // bit + for (int i = 1; i <= 3; i++) { + pstmt.setInt(i, Integer.parseInt(numericValues[3])); + } + + pstmt.execute(); + } + } + + private static void populateTable3() throws SQLException { + String sql = "insert into " + table3 + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // bit + for (int i = 1; i <= 3; i++) { + if (numericValues[0].equalsIgnoreCase("true")) { + pstmt.setBoolean(i, true); + } + else { + pstmt.setBoolean(i, false); + } + } + + // tinyint + for (int i = 4; i <= 6; i++) { + pstmt.setShort(i, Short.valueOf(numericValues[1])); + } + + // smallint + for (int i = 7; i <= 9; i++) { + pstmt.setShort(i, Short.parseShort(numericValues[2])); + } + + // int + for (int i = 10; i <= 12; i++) { + pstmt.setInt(i, Integer.parseInt(numericValues[3])); + } + + // bigint + for (int i = 13; i <= 15; i++) { + pstmt.setLong(i, Long.parseLong(numericValues[4])); + } + + // float default + for (int i = 16; i <= 18; i++) { + pstmt.setDouble(i, Double.parseDouble(numericValues[5])); + } + + // float(30) + for (int i = 19; i <= 21; i++) { + pstmt.setDouble(i, Double.parseDouble(numericValues[6])); + } + + // real + for (int i = 22; i <= 24; i++) { + pstmt.setFloat(i, Float.parseFloat(numericValues[7])); + } + + // decimal default + for (int i = 25; i <= 27; i++) { + if (numericValues[8].equalsIgnoreCase("0")) + pstmt.setBigDecimal(i, new BigDecimal(numericValues[8]), 18, 0); + else + pstmt.setBigDecimal(i, new BigDecimal(numericValues[8])); + } + + // decimal(10,5) + for (int i = 28; i <= 30; i++) { + pstmt.setBigDecimal(i, new BigDecimal(numericValues[9]), 10, 5); + } + + // numeric + for (int i = 31; i <= 33; i++) { + if (numericValues[10].equalsIgnoreCase("0")) + pstmt.setBigDecimal(i, new BigDecimal(numericValues[10]), 18, 0); + else + pstmt.setBigDecimal(i, new BigDecimal(numericValues[10])); + } + + // numeric(8,2) + for (int i = 34; i <= 36; i++) { + pstmt.setBigDecimal(i, new BigDecimal(numericValues[11]), 8, 2); + } + + // int2 + for (int i = 37; i <= 39; i++) { + pstmt.setInt(i, Integer.parseInt(numericValues[3])); + } + // smallmoney + for (int i = 40; i <= 42; i++) { + pstmt.setSmallMoney(i, new BigDecimal(numericValues[12])); + } + + // money + for (int i = 43; i <= 45; i++) { + pstmt.setMoney(i, new BigDecimal(numericValues[13])); + } + + // decimal(28,4) + for (int i = 46; i <= 48; i++) { + pstmt.setBigDecimal(i, new BigDecimal(numericValues[14]), 28, 4); + } + + // numeric(28,4) + for (int i = 49; i <= 51; i++) { + pstmt.setBigDecimal(i, new BigDecimal(numericValues[15]), 28, 4); + } + + pstmt.execute(); + } + } + + private void createMultiInsertionSelection() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + multiStatementsProcedure + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + multiStatementsProcedure; + stmt.execute(sql); + + sql = "CREATE PROCEDURE " + multiStatementsProcedure + " (@p0 char(20) = null, @p1 char(20) = null, @p2 char(20) = null, " + + "@p3 varchar(50) = null, @p4 varchar(50) = null, @p5 varchar(50) = null)" + " AS" + " INSERT INTO " + table1 + + " values (@p0,@p1,@p2,@p3,@p4,@p5)" + " INSERT INTO " + table2 + " values (@p0,@p1,@p2,@p3,@p4,@p5)" + " SELECT * FROM " + table1 + + " SELECT * FROM " + table2; + stmt.execute(sql); + } + + private void MultiInsertionSelection() throws SQLException { + + try { + String sql = "{call " + multiStatementsProcedure + " (?,?,?,?,?,?)}"; + try(SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + // char, varchar + for (int i = 1; i <= 3; i++) { + callableStatement.setString(i, charValues[0]); + } + + for (int i = 4; i <= 6; i++) { + callableStatement.setString(i, charValues[1]); + } + + boolean results = callableStatement.execute(); + + // skip update count which is given by insertion + while (false == results && (-1) != callableStatement.getUpdateCount()) { + results = callableStatement.getMoreResults(); + } + + while (results) { + try(ResultSet rs = callableStatement.getResultSet()) { + int numberOfColumns = rs.getMetaData().getColumnCount(); + + while (rs.next()) { + testGetString(rs, numberOfColumns); + } + } + results = callableStatement.getMoreResults(); + } + } + } + catch (SQLException e) { + fail(e.toString()); + } + } + + private void testGetString(ResultSet rs, + int numberOfColumns) throws SQLException { + for (int i = 1; i <= numberOfColumns; i = i + 3) { + + String stringValue1 = "" + rs.getString(i); + String stringValue2 = "" + rs.getString(i + 1); + String stringValue3 = "" + rs.getString(i + 2); + + assertTrue(stringValue1.equalsIgnoreCase(stringValue2) && stringValue2.equalsIgnoreCase(stringValue3), + "Decryption failed with getString(): " + stringValue1 + ", " + stringValue2 + ", " + stringValue3 + ".\n"); + + } + } + + private void createInputProcedure() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + inputProcedure + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + inputProcedure; + stmt.execute(sql); + + sql = "CREATE PROCEDURE " + inputProcedure + " @p0 int, @p1 decimal(18, 0), " + + "@p2 float, @p3 real, @p4 numeric(18, 0), @p5 smallmoney, @p6 money," + + "@p7 bit, @p8 smallint, @p9 bigint, @p10 float(30), @p11 decimal(10,5), @p12 numeric(8,2), " + + "@p13 decimal(28,4), @p14 numeric(28,4) " + " AS" + " SELECT top 1 RandomizedInt FROM " + numericTable + + " where DeterministicInt=@p0 and DeterministicDecimalDefault=@p1 and " + + " DeterministicFloatDefault=@p2 and DeterministicReal=@p3 and DeterministicNumericDefault=@p4 and" + + " DeterministicSmallMoney=@p5 and DeterministicMoney=@p6 and DeterministicBit=@p7 and" + + " DeterministicSmallint=@p8 and DeterministicBigint=@p9 and DeterministicFloat=@p10 and" + + " DeterministicDecimal=@p11 and DeterministicNumeric=@p12 and DeterministicDecimal2=@p13 and" + " DeterministicNumeric2=@p14 "; + + stmt.execute(sql); + } + + private void testInputProcedure(String sql, + String[] values) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.setInt(1, Integer.parseInt(values[3])); + if (RandomData.returnZero) + callableStatement.setBigDecimal(2, new BigDecimal(values[8]), 18, 0); + else + callableStatement.setBigDecimal(2, new BigDecimal(values[8])); + callableStatement.setDouble(3, Double.parseDouble(values[5])); + callableStatement.setFloat(4, Float.parseFloat(values[7])); + if (RandomData.returnZero) + callableStatement.setBigDecimal(5, new BigDecimal(values[10]), 18, 0); // numeric(18,0) + else + callableStatement.setBigDecimal(5, new BigDecimal(values[10])); // numeric(18,0) + callableStatement.setSmallMoney(6, new BigDecimal(values[12])); + callableStatement.setMoney(7, new BigDecimal(values[13])); + if (values[0].equalsIgnoreCase("true")) + callableStatement.setBoolean(8, true); + else + callableStatement.setBoolean(8, false); + callableStatement.setShort(9, Short.parseShort(values[2])); // smallint + callableStatement.setLong(10, Long.parseLong(values[4])); // bigint + callableStatement.setDouble(11, Double.parseDouble(values[6])); // float30 + callableStatement.setBigDecimal(12, new BigDecimal(values[9]), 10, 5); // decimal(10,5) + callableStatement.setBigDecimal(13, new BigDecimal(values[11]), 8, 2); // numeric(8,2) + callableStatement.setBigDecimal(14, new BigDecimal(values[14]), 28, 4); + callableStatement.setBigDecimal(15, new BigDecimal(values[15]), 28, 4); + + try (SQLServerResultSet rs = (SQLServerResultSet) callableStatement.executeQuery()) { + rs.next(); + assertEquals(rs.getString(1), values[3], "" + "Test for input parameter fails.\n"); + } + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void createInputProcedure2() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + inputProcedure2 + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + inputProcedure2; + stmt.execute(sql); + + sql = "CREATE PROCEDURE " + inputProcedure2 + + " @p0 varchar(50), @p1 uniqueidentifier, @p2 varchar(max), @p3 nchar(30), @p4 nvarchar(60), @p5 nvarchar(max), " + + " @p6 varchar(8000), @p7 nvarchar(4000)" + " AS" + + " SELECT top 1 RandomizedVarchar, DeterministicUniqueidentifier, DeterministicVarcharMax, RandomizedNchar, " + + " DeterministicNvarchar, DeterministicNvarcharMax, DeterministicVarchar8000, RandomizedNvarchar4000 FROM " + charTable + + " where DeterministicVarchar = @p0 and DeterministicUniqueidentifier =@p1"; + + stmt.execute(sql); + } + + private void testInputProcedure2(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.setString(1, charValues[1]); + callableStatement.setUniqueIdentifier(2, charValues[6]); + callableStatement.setString(3, charValues[2]); + callableStatement.setNString(4, charValues[3]); + callableStatement.setNString(5, charValues[4]); + callableStatement.setNString(6, charValues[5]); + callableStatement.setString(7, charValues[7]); + callableStatement.setNString(8, charValues[8]); + + try (SQLServerResultSet rs = (SQLServerResultSet) callableStatement.executeQuery()) { + rs.next(); + assertEquals(rs.getString(1).trim(), charValues[1], "Test for input parameter fails.\n"); + assertEquals(rs.getUniqueIdentifier(2), charValues[6].toUpperCase(), "Test for input parameter fails.\n"); + assertEquals(rs.getString(3).trim(), charValues[2], "Test for input parameter fails.\n"); + assertEquals(rs.getString(4).trim(), charValues[3], "Test for input parameter fails.\n"); + assertEquals(rs.getString(5).trim(), charValues[4], "Test for input parameter fails.\n"); + assertEquals(rs.getString(6).trim(), charValues[5], "Test for input parameter fails.\n"); + assertEquals(rs.getString(7).trim(), charValues[7], "Test for input parameter fails.\n"); + assertEquals(rs.getString(8).trim(), charValues[8], "Test for input parameter fails.\n"); + } + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void createOutputProcedure3() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + outputProcedure3 + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + outputProcedure3; + stmt.execute(sql); + + sql = "CREATE PROCEDURE " + outputProcedure3 + " @p0 int OUTPUT, @p1 int OUTPUT " + " AS" + " SELECT top 1 @p0=DeterministicInt FROM " + + table3 + " SELECT top 1 @p1=RandomizedInt FROM " + table4; + + stmt.execute(sql); + } + + private void testOutputProcedure3RandomOrder(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter(1, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(2, java.sql.Types.INTEGER); + + callableStatement.execute(); + + int intValue2 = callableStatement.getInt(2); + assertEquals("" + intValue2, numericValues[3], "Test for output parameter fails.\n"); + + int intValue = callableStatement.getInt(1); + assertEquals("" + intValue, numericValues[3], "Test for output parameter fails.\n"); + + int intValue3 = callableStatement.getInt(2); + assertEquals("" + intValue3, numericValues[3], "Test for output parameter fails.\n"); + + int intValue4 = callableStatement.getInt(2); + assertEquals("" + intValue4, numericValues[3], "Test for output parameter fails.\n"); + + int intValue5 = callableStatement.getInt(1); + assertEquals("" + intValue5, numericValues[3], "Test for output parameter fails.\n"); + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void testOutputProcedure3Inorder(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter(1, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(2, java.sql.Types.INTEGER); + + callableStatement.execute(); + + int intValue = callableStatement.getInt(1); + assertEquals("" + intValue, numericValues[3], "Test for output parameter fails.\n"); + + int intValue2 = callableStatement.getInt(2); + assertEquals("" + intValue2, numericValues[3], "Test for output parameter fails.\n"); + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void testOutputProcedure3ReverseOrder(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter(1, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(2, java.sql.Types.INTEGER); + + callableStatement.execute(); + + int intValue2 = callableStatement.getInt(2); + assertEquals("" + intValue2, numericValues[3], "Test for output parameter fails.\n"); + + int intValue = callableStatement.getInt(1); + assertEquals("" + intValue, numericValues[3], "Test for output parameter fails.\n"); + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void createOutputProcedure2() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + outputProcedure2 + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + outputProcedure2; + stmt.execute(sql); + + sql = "CREATE PROCEDURE " + outputProcedure2 + + " @p0 int OUTPUT, @p1 int OUTPUT, @p2 smallint OUTPUT, @p3 smallint OUTPUT, @p4 tinyint OUTPUT, @p5 tinyint OUTPUT, @p6 smallmoney OUTPUT," + + " @p7 smallmoney OUTPUT, @p8 money OUTPUT, @p9 money OUTPUT " + " AS" + + " SELECT top 1 @p0=PlainInt, @p1=DeterministicInt, @p2=PlainSmallint," + + " @p3=RandomizedSmallint, @p4=PlainTinyint, @p5=DeterministicTinyint, @p6=DeterministicSmallMoney, @p7=PlainSmallMoney," + + " @p8=PlainMoney, @p9=DeterministicMoney FROM " + table3; + + stmt.execute(sql); + } + + private void testOutputProcedure2RandomOrder(String sql, + String[] values) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter(1, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(2, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(3, java.sql.Types.SMALLINT); + callableStatement.registerOutParameter(4, java.sql.Types.SMALLINT); + callableStatement.registerOutParameter(5, java.sql.Types.TINYINT); + callableStatement.registerOutParameter(6, java.sql.Types.TINYINT); + callableStatement.registerOutParameter(7, microsoft.sql.Types.SMALLMONEY); + callableStatement.registerOutParameter(8, microsoft.sql.Types.SMALLMONEY); + callableStatement.registerOutParameter(9, microsoft.sql.Types.MONEY); + callableStatement.registerOutParameter(10, microsoft.sql.Types.MONEY); + + callableStatement.execute(); + + BigDecimal ecnryptedSmallMoney = callableStatement.getSmallMoney(7); + assertEquals("" + ecnryptedSmallMoney, values[12], "Test for output parameter fails.\n"); + + short encryptedSmallint = callableStatement.getShort(4); + assertEquals("" + encryptedSmallint, values[2], "Test for output parameter fails.\n"); + + BigDecimal SmallMoneyValue = callableStatement.getSmallMoney(8); + assertEquals("" + SmallMoneyValue, values[12], "Test for output parameter fails.\n"); + + short encryptedTinyint = callableStatement.getShort(6); + assertEquals("" + encryptedTinyint, values[1], "Test for output parameter fails.\n"); + + short tinyintValue = callableStatement.getShort(5); + assertEquals("" + tinyintValue, values[1], "Test for output parameter fails.\n"); + + BigDecimal encryptedMoneyValue = callableStatement.getMoney(9); + assertEquals("" + encryptedMoneyValue, values[13], "Test for output parameter fails.\n"); + + short smallintValue = callableStatement.getShort(3); + assertEquals("" + smallintValue, values[2], "Test for output parameter fails.\n"); + + int intValue = callableStatement.getInt(1); + assertEquals("" + intValue, values[3], "Test for output parameter fails.\n"); + + BigDecimal encryptedSmallMoney = callableStatement.getMoney(10); + assertEquals("" + encryptedSmallMoney, values[13], "Test for output parameter fails.\n"); + + int encryptedInt = callableStatement.getInt(2); + assertEquals("" + encryptedInt, values[3], "Test for output parameter fails.\n"); + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void testOutputProcedure2Inorder(String sql, + String[] values) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter(1, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(2, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(3, java.sql.Types.SMALLINT); + callableStatement.registerOutParameter(4, java.sql.Types.SMALLINT); + callableStatement.registerOutParameter(5, java.sql.Types.TINYINT); + callableStatement.registerOutParameter(6, java.sql.Types.TINYINT); + callableStatement.registerOutParameter(7, microsoft.sql.Types.SMALLMONEY); + callableStatement.registerOutParameter(8, microsoft.sql.Types.SMALLMONEY); + callableStatement.registerOutParameter(9, microsoft.sql.Types.MONEY); + callableStatement.registerOutParameter(10, microsoft.sql.Types.MONEY); + callableStatement.execute(); + + int intValue = callableStatement.getInt(1); + assertEquals("" + intValue, values[3], "Test for output parameter fails.\n"); + + int encryptedInt = callableStatement.getInt(2); + assertEquals("" + encryptedInt, values[3], "Test for output parameter fails.\n"); + + short smallintValue = callableStatement.getShort(3); + assertEquals("" + smallintValue, values[2], "Test for output parameter fails.\n"); + + short encryptedSmallint = callableStatement.getShort(4); + assertEquals("" + encryptedSmallint, values[2], "Test for output parameter fails.\n"); + + short tinyintValue = callableStatement.getShort(5); + assertEquals("" + tinyintValue, values[1], "Test for output parameter fails.\n"); + + short encryptedTinyint = callableStatement.getShort(6); + assertEquals("" + encryptedTinyint, values[1], "Test for output parameter fails.\n"); + + BigDecimal encryptedSmallMoney = callableStatement.getSmallMoney(7); + assertEquals("" + encryptedSmallMoney, values[12], "Test for output parameter fails.\n"); + + BigDecimal SmallMoneyValue = callableStatement.getSmallMoney(8); + assertEquals("" + SmallMoneyValue, values[12], "Test for output parameter fails.\n"); + + BigDecimal MoneyValue = callableStatement.getMoney(9); + assertEquals("" + MoneyValue, values[13], "Test for output parameter fails.\n"); + + BigDecimal encryptedMoney = callableStatement.getMoney(10); + assertEquals("" + encryptedMoney, values[13], "Test for output parameter fails.\n"); + + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void testOutputProcedure2ReverseOrder(String sql, + String[] values) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter(1, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(2, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(3, java.sql.Types.SMALLINT); + callableStatement.registerOutParameter(4, java.sql.Types.SMALLINT); + callableStatement.registerOutParameter(5, java.sql.Types.TINYINT); + callableStatement.registerOutParameter(6, java.sql.Types.TINYINT); + callableStatement.registerOutParameter(7, microsoft.sql.Types.SMALLMONEY); + callableStatement.registerOutParameter(8, microsoft.sql.Types.SMALLMONEY); + callableStatement.registerOutParameter(9, microsoft.sql.Types.MONEY); + callableStatement.registerOutParameter(10, microsoft.sql.Types.MONEY); + + callableStatement.execute(); + + BigDecimal encryptedMoney = callableStatement.getMoney(10); + assertEquals("" + encryptedMoney, values[13], "Test for output parameter fails.\n"); + + BigDecimal MoneyValue = callableStatement.getMoney(9); + assertEquals("" + MoneyValue, values[13], "Test for output parameter fails.\n"); + + BigDecimal SmallMoneyValue = callableStatement.getSmallMoney(8); + assertEquals("" + SmallMoneyValue, values[12], "Test for output parameter fails.\n"); + + BigDecimal encryptedSmallMoney = callableStatement.getSmallMoney(7); + assertEquals("" + encryptedSmallMoney, values[12], "Test for output parameter fails.\n"); + + short encryptedTinyint = callableStatement.getShort(6); + assertEquals("" + encryptedTinyint, values[1], "Test for output parameter fails.\n"); + + short tinyintValue = callableStatement.getShort(5); + assertEquals("" + tinyintValue, values[1], "Test for output parameter fails.\n"); + + short encryptedSmallint = callableStatement.getShort(4); + assertEquals("" + encryptedSmallint, values[2], "Test for output parameter fails.\n"); + + short smallintValue = callableStatement.getShort(3); + assertEquals("" + smallintValue, values[2], "Test for output parameter fails.\n"); + + int encryptedInt = callableStatement.getInt(2); + assertEquals("" + encryptedInt, values[3], "Test for output parameter fails.\n"); + + int intValue = callableStatement.getInt(1); + assertEquals("" + intValue, values[3], "Test for output parameter fails.\n"); + + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void createOutputProcedure() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + outputProcedure + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + outputProcedure; + stmt.execute(sql); + + sql = "CREATE PROCEDURE " + outputProcedure + " @p0 int OUTPUT, @p1 float OUTPUT, @p2 smallint OUTPUT, " + + "@p3 bigint OUTPUT, @p4 tinyint OUTPUT, @p5 smallmoney OUTPUT, @p6 money OUTPUT " + " AS" + + " SELECT top 1 @p0=RandomizedInt, @p1=DeterministicFloatDefault, @p2=RandomizedSmallint," + + " @p3=RandomizedBigint, @p4=DeterministicTinyint, @p5=DeterministicSmallMoney, @p6=DeterministicMoney FROM " + table3; + + stmt.execute(sql); + } + + private void testOutputProcedureRandomOrder(String sql, + String[] values) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter(1, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(2, java.sql.Types.DOUBLE); + callableStatement.registerOutParameter(3, java.sql.Types.SMALLINT); + callableStatement.registerOutParameter(4, java.sql.Types.BIGINT); + callableStatement.registerOutParameter(5, java.sql.Types.TINYINT); + callableStatement.registerOutParameter(6, microsoft.sql.Types.SMALLMONEY); + callableStatement.registerOutParameter(7, microsoft.sql.Types.MONEY); + + callableStatement.execute(); + + double floatValue0 = callableStatement.getDouble(2); + assertEquals("" + floatValue0, "" + values[5], "Test for output parameter fails.\n"); + + long bigintValue = callableStatement.getLong(4); + assertEquals("" + bigintValue, values[4], "Test for output parameter fails.\n"); + + short tinyintValue = callableStatement.getShort(5); // tinyint + assertEquals("" + tinyintValue, values[1], "Test for output parameter fails.\n"); + + double floatValue1 = callableStatement.getDouble(2); + assertEquals("" + floatValue1, "" + values[5], "Test for output parameter fails.\n"); + + int intValue2 = callableStatement.getInt(1); + assertEquals("" + intValue2, "" + values[3], "Test for output parameter fails.\n"); + + double floatValue2 = callableStatement.getDouble(2); + assertEquals("" + floatValue2, "" + values[5], "Test for output parameter fails.\n"); + + short shortValue3 = callableStatement.getShort(3); // smallint + assertEquals("" + shortValue3, "" + values[2], "Test for output parameter fails.\n"); + + short shortValue32 = callableStatement.getShort(3); + assertEquals("" + shortValue32, "" + values[2], "Test for output parameter fails.\n"); + + BigDecimal smallmoney1 = callableStatement.getSmallMoney(6); + assertEquals("" + smallmoney1, "" + values[12], "Test for output parameter fails.\n"); + BigDecimal money1 = callableStatement.getMoney(7); + assertEquals("" + money1, "" + values[13], "Test for output parameter fails.\n"); + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void testOutputProcedureInorder(String sql, + String[] values) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter(1, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(2, java.sql.Types.DOUBLE); + callableStatement.registerOutParameter(3, java.sql.Types.SMALLINT); + callableStatement.registerOutParameter(4, java.sql.Types.BIGINT); + callableStatement.registerOutParameter(5, java.sql.Types.TINYINT); + callableStatement.registerOutParameter(6, microsoft.sql.Types.SMALLMONEY); + callableStatement.registerOutParameter(7, microsoft.sql.Types.MONEY); + + callableStatement.execute(); + + int intValue2 = callableStatement.getInt(1); + assertEquals("" + intValue2, values[3], "Test for output parameter fails.\n"); + + double floatValue0 = callableStatement.getDouble(2); + assertEquals("" + floatValue0, values[5], "Test for output parameter fails.\n"); + + short shortValue3 = callableStatement.getShort(3); + assertEquals("" + shortValue3, values[2], "Test for output parameter fails.\n"); + + long bigintValue = callableStatement.getLong(4); + assertEquals("" + bigintValue, values[4], "Test for output parameter fails.\n"); + + short tinyintValue = callableStatement.getShort(5); + assertEquals("" + tinyintValue, values[1], "Test for output parameter fails.\n"); + + BigDecimal smallMoney1 = callableStatement.getSmallMoney(6); + assertEquals("" + smallMoney1, values[12], "Test for output parameter fails.\n"); + + BigDecimal money1 = callableStatement.getMoney(7); + assertEquals("" + money1, values[13], "Test for output parameter fails.\n"); + + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void testOutputProcedureReverseOrder(String sql, + String[] values) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter(1, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(2, java.sql.Types.DOUBLE); + callableStatement.registerOutParameter(3, java.sql.Types.SMALLINT); + callableStatement.registerOutParameter(4, java.sql.Types.BIGINT); + callableStatement.registerOutParameter(5, java.sql.Types.TINYINT); + callableStatement.registerOutParameter(6, microsoft.sql.Types.SMALLMONEY); + callableStatement.registerOutParameter(7, microsoft.sql.Types.MONEY); + callableStatement.execute(); + + BigDecimal smallMoney1 = callableStatement.getSmallMoney(6); + assertEquals("" + smallMoney1, values[12], "Test for output parameter fails.\n"); + + BigDecimal money1 = callableStatement.getMoney(7); + assertEquals("" + money1, values[13], "Test for output parameter fails.\n"); + + short tinyintValue = callableStatement.getShort(5); + assertEquals("" + tinyintValue, values[1], "Test for output parameter fails.\n"); + + long bigintValue = callableStatement.getLong(4); + assertEquals("" + bigintValue, values[4], "Test for output parameter fails.\n"); + + short shortValue3 = callableStatement.getShort(3); + assertEquals("" + shortValue3, values[2], "Test for output parameter fails.\n"); + + double floatValue0 = callableStatement.getDouble(2); + assertEquals("" + floatValue0, values[5], "Test for output parameter fails.\n"); + + int intValue2 = callableStatement.getInt(1); + assertEquals("" + intValue2, values[3], "Test for output parameter fails.\n"); + + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void createInOutProcedure() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + inoutProcedure + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + inoutProcedure; + stmt.execute(sql); + + sql = "CREATE PROCEDURE " + inoutProcedure + " @p0 int OUTPUT" + " AS" + " SELECT top 1 @p0=DeterministicInt FROM " + table3 + + " where DeterministicInt=@p0"; + + stmt.execute(sql); + } + + private void testInOutProcedure(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.setInt(1, Integer.parseInt(numericValues[3])); + callableStatement.registerOutParameter(1, java.sql.Types.INTEGER); + callableStatement.execute(); + + int intValue = callableStatement.getInt(1); + + assertEquals("" + intValue, numericValues[3], "Test for Inout parameter fails.\n"); + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void createMixedProcedure() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + mixedProcedure + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + mixedProcedure; + stmt.execute(sql); + + sql = "CREATE PROCEDURE " + mixedProcedure + " @p0 int OUTPUT, @p1 float OUTPUT, @p3 decimal " + " AS" + + " SELECT top 1 @p0=DeterministicInt2, @p1=RandomizedFloatDefault FROM " + table3 + + " where DeterministicInt=@p0 and DeterministicDecimalDefault=@p3" + " return 123"; + + stmt.execute(sql); + } + + private void testMixedProcedure(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter(1, java.sql.Types.INTEGER); + callableStatement.setInt(2, Integer.parseInt(numericValues[3])); + callableStatement.registerOutParameter(2, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(3, java.sql.Types.DOUBLE); + if (RandomData.returnZero) + callableStatement.setBigDecimal(4, new BigDecimal(numericValues[8]), 18, 0); + else + callableStatement.setBigDecimal(4, new BigDecimal(numericValues[8])); + callableStatement.execute(); + + int intValue = callableStatement.getInt(2); + assertEquals("" + intValue, numericValues[3], "Test for Inout parameter fails.\n"); + + double floatValue = callableStatement.getDouble(3); + assertEquals("" + floatValue, numericValues[5], "Test for output parameter fails.\n"); + + int returnedValue = callableStatement.getInt(1); + assertEquals("" + returnedValue, "" + 123, "Test for Inout parameter fails.\n"); + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void createMixedProcedure2() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + mixedProcedure2 + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + mixedProcedure2; + stmt.execute(sql); + + sql = "CREATE PROCEDURE " + mixedProcedure2 + " @p0 int OUTPUT, @p1 float OUTPUT, @p3 int, @p4 float " + " AS" + + " SELECT top 1 @p0=DeterministicInt, @p1=PlainFloatDefault FROM " + table3 + + " where PlainInt=@p3 and DeterministicFloatDefault=@p4"; + + stmt.execute(sql); + } + + private void testMixedProcedure2RandomOrder(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter(1, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(2, java.sql.Types.FLOAT); + callableStatement.setInt(3, Integer.parseInt(numericValues[3])); + callableStatement.setDouble(4, Double.parseDouble(numericValues[5])); + callableStatement.execute(); + + double floatValue = callableStatement.getDouble(2); + assertEquals("" + floatValue, numericValues[5], "Test for output parameter fails.\n"); + + int intValue = callableStatement.getInt(1); + assertEquals("" + intValue, numericValues[3], "Test for output parameter fails.\n"); + + double floatValue2 = callableStatement.getDouble(2); + assertEquals("" + floatValue2, numericValues[5], "Test for output parameter fails.\n"); + + int intValue2 = callableStatement.getInt(1); + assertEquals("" + intValue2, numericValues[3], "Test for output parameter fails.\n"); + + int intValue3 = callableStatement.getInt(1); + assertEquals("" + intValue3, numericValues[3], "Test for output parameter fails.\n"); + + double floatValue3 = callableStatement.getDouble(2); + assertEquals("" + floatValue3, numericValues[5], "Test for output parameter fails.\n"); + + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void testMixedProcedure2Inorder(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter(1, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(2, java.sql.Types.FLOAT); + callableStatement.setInt(3, Integer.parseInt(numericValues[3])); + callableStatement.setDouble(4, Double.parseDouble(numericValues[5])); + callableStatement.execute(); + + int intValue = callableStatement.getInt(1); + assertEquals("" + intValue, numericValues[3], "Test for output parameter fails.\n"); + + double floatValue = callableStatement.getDouble(2); + assertEquals("" + floatValue, numericValues[5], "Test for output parameter fails.\n"); + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void createMixedProcedure3() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + mixedProcedure3 + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + mixedProcedure3; + stmt.execute(sql); + + sql = "CREATE PROCEDURE " + mixedProcedure3 + " @p0 bigint OUTPUT, @p1 float OUTPUT, @p2 int OUTPUT, @p3 smallint" + " AS" + + " SELECT top 1 @p0=PlainBigint, @p1=PlainFloatDefault FROM " + table3 + " where PlainInt=@p2 and PlainSmallint=@p3"; + + stmt.execute(sql); + } + + private void testMixedProcedure3RandomOrder(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter(1, java.sql.Types.BIGINT); + callableStatement.registerOutParameter(2, java.sql.Types.FLOAT); + callableStatement.setInt(3, Integer.parseInt(numericValues[3])); + callableStatement.setShort(4, Short.parseShort(numericValues[2])); + callableStatement.execute(); + + double floatValue = callableStatement.getDouble(2); + assertEquals("" + floatValue, numericValues[5], "Test for output parameter fails.\n"); + + long bigintValue = callableStatement.getLong(1); + assertEquals("" + bigintValue, numericValues[4], "Test for output parameter fails.\n"); + + long bigintValue1 = callableStatement.getLong(1); + assertEquals("" + bigintValue1, numericValues[4], "Test for output parameter fails.\n"); + + double floatValue2 = callableStatement.getDouble(2); + assertEquals("" + floatValue2, numericValues[5], "Test for output parameter fails.\n"); + + double floatValue3 = callableStatement.getDouble(2); + assertEquals("" + floatValue3, numericValues[5], "Test for output parameter fails.\n"); + + long bigintValue3 = callableStatement.getLong(1); + assertEquals("" + bigintValue3, numericValues[4], "Test for output parameter fails.\n"); + + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void testMixedProcedure3Inorder(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter(1, java.sql.Types.BIGINT); + callableStatement.registerOutParameter(2, java.sql.Types.FLOAT); + callableStatement.setInt(3, Integer.parseInt(numericValues[3])); + callableStatement.setShort(4, Short.parseShort(numericValues[2])); + callableStatement.execute(); + + long bigintValue = callableStatement.getLong(1); + assertEquals("" + bigintValue, numericValues[4], "Test for output parameter fails.\n"); + + double floatValue = callableStatement.getDouble(2); + assertEquals("" + floatValue, numericValues[5], "Test for output parameter fails.\n"); + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void testMixedProcedure3ReverseOrder(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter(1, java.sql.Types.BIGINT); + callableStatement.registerOutParameter(2, java.sql.Types.FLOAT); + callableStatement.setInt(3, Integer.parseInt(numericValues[3])); + callableStatement.setShort(4, Short.parseShort(numericValues[2])); + callableStatement.execute(); + + double floatValue = callableStatement.getDouble(2); + assertEquals("" + floatValue, numericValues[5], "Test for output parameter fails.\n"); + + long bigintValue = callableStatement.getLong(1); + assertEquals("" + bigintValue, numericValues[4], "Test for output parameter fails.\n"); + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void createMixedProcedureNumericPrcisionScale() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + mixedProcedureNumericPrcisionScale + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + mixedProcedureNumericPrcisionScale; + stmt.execute(sql); + + sql = "CREATE PROCEDURE " + mixedProcedureNumericPrcisionScale + + " @p1 decimal(18,0) OUTPUT, @p2 decimal(10,5) OUTPUT, @p3 numeric(18, 0) OUTPUT, @p4 numeric(8,2) OUTPUT " + " AS" + + " SELECT top 1 @p1=RandomizedDecimalDefault, @p2=DeterministicDecimal," + + " @p3=RandomizedNumericDefault, @p4=DeterministicNumeric FROM " + table3 + + " where DeterministicDecimal=@p2 and DeterministicNumeric=@p4" + " return 123"; + + stmt.execute(sql); + } + + private void testMixedProcedureNumericPrcisionScaleInorder(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter(1, java.sql.Types.DECIMAL, 18, 0); + callableStatement.registerOutParameter(2, java.sql.Types.DECIMAL, 10, 5); + callableStatement.registerOutParameter(3, java.sql.Types.NUMERIC, 18, 0); + callableStatement.registerOutParameter(4, java.sql.Types.NUMERIC, 8, 2); + callableStatement.setBigDecimal(2, new BigDecimal(numericValues[9]), 10, 5); + callableStatement.setBigDecimal(4, new BigDecimal(numericValues[11]), 8, 2); + callableStatement.execute(); + + BigDecimal value1 = callableStatement.getBigDecimal(1); + assertEquals(value1, new BigDecimal(numericValues[8]), "Test for input output parameter fails.\n"); + + BigDecimal value2 = callableStatement.getBigDecimal(2); + assertEquals(value2, new BigDecimal(numericValues[9]), "Test for input output parameter fails.\n"); + + BigDecimal value3 = callableStatement.getBigDecimal(3); + assertEquals(value3, new BigDecimal(numericValues[10]), "Test for input output parameter fails.\n"); + + BigDecimal value4 = callableStatement.getBigDecimal(4); + assertEquals(value4, new BigDecimal(numericValues[11]), "Test for input output parameter fails.\n"); + + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void testMixedProcedureNumericPrcisionScaleParameterName(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter("p1", java.sql.Types.DECIMAL, 18, 0); + callableStatement.registerOutParameter("p2", java.sql.Types.DECIMAL, 10, 5); + callableStatement.registerOutParameter("p3", java.sql.Types.NUMERIC, 18, 0); + callableStatement.registerOutParameter("p4", java.sql.Types.NUMERIC, 8, 2); + callableStatement.setBigDecimal("p2", new BigDecimal(numericValues[9]), 10, 5); + callableStatement.setBigDecimal("p4", new BigDecimal(numericValues[11]), 8, 2); + callableStatement.execute(); + + BigDecimal value1 = callableStatement.getBigDecimal(1); + assertEquals(value1, new BigDecimal(numericValues[8]), "Test for input output parameter fails.\n"); + + BigDecimal value2 = callableStatement.getBigDecimal(2); + assertEquals(value2, new BigDecimal(numericValues[9]), "Test for input output parameter fails.\n"); + + BigDecimal value3 = callableStatement.getBigDecimal(3); + assertEquals(value3, new BigDecimal(numericValues[10]), "Test for input output parameter fails.\n"); + + BigDecimal value4 = callableStatement.getBigDecimal(4); + assertEquals(value4, new BigDecimal(numericValues[11]), "Test for input output parameter fails.\n"); + + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void createOutputProcedureChar() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + outputProcedureChar + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + outputProcedureChar; + stmt.execute(sql); + + sql = "CREATE PROCEDURE " + outputProcedureChar + " @p0 char(20) OUTPUT,@p1 varchar(50) OUTPUT,@p2 nchar(30) OUTPUT," + + "@p3 nvarchar(60) OUTPUT, @p4 uniqueidentifier OUTPUT, @p5 varchar(max) OUTPUT, @p6 nvarchar(max) OUTPUT, @p7 varchar(8000) OUTPUT, @p8 nvarchar(4000) OUTPUT" + + " AS" + " SELECT top 1 @p0=DeterministicChar,@p1=RandomizedVarChar,@p2=RandomizedNChar," + + " @p3=DeterministicNVarChar, @p4=DeterministicUniqueidentifier, @p5=DeterministicVarcharMax," + + " @p6=DeterministicNvarcharMax, @p7=DeterministicVarchar8000, @p8=RandomizedNvarchar4000 FROM " + charTable; + + stmt.execute(sql); + } + + private void testOutputProcedureCharInorder(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + callableStatement.registerOutParameter(1, java.sql.Types.CHAR, 20, 0); + callableStatement.registerOutParameter(2, java.sql.Types.VARCHAR, 50, 0); + callableStatement.registerOutParameter(3, java.sql.Types.NCHAR, 30, 0); + callableStatement.registerOutParameter(4, java.sql.Types.NVARCHAR, 60, 0); + callableStatement.registerOutParameter(5, microsoft.sql.Types.GUID); + callableStatement.registerOutParameter(6, java.sql.Types.LONGVARCHAR); + callableStatement.registerOutParameter(7, java.sql.Types.LONGNVARCHAR); + callableStatement.registerOutParameter(8, java.sql.Types.VARCHAR, 8000, 0); + callableStatement.registerOutParameter(9, java.sql.Types.NVARCHAR, 4000, 0); + + callableStatement.execute(); + String charValue = callableStatement.getString(1).trim(); + assertEquals(charValue, charValues[0], "Test for output parameter fails.\n"); + + String varcharValue = callableStatement.getString(2).trim(); + assertEquals(varcharValue, charValues[1], "Test for output parameter fails.\n"); + + String ncharValue = callableStatement.getString(3).trim(); + assertEquals(ncharValue, charValues[3], "Test for output parameter fails.\n"); + + String nvarcharValue = callableStatement.getString(4).trim(); + assertEquals(nvarcharValue, charValues[4], "Test for output parameter fails.\n"); + + String uniqueIdentifierValue = callableStatement.getString(5).trim(); + assertEquals(uniqueIdentifierValue.toLowerCase(), charValues[6], "Test for output parameter fails.\n"); + + String varcharValuemax = callableStatement.getString(6).trim(); + assertEquals(varcharValuemax, charValues[2], "Test for output parameter fails.\n"); + + String nvarcharValuemax = callableStatement.getString(7).trim(); + assertEquals(nvarcharValuemax, charValues[5], "Test for output parameter fails.\n"); + + String varcharValue8000 = callableStatement.getString(8).trim(); + assertEquals(varcharValue8000, charValues[7], "Test for output parameter fails.\n"); + + String nvarcharValue4000 = callableStatement.getNString(9).trim(); + assertEquals(nvarcharValue4000, charValues[8], "Test for output parameter fails.\n"); + + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void testOutputProcedureCharInorderObject(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + callableStatement.registerOutParameter(1, java.sql.Types.CHAR, 20, 0); + callableStatement.registerOutParameter(2, java.sql.Types.VARCHAR, 50, 0); + callableStatement.registerOutParameter(3, java.sql.Types.NCHAR, 30, 0); + callableStatement.registerOutParameter(4, java.sql.Types.NVARCHAR, 60, 0); + callableStatement.registerOutParameter(5, microsoft.sql.Types.GUID); + callableStatement.registerOutParameter(6, java.sql.Types.LONGVARCHAR); + callableStatement.registerOutParameter(7, java.sql.Types.LONGNVARCHAR); + callableStatement.registerOutParameter(8, java.sql.Types.VARCHAR, 8000, 0); + callableStatement.registerOutParameter(9, java.sql.Types.NVARCHAR, 4000, 0); + + callableStatement.execute(); + + String charValue = (String) callableStatement.getObject(1); + assertEquals(charValue.trim(), charValues[0], "Test for output parameter fails.\n"); + + String varcharValue = (String) callableStatement.getObject(2); + assertEquals(varcharValue.trim(), charValues[1], "Test for output parameter fails.\n"); + + String ncharValue = (String) callableStatement.getObject(3); + assertEquals(ncharValue.trim(), charValues[3], "Test for output parameter fails.\n"); + + String nvarcharValue = (String) callableStatement.getObject(4); + assertEquals(nvarcharValue.trim(), charValues[4], "Test for output parameter fails.\n"); + + String uniqueIdentifierValue = (String) callableStatement.getObject(5); + assertEquals(uniqueIdentifierValue.toLowerCase(), charValues[6], "Test for output parameter fails.\n"); + + String varcharValuemax = (String) callableStatement.getObject(6); + + assertEquals(varcharValuemax, charValues[2], "Test for output parameter fails.\n"); + + String nvarcharValuemax = (String) callableStatement.getObject(7); + + assertEquals(nvarcharValuemax.trim(), charValues[5], "Test for output parameter fails.\n"); + + String varcharValue8000 = (String) callableStatement.getObject(8); + assertEquals(varcharValue8000, charValues[7], "Test for output parameter fails.\n"); + + String nvarcharValue4000 = (String) callableStatement.getObject(9); + assertEquals(nvarcharValue4000, charValues[8], "Test for output parameter fails.\n"); + + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void createOutputProcedureNumeric() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + outputProcedureNumeric + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + outputProcedureNumeric; + stmt.execute(sql); + + sql = "CREATE PROCEDURE " + outputProcedureNumeric + " @p0 bit OUTPUT, @p1 tinyint OUTPUT, @p2 smallint OUTPUT, @p3 int OUTPUT," + + " @p4 bigint OUTPUT, @p5 float OUTPUT, @p6 float(30) output, @p7 real output, @p8 decimal(18, 0) output, @p9 decimal(10,5) output," + + " @p10 numeric(18, 0) output, @p11 numeric(8,2) output, @p12 smallmoney output, @p13 money output, @p14 decimal(28,4) output, @p15 numeric(28,4) output" + + " AS" + " SELECT top 1 @p0=DeterministicBit, @p1=RandomizedTinyint, @p2=DeterministicSmallint," + + " @p3=RandomizedInt, @p4=DeterministicBigint, @p5=RandomizedFloatDefault, @p6=DeterministicFloat," + + " @p7=RandomizedReal, @p8=DeterministicDecimalDefault, @p9=RandomizedDecimal," + + " @p10=DeterministicNumericDefault, @p11=RandomizedNumeric, @p12=RandomizedSmallMoney, @p13=DeterministicMoney," + + " @p14=DeterministicDecimal2, @p15=DeterministicNumeric2 FROM " + numericTable; + + stmt.execute(sql); + } + + private void testOutputProcedureNumericInorder(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + callableStatement.registerOutParameter(1, java.sql.Types.BIT); + callableStatement.registerOutParameter(2, java.sql.Types.TINYINT); + callableStatement.registerOutParameter(3, java.sql.Types.SMALLINT); + callableStatement.registerOutParameter(4, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(5, java.sql.Types.BIGINT); + callableStatement.registerOutParameter(6, java.sql.Types.DOUBLE); + callableStatement.registerOutParameter(7, java.sql.Types.DOUBLE, 30, 0); + callableStatement.registerOutParameter(8, java.sql.Types.REAL); + callableStatement.registerOutParameter(9, java.sql.Types.DECIMAL, 18, 0); + callableStatement.registerOutParameter(10, java.sql.Types.DECIMAL, 10, 5); + callableStatement.registerOutParameter(11, java.sql.Types.NUMERIC, 18, 0); + callableStatement.registerOutParameter(12, java.sql.Types.NUMERIC, 8, 2); + callableStatement.registerOutParameter(13, microsoft.sql.Types.SMALLMONEY); + callableStatement.registerOutParameter(14, microsoft.sql.Types.MONEY); + callableStatement.registerOutParameter(15, java.sql.Types.DECIMAL, 28, 4); + callableStatement.registerOutParameter(16, java.sql.Types.NUMERIC, 28, 4); + + callableStatement.execute(); + + int bitValue = callableStatement.getInt(1); + if (bitValue == 0) + assertEquals("" + false, numericValues[0], "Test for output parameter fails.\n"); + else + assertEquals("" + true, numericValues[0], "Test for output parameter fails.\n"); + + short tinyIntValue = callableStatement.getShort(2); + assertEquals("" + tinyIntValue, numericValues[1], "Test for output parameter fails.\n"); + + short smallIntValue = callableStatement.getShort(3); + assertEquals("" + smallIntValue, numericValues[2], "Test for output parameter fails.\n"); + + int intValue = callableStatement.getInt(4); + assertEquals("" + intValue, numericValues[3], "Test for output parameter fails.\n"); + + long bigintValue = callableStatement.getLong(5); + assertEquals("" + bigintValue, numericValues[4], "Test for output parameter fails.\n"); + + double floatDefault = callableStatement.getDouble(6); + assertEquals("" + floatDefault, numericValues[5], "Test for output parameter fails.\n"); + + double floatValue = callableStatement.getDouble(7); + assertEquals("" + floatValue, numericValues[6], "Test for output parameter fails.\n"); + + float realValue = callableStatement.getFloat(8); + assertEquals("" + realValue, numericValues[7], "Test for output parameter fails.\n"); + + BigDecimal decimalDefault = callableStatement.getBigDecimal(9); + assertEquals(decimalDefault, new BigDecimal(numericValues[8]), "Test for output parameter fails.\n"); + + BigDecimal decimalValue = callableStatement.getBigDecimal(10); + assertEquals(decimalValue, new BigDecimal(numericValues[9]), "Test for output parameter fails.\n"); + + BigDecimal numericDefault = callableStatement.getBigDecimal(11); + assertEquals(numericDefault, new BigDecimal(numericValues[10]), "Test for output parameter fails.\n"); + + BigDecimal numericValue = callableStatement.getBigDecimal(12); + assertEquals(numericValue, new BigDecimal(numericValues[11]), "Test for output parameter fails.\n"); + + BigDecimal smallMoneyValue = callableStatement.getSmallMoney(13); + assertEquals(smallMoneyValue, new BigDecimal(numericValues[12]), "Test for output parameter fails.\n"); + + BigDecimal moneyValue = callableStatement.getMoney(14); + assertEquals(moneyValue, new BigDecimal(numericValues[13]), "Test for output parameter fails.\n"); + + BigDecimal decimalValue2 = callableStatement.getBigDecimal(15); + assertEquals(decimalValue2, new BigDecimal(numericValues[14]), "Test for output parameter fails.\n"); + + BigDecimal numericValue2 = callableStatement.getBigDecimal(16); + assertEquals(numericValue2, new BigDecimal(numericValues[15]), "Test for output parameter fails.\n"); + + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void testcoerctionsOutputProcedureNumericInorder(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + callableStatement.registerOutParameter(1, java.sql.Types.BIT); + callableStatement.registerOutParameter(2, java.sql.Types.TINYINT); + callableStatement.registerOutParameter(3, java.sql.Types.SMALLINT); + callableStatement.registerOutParameter(4, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(5, java.sql.Types.BIGINT); + callableStatement.registerOutParameter(6, java.sql.Types.DOUBLE); + callableStatement.registerOutParameter(7, java.sql.Types.DOUBLE, 30, 0); + callableStatement.registerOutParameter(8, java.sql.Types.REAL); + callableStatement.registerOutParameter(9, java.sql.Types.DECIMAL, 18, 0); + callableStatement.registerOutParameter(10, java.sql.Types.DECIMAL, 10, 5); + callableStatement.registerOutParameter(11, java.sql.Types.NUMERIC, 18, 0); + callableStatement.registerOutParameter(12, java.sql.Types.NUMERIC, 8, 2); + callableStatement.registerOutParameter(13, microsoft.sql.Types.SMALLMONEY); + callableStatement.registerOutParameter(14, microsoft.sql.Types.MONEY); + callableStatement.registerOutParameter(15, java.sql.Types.DECIMAL, 28, 4); + callableStatement.registerOutParameter(16, java.sql.Types.NUMERIC, 28, 4); + + callableStatement.execute(); + + Class[] boolean_coercions = {Object.class, Short.class, Integer.class, Long.class, Float.class, Double.class, BigDecimal.class, + String.class}; + for (int i = 0; i < boolean_coercions.length; i++) { + Object value = getxxx(1, boolean_coercions[i], callableStatement); + Object boolVal = null; + if (value.toString().equals("1") || value.equals(true) || value.toString().equals("1.0")) + boolVal = true; + else if (value.toString().equals("0") || value.equals(false) || value.toString().equals("0.0")) + boolVal = false; + assertEquals("" + boolVal, numericValues[0], "Test for output parameter fails.\n"); + } + Class[] tinyint_coercions = {Object.class, Short.class, Integer.class, Long.class, Float.class, Double.class, BigDecimal.class, + String.class}; + for (int i = 0; i < tinyint_coercions.length; i++) { + + Object tinyIntValue = getxxx(2, tinyint_coercions[i], callableStatement); + Object x = createValue(tinyint_coercions[i], 1); + + if (x instanceof String) + assertEquals("" + tinyIntValue, x, "Test for output parameter fails.\n"); + else + assertEquals(tinyIntValue, x, "Test for output parameter fails.\n"); + } + + Class[] smallint_coercions = {Object.class, Short.class, Integer.class, Long.class, Float.class, Double.class, BigDecimal.class, + String.class}; + for (int i = 0; i < smallint_coercions.length; i++) { + Object smallIntValue = getxxx(3, smallint_coercions[i], callableStatement); + Object x = createValue(smallint_coercions[i], 2); + + if (x instanceof String) + assertEquals("" + smallIntValue, x, "Test for output parameter fails.\n"); + else + assertEquals(smallIntValue, x, "Test for output parameter fails.\n"); + } + + Class[] int_coercions = {Object.class, Short.class, Integer.class, Long.class, Float.class, Double.class, BigDecimal.class, String.class}; + for (int i = 0; i < int_coercions.length; i++) { + Object IntValue = getxxx(4, int_coercions[i], callableStatement); + Object x = createValue(int_coercions[i], 3); + if (x != null) { + if (x instanceof String) + assertEquals("" + IntValue, x, "Test for output parameter fails.\n"); + else + assertEquals(IntValue, x, "Test for output parameter fails.\n"); + } + } + + Class[] bigint_coercions = {Object.class, Short.class, Integer.class, Long.class, Float.class, Double.class, BigDecimal.class, + String.class}; + for (int i = 0; i < int_coercions.length; i++) { + Object bigIntValue = getxxx(5, bigint_coercions[i], callableStatement); + Object x = createValue(bigint_coercions[i], 4); + if (x != null) { + if (x instanceof String) + assertEquals("" + bigIntValue, x, "Test for output parameter fails.\n"); + else + assertEquals(bigIntValue, x, "Test for output parameter fails.\n"); + } + } + + Class[] float_coercions = {Object.class, Short.class, Integer.class, Long.class, Float.class, Double.class, BigDecimal.class, + String.class}; + for (int i = 0; i < float_coercions.length; i++) { + Object floatDefaultValue = getxxx(6, float_coercions[i], callableStatement); + Object x = createValue(float_coercions[i], 5); + if (x != null) { + if (x instanceof String) + assertEquals("" + floatDefaultValue, x, "Test for output parameter fails.\n"); + else + assertEquals(floatDefaultValue, x, "Test for output parameter fails.\n"); + } + } + + for (int i = 0; i < float_coercions.length; i++) { + Object floatValue = getxxx(7, float_coercions[i], callableStatement); + Object x = createValue(float_coercions[i], 6); + if (x != null) { + if (x instanceof String) + assertEquals("" + floatValue, x, "Test for output parameter fails.\n"); + else + assertEquals(floatValue, x, "Test for output parameter fails.\n"); + } + } + + Class[] real_coercions = {Object.class, Short.class, Integer.class, Long.class, Float.class, BigDecimal.class, String.class}; + for (int i = 0; i < real_coercions.length; i++) { + + Object realValue = getxxx(8, real_coercions[i], callableStatement); + + Object x = createValue(real_coercions[i], 7); + if (x != null) { + if (x instanceof String) + assertEquals("" + realValue, x, "Test for output parameter fails for Coercion: " + real_coercions[i] + " for real value.\n"); + else + assertEquals(realValue, x, "Test for output parameter fails for Coercion: " + real_coercions[i] + " for real value.\n"); + } + } + + Class[] decimalDefault_coercions = {Object.class, Short.class, Integer.class, Long.class, Float.class, Double.class, BigDecimal.class, + String.class}; + for (int i = 0; i < decimalDefault_coercions.length; i++) { + Object decimalDefaultValue = getxxx(9, decimalDefault_coercions[i], callableStatement); + Object x = createValue(decimalDefault_coercions[i], 8); + if (x != null) { + if (x instanceof String) + assertEquals("" + decimalDefaultValue, x, "Test for output parameter fails.\n"); + else + assertEquals(decimalDefaultValue, x, "Test for output parameter fails.\n"); + } + } + + for (int i = 0; i < decimalDefault_coercions.length; i++) { + Object decimalValue = getxxx(10, decimalDefault_coercions[i], callableStatement); + Object x = createValue(decimalDefault_coercions[i], 9); + if (x != null) { + if (x instanceof String) + assertEquals("" + decimalValue, x, "Test for output parameter fails.\n"); + else + assertEquals(decimalValue, x, "Test for output parameter fails.\n"); + } + } + + for (int i = 0; i < decimalDefault_coercions.length; i++) { + Object numericDefaultValue = getxxx(11, decimalDefault_coercions[i], callableStatement); + Object x = createValue(decimalDefault_coercions[i], 10); + if (x != null) { + if (x instanceof String) + assertEquals("" + numericDefaultValue, x, "Test for output parameter fails.\n"); + else + assertEquals(numericDefaultValue, x, "Test for output parameter fails.\n"); + } + } + + for (int i = 0; i < decimalDefault_coercions.length; i++) { + Object numericValue = getxxx(12, decimalDefault_coercions[i], callableStatement); + Object x = createValue(decimalDefault_coercions[i], 11); + if (x != null) { + if (x instanceof String) + assertEquals("" + numericValue, x, "Test for output parameter fails.\n"); + else + assertEquals(numericValue, x, "Test for output parameter fails.\n"); + } + } + + for (int i = 0; i < decimalDefault_coercions.length; i++) { + Object smallMoneyValue = getxxx(13, decimalDefault_coercions[i], callableStatement); + Object x = createValue(decimalDefault_coercions[i], 12); + if (x != null) { + if (x instanceof String) + assertEquals("" + smallMoneyValue, x, "Test for output parameter fails.\n"); + else + assertEquals(smallMoneyValue, x, "Test for output parameter fails.\n"); + } + } + + for (int i = 0; i < decimalDefault_coercions.length; i++) { + Object moneyValue = getxxx(14, decimalDefault_coercions[i], callableStatement); + Object x = createValue(decimalDefault_coercions[i], 13); + if (x != null) { + if (x instanceof String) + assertEquals("" + moneyValue, x, "Test for output parameter fails.\n"); + else + assertEquals(moneyValue, x, "Test for output parameter fails.\n"); + } + } + for (int i = 0; i < decimalDefault_coercions.length; i++) { + Object decimalValue2 = getxxx(15, decimalDefault_coercions[i], callableStatement); + Object x = createValue(decimalDefault_coercions[i], 14); + if (x != null) { + if (x instanceof String) + assertEquals("" + decimalValue2, x, "Test for output parameter fails.\n"); + else + assertEquals(decimalValue2, x, "Test for output parameter fails.\n"); + } + } + + for (int i = 0; i < decimalDefault_coercions.length; i++) { + Object numericValue1 = getxxx(16, decimalDefault_coercions[i], callableStatement); + Object x = createValue(decimalDefault_coercions[i], 15); + if (x != null) { + if (x instanceof String) + assertEquals("" + numericValue1, x, "Test for output parameter fails.\n"); + else + assertEquals(numericValue1, x, "Test for output parameter fails.\n"); + } + } + + } + catch (Exception e) { + fail(e.toString()); + } + } + + private Object createValue(Class coercion, + int index) { + try { + if (coercion == Float.class) + return Float.parseFloat(numericValues[index]); + if (coercion == Integer.class) + return Integer.parseInt(numericValues[index]); + if (coercion == String.class || coercion == Boolean.class || coercion == Object.class) + return (numericValues[index]); + if (coercion == Byte.class) + return Byte.parseByte(numericValues[index]); + if (coercion == Short.class) + return Short.parseShort(numericValues[index]); + if (coercion == Long.class) + return Long.parseLong(numericValues[index]); + if (coercion == Double.class) + return Double.parseDouble(numericValues[index]); + if (coercion == BigDecimal.class) + return new BigDecimal(numericValues[index]); + } + catch (java.lang.NumberFormatException e) { + } + return null; + } + + private Object getxxx(int ordinal, + Class coercion, + SQLServerCallableStatement callableStatement) throws SQLException { + + if (coercion == null || coercion == Object.class) { + return callableStatement.getObject(ordinal); + } + else if (coercion == String.class) { + return callableStatement.getString(ordinal); + } + else if (coercion == Boolean.class) { + return new Boolean(callableStatement.getBoolean(ordinal)); + } + else if (coercion == Byte.class) { + return new Byte(callableStatement.getByte(ordinal)); + } + else if (coercion == Short.class) { + return new Short(callableStatement.getShort(ordinal)); + } + else if (coercion == Integer.class) { + return new Integer(callableStatement.getInt(ordinal)); + } + else if (coercion == Long.class) { + return new Long(callableStatement.getLong(ordinal)); + } + else if (coercion == Float.class) { + return new Float(callableStatement.getFloat(ordinal)); + } + else if (coercion == Double.class) { + return new Double(callableStatement.getDouble(ordinal)); + } + else if (coercion == BigDecimal.class) { + return callableStatement.getBigDecimal(ordinal); + } + else if (coercion == byte[].class) { + return callableStatement.getBytes(ordinal); + } + else if (coercion == java.sql.Date.class) { + return callableStatement.getDate(ordinal); + } + else if (coercion == Time.class) { + return callableStatement.getTime(ordinal); + } + else if (coercion == Timestamp.class) { + return callableStatement.getTimestamp(ordinal); + } + else if (coercion == microsoft.sql.DateTimeOffset.class) { + return callableStatement.getDateTimeOffset(ordinal); + } + else { + // Otherwise + fail("Unhandled type: " + coercion); + } + + return null; + } + + private void createOutputProcedureBinary() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + outputProcedureBinary + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + outputProcedureBinary; + stmt.execute(sql); + + sql = "CREATE PROCEDURE " + outputProcedureBinary + " @p0 binary(20) OUTPUT,@p1 varbinary(50) OUTPUT,@p2 varbinary(max) OUTPUT," + + " @p3 binary(512) OUTPUT,@p4 varbinary(8000) OUTPUT " + " AS" + + " SELECT top 1 @p0=RandomizedBinary,@p1=DeterministicVarbinary,@p2=DeterministicVarbinaryMax," + + " @p3=DeterministicBinary512,@p4=DeterministicBinary8000 FROM " + binaryTable; + + stmt.execute(sql); + } + + private void testOutputProcedureBinaryInorder(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + callableStatement.registerOutParameter(1, java.sql.Types.BINARY, 20, 0); + callableStatement.registerOutParameter(2, java.sql.Types.VARBINARY, 50, 0); + callableStatement.registerOutParameter(3, java.sql.Types.LONGVARBINARY); + callableStatement.registerOutParameter(4, java.sql.Types.BINARY, 512, 0); + callableStatement.registerOutParameter(5, java.sql.Types.VARBINARY, 8000, 0); + callableStatement.execute(); + + byte[] expected = byteValues.get(0); + + byte[] received1 = callableStatement.getBytes(1); + for (int i = 0; i < expected.length; i++) { + assertEquals(received1[i], expected[i], "Test for output parameter fails.\n"); + } + + expected = byteValues.get(1); + byte[] received2 = callableStatement.getBytes(2); + for (int i = 0; i < expected.length; i++) { + assertEquals(received2[i], expected[i], "Test for output parameter fails.\n"); + } + + expected = byteValues.get(2); + byte[] received3 = callableStatement.getBytes(3); + for (int i = 0; i < expected.length; i++) { + assertEquals(received3[i], expected[i], "Test for output parameter fails.\n"); + } + + expected = byteValues.get(3); + byte[] received4 = callableStatement.getBytes(4); + for (int i = 0; i < expected.length; i++) { + assertEquals(received4[i], expected[i], "Test for output parameter fails.\n"); + } + + expected = byteValues.get(4); + byte[] received5 = callableStatement.getBytes(5); + for (int i = 0; i < expected.length; i++) { + assertEquals(received5[i], expected[i], "Test for output parameter fails.\n"); + } + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void testOutputProcedureBinaryInorderObject(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + callableStatement.registerOutParameter(1, java.sql.Types.BINARY, 20, 0); + callableStatement.registerOutParameter(2, java.sql.Types.VARBINARY, 50, 0); + callableStatement.registerOutParameter(3, java.sql.Types.LONGVARBINARY); + callableStatement.registerOutParameter(4, java.sql.Types.BINARY, 512, 0); + callableStatement.registerOutParameter(5, java.sql.Types.VARBINARY, 8000, 0); + callableStatement.execute(); + + int index = 1; + for (int i = 0; i < byteValues.size(); i++) { + byte[] expected = null; + if (null != byteValues.get(i)) + expected = byteValues.get(i); + + byte[] binaryValue = (byte[]) callableStatement.getObject(index); + try { + if (null != byteValues.get(i)) { + for (int j = 0; j < expected.length; j++) { + assertEquals(expected[j] == binaryValue[j] && expected[j] == binaryValue[j] && expected[j] == binaryValue[j], true, + "Decryption failed with getObject(): " + binaryValue + ", " + binaryValue + ", " + binaryValue + ".\n"); + } + } + } + catch (Exception e) { + fail(e.toString()); + } + finally { + index++; + } + } + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void testOutputProcedureBinaryInorderString(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + callableStatement.registerOutParameter(1, java.sql.Types.BINARY, 20, 0); + callableStatement.registerOutParameter(2, java.sql.Types.VARBINARY, 50, 0); + callableStatement.registerOutParameter(3, java.sql.Types.LONGVARBINARY); + callableStatement.registerOutParameter(4, java.sql.Types.BINARY, 512, 0); + callableStatement.registerOutParameter(5, java.sql.Types.VARBINARY, 8000, 0); + callableStatement.execute(); + + int index = 1; + for (int i = 0; i < byteValues.size(); i++) { + String stringValue1 = ("" + callableStatement.getString(index)).trim(); + + StringBuffer expected = new StringBuffer(); + String expectedStr = null; + + if (null != byteValues.get(i)) { + for (byte b : byteValues.get(i)) { + expected.append(String.format("%02X", b)); + } + expectedStr = "" + expected.toString(); + } + else { + expectedStr = "null"; + } + try { + assertEquals(stringValue1.startsWith(expectedStr), true, + "\nDecryption failed with getString(): " + stringValue1 + ".\nExpected Value: " + expectedStr); + } + catch (Exception e) { + fail(e.toString()); + } + finally { + index++; + } + } + } + } + + protected static void createDateTableCallableStatement() throws SQLException { + String sql = "create table " + dateTable + " (" + "PlainDate date null," + + "RandomizedDate date ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDate date ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainDatetime2Default datetime2 null," + + "RandomizedDatetime2Default datetime2 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDatetime2Default datetime2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainDatetimeoffsetDefault datetimeoffset null," + + "RandomizedDatetimeoffsetDefault datetimeoffset ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDatetimeoffsetDefault datetimeoffset ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainTimeDefault time null," + + "RandomizedTimeDefault time ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicTimeDefault time ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainDatetime2 datetime2(2) null," + + "RandomizedDatetime2 datetime2(2) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDatetime2 datetime2(2) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainTime time(2) null," + + "RandomizedTime time(2) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicTime time(2) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainDatetimeoffset datetimeoffset(2) null," + + "RandomizedDatetimeoffset datetimeoffset(2) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDatetimeoffset datetimeoffset(2) ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainDateTime DateTime null," + + "RandomizedDateTime DateTime ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicDateTime DateTime ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainSmallDatetime smalldatetime null," + + "RandomizedSmallDatetime smalldatetime ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicSmallDatetime smalldatetime ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + ");"; + + try { + stmt.execute(sql); + stmt.execute("DBCC FREEPROCCACHE"); + } + catch (SQLException e) { + fail(e.toString()); + } + } + + private static LinkedList createTemporalTypesCallableStatement(boolean nullable) { + + Date date = RandomData.generateDate(nullable); + Timestamp datetime2 = RandomData.generateDatetime2(7, nullable); + DateTimeOffset datetimeoffset = RandomData.generateDatetimeoffset(7, nullable); + Time time = RandomData.generateTime(7, nullable); + Timestamp datetime2_2 = RandomData.generateDatetime2(2, nullable); + Time time_2 = RandomData.generateTime(2, nullable); + DateTimeOffset datetimeoffset_2 = RandomData.generateDatetimeoffset(2, nullable); + + Timestamp datetime = RandomData.generateDatetime(nullable); + Timestamp smalldatetime = RandomData.generateSmalldatetime(nullable); + + LinkedList list = new LinkedList<>(); + list.add(date); + list.add(datetime2); + list.add(datetimeoffset); + list.add(time); + list.add(datetime2_2); + list.add(time_2); + list.add(datetimeoffset_2); + list.add(datetime); + list.add(smalldatetime); + + return list; + } + + private static void populateDateNormalCase() throws SQLException { + String sql = "insert into " + dateTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + + "?,?,?" + ")"; + + SQLServerPreparedStatement sqlPstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting); + + // date + for (int i = 1; i <= 3; i++) { + sqlPstmt.setDate(i, (Date) dateValues.get(0)); + } + + // datetime2 default + for (int i = 4; i <= 6; i++) { + sqlPstmt.setTimestamp(i, (Timestamp) dateValues.get(1)); + } + + // datetimeoffset default + for (int i = 7; i <= 9; i++) { + sqlPstmt.setDateTimeOffset(i, (DateTimeOffset) dateValues.get(2)); + } + + // time default + for (int i = 10; i <= 12; i++) { + sqlPstmt.setTime(i, (Time) dateValues.get(3)); + } + + // datetime2(2) + for (int i = 13; i <= 15; i++) { + sqlPstmt.setTimestamp(i, (Timestamp) dateValues.get(4), 2); + } + + // time(2) + for (int i = 16; i <= 18; i++) { + sqlPstmt.setTime(i, (Time) dateValues.get(5), 2); + } + + // datetimeoffset(2) + for (int i = 19; i <= 21; i++) { + sqlPstmt.setDateTimeOffset(i, (DateTimeOffset) dateValues.get(6), 2); + } + + // datetime() + for (int i = 22; i <= 24; i++) { + sqlPstmt.setDateTime(i, (Timestamp) dateValues.get(7)); + } + + // smalldatetime() + for (int i = 25; i <= 27; i++) { + sqlPstmt.setSmallDateTime(i, (Timestamp) dateValues.get(8)); + } + sqlPstmt.execute(); + } + + private void createOutputProcedureDate() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + outputProcedureDate + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + outputProcedureDate; + stmt.execute(sql); + + sql = "CREATE PROCEDURE " + outputProcedureDate + " @p0 date OUTPUT, @p01 date OUTPUT, @p1 datetime2 OUTPUT, @p11 datetime2 OUTPUT," + + " @p2 datetimeoffset OUTPUT, @p21 datetimeoffset OUTPUT, @p3 time OUTPUT, @p31 time OUTPUT, @p4 datetime OUTPUT, @p41 datetime OUTPUT," + + " @p5 smalldatetime OUTPUT, @p51 smalldatetime OUTPUT, @p6 datetime2(2) OUTPUT, @p61 datetime2(2) OUTPUT, @p7 time(2) OUTPUT, @p71 time(2) OUTPUT, " + + " @p8 datetimeoffset(2) OUTPUT, @p81 datetimeoffset(2) OUTPUT " + " AS" + + " SELECT top 1 @p0=PlainDate,@p01=RandomizedDate,@p1=PlainDatetime2Default,@p11=RandomizedDatetime2Default," + + " @p2=PlainDatetimeoffsetDefault,@p21=DeterministicDatetimeoffsetDefault," + " @p3=PlainTimeDefault,@p31=DeterministicTimeDefault," + + " @p4=PlainDateTime,@p41=DeterministicDateTime, @p5=PlainSmallDateTime,@p51=RandomizedSmallDateTime, " + + " @p6=PlainDatetime2,@p61=RandomizedDatetime2, @p7=PlainTime,@p71=Deterministictime, " + + " @p8=PlainDatetimeoffset, @p81=RandomizedDatetimeoffset" + " FROM " + dateTable; + + stmt.execute(sql); + } + + private void testOutputProcedureDateInorder(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + callableStatement.registerOutParameter(1, java.sql.Types.DATE); + callableStatement.registerOutParameter(2, java.sql.Types.DATE); + callableStatement.registerOutParameter(3, java.sql.Types.TIMESTAMP); + callableStatement.registerOutParameter(4, java.sql.Types.TIMESTAMP); + callableStatement.registerOutParameter(5, microsoft.sql.Types.DATETIMEOFFSET); + callableStatement.registerOutParameter(6, microsoft.sql.Types.DATETIMEOFFSET); + callableStatement.registerOutParameter(7, java.sql.Types.TIME); + callableStatement.registerOutParameter(8, java.sql.Types.TIME); + callableStatement.registerOutParameter(9, microsoft.sql.Types.DATETIME); // datetime + callableStatement.registerOutParameter(10, microsoft.sql.Types.DATETIME); // datetime + callableStatement.registerOutParameter(11, microsoft.sql.Types.SMALLDATETIME); // smalldatetime + callableStatement.registerOutParameter(12, microsoft.sql.Types.SMALLDATETIME); // smalldatetime + callableStatement.registerOutParameter(13, java.sql.Types.TIMESTAMP, 2); + callableStatement.registerOutParameter(14, java.sql.Types.TIMESTAMP, 2); + callableStatement.registerOutParameter(15, java.sql.Types.TIME, 2); + callableStatement.registerOutParameter(16, java.sql.Types.TIME, 2); + callableStatement.registerOutParameter(17, microsoft.sql.Types.DATETIMEOFFSET, 2); + callableStatement.registerOutParameter(18, microsoft.sql.Types.DATETIMEOFFSET, 2); + callableStatement.execute(); + + assertEquals(callableStatement.getDate(1), callableStatement.getDate(2), "Test for output parameter fails.\n"); + + assertEquals(callableStatement.getTimestamp(3), callableStatement.getTimestamp(4), "Test for output parameter fails.\n"); + + assertEquals(callableStatement.getDateTimeOffset(5), callableStatement.getDateTimeOffset(6), "Test for output parameter fails.\n"); + + assertEquals(callableStatement.getTime(7), callableStatement.getTime(8), "Test for output parameter fails.\n"); + assertEquals(callableStatement.getDateTime(9), // actual plain + callableStatement.getDateTime(10), // received expected enc + "Test for output parameter fails.\n"); + assertEquals(callableStatement.getSmallDateTime(11), callableStatement.getSmallDateTime(12), "Test for output parameter fails.\n"); + assertEquals(callableStatement.getTimestamp(13), callableStatement.getTimestamp(14), "Test for output parameter fails.\n"); + assertEquals(callableStatement.getTime(15).getTime(), callableStatement.getTime(16).getTime(), "Test for output parameter fails.\n"); + assertEquals(callableStatement.getDateTimeOffset(17), callableStatement.getDateTimeOffset(18), "Test for output parameter fails.\n"); + + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void testOutputProcedureDateInorderObject(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + callableStatement.registerOutParameter(1, java.sql.Types.DATE); + callableStatement.registerOutParameter(2, java.sql.Types.DATE); + callableStatement.registerOutParameter(3, java.sql.Types.TIMESTAMP); + callableStatement.registerOutParameter(4, java.sql.Types.TIMESTAMP); + callableStatement.registerOutParameter(5, microsoft.sql.Types.DATETIMEOFFSET); + callableStatement.registerOutParameter(6, microsoft.sql.Types.DATETIMEOFFSET); + callableStatement.registerOutParameter(7, java.sql.Types.TIME); + callableStatement.registerOutParameter(8, java.sql.Types.TIME); + callableStatement.registerOutParameter(9, microsoft.sql.Types.DATETIME); // datetime + callableStatement.registerOutParameter(10, microsoft.sql.Types.DATETIME); // datetime + callableStatement.registerOutParameter(11, microsoft.sql.Types.SMALLDATETIME); // smalldatetime + callableStatement.registerOutParameter(12, microsoft.sql.Types.SMALLDATETIME); // smalldatetime + callableStatement.registerOutParameter(13, java.sql.Types.TIMESTAMP, 2); + callableStatement.registerOutParameter(14, java.sql.Types.TIMESTAMP, 2); + callableStatement.registerOutParameter(15, java.sql.Types.TIME, 2); + callableStatement.registerOutParameter(16, java.sql.Types.TIME, 2); + callableStatement.registerOutParameter(17, microsoft.sql.Types.DATETIMEOFFSET, 2); + callableStatement.registerOutParameter(18, microsoft.sql.Types.DATETIMEOFFSET, 2); + callableStatement.execute(); + + assertEquals(callableStatement.getObject(1), callableStatement.getObject(2), "Test for output parameter fails.\n"); + + assertEquals(callableStatement.getObject(3), callableStatement.getObject(4), "Test for output parameter fails.\n"); + + assertEquals(callableStatement.getObject(5), callableStatement.getObject(6), "Test for output parameter fails.\n"); + + assertEquals(callableStatement.getObject(7), callableStatement.getObject(8), "Test for output parameter fails.\n"); + assertEquals(callableStatement.getObject(9), // actual plain + callableStatement.getObject(10), // received expected enc + "Test for output parameter fails.\n"); + assertEquals(callableStatement.getObject(11), callableStatement.getObject(12), "Test for output parameter fails.\n"); + assertEquals(callableStatement.getObject(13), callableStatement.getObject(14), "Test for output parameter fails.\n"); + assertEquals(callableStatement.getObject(15), callableStatement.getObject(16), "Test for output parameter fails.\n"); + assertEquals(callableStatement.getObject(17), callableStatement.getObject(18), "Test for output parameter fails.\n"); + + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void createOutputProcedureBatch() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + outputProcedureBatch + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + outputProcedureBatch; + stmt.execute(sql); + + // If a procedure contains more than one SQL statement, it is considered + // to be a batch of SQL statements. + sql = "CREATE PROCEDURE " + outputProcedureBatch + " @p0 int OUTPUT, @p1 float OUTPUT, @p2 smallint OUTPUT, @p3 smallmoney OUTPUT " + " AS" + + " select top 1 @p0=RandomizedInt FROM " + table3 + " select top 1 @p1=DeterministicFloatDefault FROM " + table3 + + " select top 1 @p2=RandomizedSmallint FROM " + table3 + " select top 1 @p3=DeterministicSmallMoney FROM " + table3; + + stmt.execute(sql); + } + + private void testOutputProcedureBatchInorder(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + + callableStatement.registerOutParameter(1, java.sql.Types.INTEGER); + callableStatement.registerOutParameter(2, java.sql.Types.DOUBLE); + callableStatement.registerOutParameter(3, java.sql.Types.SMALLINT); + callableStatement.registerOutParameter(4, microsoft.sql.Types.SMALLMONEY); + callableStatement.execute(); + + int intValue2 = callableStatement.getInt(1); + assertEquals("" + intValue2, numericValues[3], "Test for output parameter fails.\n"); + + double floatValue0 = callableStatement.getDouble(2); + assertEquals("" + floatValue0, numericValues[5], "Test for output parameter fails.\n"); + + short shortValue3 = callableStatement.getShort(3); + assertEquals("" + shortValue3, numericValues[2], "Test for output parameter fails.\n"); + + BigDecimal smallmoneyValue = callableStatement.getSmallMoney(4); + assertEquals("" + smallmoneyValue, numericValues[12], "Test for output parameter fails.\n"); + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void createOutputProcedure4() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + outputProcedure4 + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + outputProcedure4; + stmt.execute(sql); + + sql = "create procedure " + outputProcedure4 + + " @in1 int, @in2 smallint, @in3 bigint, @in4 int, @in5 smallint, @in6 bigint, @out1 int output, @out2 smallint output, @out3 bigint output, @out4 int output, @out5 smallint output, @out6 bigint output" + + " as " + " insert into " + table5 + " values (@in1, @in2, @in3)" + " insert into " + table6 + " values (@in4, @in5, @in6)" + + " select * from " + table5 + " select * from " + table6 + " select @out1 = c1, @out2=c2, @out3=c3 from " + table5 + + " select @out4 = c1, @out5=c2, @out6=c3 from " + table6; + + stmt.execute(sql); + } + + private void createMixedProcedureDateScale() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + MixedProcedureDateScale + + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + MixedProcedureDateScale; + stmt.execute(sql); + + sql = "CREATE PROCEDURE " + MixedProcedureDateScale + " @p1 datetime2(2) OUTPUT, @p2 datetime2(2) OUTPUT," + + " @p3 time(2) OUTPUT, @p4 time(2) OUTPUT, @p5 datetimeoffset(2) OUTPUT, @p6 datetimeoffset(2) OUTPUT " + " AS" + + " SELECT top 1 @p1=DeterministicDatetime2,@p2=RandomizedDatetime2,@p3=DeterministicTime,@p4=RandomizedTime," + + " @p5=DeterministicDatetimeoffset,@p6=RandomizedDatetimeoffset " + " FROM " + scaleDateTable + + " where DeterministicDatetime2 = @p1 and DeterministicTime = @p3 and DeterministicDatetimeoffset=@p5"; + + stmt.execute(sql); + } + + private void testMixedProcedureDateScaleInorder(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + callableStatement.registerOutParameter(1, java.sql.Types.TIMESTAMP, 2); + callableStatement.registerOutParameter(2, java.sql.Types.TIMESTAMP, 2); + callableStatement.registerOutParameter(3, java.sql.Types.TIME, 2); + callableStatement.registerOutParameter(4, java.sql.Types.TIME, 2); + callableStatement.registerOutParameter(5, microsoft.sql.Types.DATETIMEOFFSET, 2); + callableStatement.registerOutParameter(6, microsoft.sql.Types.DATETIMEOFFSET, 2); + callableStatement.setTimestamp(1, (Timestamp) dateValues.get(4), 2); + callableStatement.setTime(3, (Time) dateValues.get(5), 2); + callableStatement.setDateTimeOffset(5, (DateTimeOffset) dateValues.get(6), 2); + callableStatement.execute(); + + assertEquals(callableStatement.getTimestamp(1), callableStatement.getTimestamp(2), "Test for output parameter fails.\n"); + + assertEquals(callableStatement.getTime(3), callableStatement.getTime(4), "Test for output parameter fails.\n"); + + assertEquals(callableStatement.getDateTimeOffset(5), callableStatement.getDateTimeOffset(6), "Test for output parameter fails.\n"); + + } + catch (Exception e) { + fail(e.toString()); + } + } + + private void testMixedProcedureDateScaleWithParameterName(String sql) throws SQLException { + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) Util.getCallableStmt(con, sql, stmtColEncSetting)) { + callableStatement.registerOutParameter("p1", java.sql.Types.TIMESTAMP, 2); + callableStatement.registerOutParameter("p2", java.sql.Types.TIMESTAMP, 2); + callableStatement.registerOutParameter("p3", java.sql.Types.TIME, 2); + callableStatement.registerOutParameter("p4", java.sql.Types.TIME, 2); + callableStatement.registerOutParameter("p5", microsoft.sql.Types.DATETIMEOFFSET, 2); + callableStatement.registerOutParameter("p6", microsoft.sql.Types.DATETIMEOFFSET, 2); + callableStatement.setTimestamp("p1", (Timestamp) dateValues.get(4), 2); + callableStatement.setTime("p3", (Time) dateValues.get(5), 2); + callableStatement.setDateTimeOffset("p5", (DateTimeOffset) dateValues.get(6), 2); + callableStatement.execute(); + + assertEquals(callableStatement.getTimestamp(1), callableStatement.getTimestamp(2), "Test for output parameter fails.\n"); + + assertEquals(callableStatement.getTime(3), callableStatement.getTime(4), "Test for output parameter fails.\n"); + + assertEquals(callableStatement.getDateTimeOffset(5), callableStatement.getDateTimeOffset(6), "Test for output parameter fails.\n"); + + } + catch (Exception e) { + fail(e.toString()); + } + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java new file mode 100644 index 000000000..06e44276b --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java @@ -0,0 +1,1126 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.AlwaysEncrypted; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.LinkedList; + +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; +import org.opentest4j.TestAbortedException; + +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.jdbc.SQLServerResultSet; +import com.microsoft.sqlserver.jdbc.SQLServerStatement; +import com.microsoft.sqlserver.testframework.util.RandomData; +import com.microsoft.sqlserver.testframework.util.Util; + +/** + * Tests Decryption and encryption of values + * + */ +@RunWith(JUnitPlatform.class) +public class JDBCEncryptionDecryptionTest extends AESetup { + + private boolean nullable = false; + private String[] numericValues = null; + private String[] numericValues2 = null; + private String[] numericValuesNull = null; + private String[] numericValuesNull2 = null; + private String[] charValues = null; + + private LinkedList byteValuesSetObject = null; + private LinkedList byteValuesNull = null; + + private LinkedList dateValues = null; + + /** + * Junit test case for char set string for string values + * + * @throws SQLException + */ + @Test + public void testCharSpecificSetter() throws SQLException { + charValues = createCharValues(nullable); + dropTables(stmt); + createCharTable(); + populateCharNormalCase(charValues); + testChar(stmt, charValues); + testChar(null, charValues); + } + + /** + * Junit test case for char set object for string values + * + * @throws SQLException + */ + @Test + public void testCharSetObject() throws SQLException { + charValues = createCharValues(nullable); + dropTables(stmt); + createCharTable(); + populateCharSetObject(charValues); + testChar(stmt, charValues); + testChar(null, charValues); + } + + /** + * Junit test case for char set object for jdbc string values + * + * @throws SQLException + */ + @Test + public void testCharSetObjectWithJDBCTypes() throws SQLException { + skipTestForJava7(); + + charValues = createCharValues(nullable); + dropTables(stmt); + createCharTable(); + populateCharSetObjectWithJDBCTypes(charValues); + testChar(stmt, charValues); + testChar(null, charValues); + } + + /** + * Junit test case for char set string for null values + * + * @throws SQLException + */ + @Test + public void testCharSpecificSetterNull() throws SQLException { + String[] charValuesNull = {null, null, null, null, null, null, null, null, null}; + dropTables(stmt); + createCharTable(); + populateCharNormalCase(charValuesNull); + testChar(stmt, charValuesNull); + testChar(null, charValuesNull); + } + + /** + * Junit test case for char set object for null values + * + * @throws SQLException + */ + @Test + public void testCharSetObjectNull() throws SQLException { + String[] charValuesNull = {null, null, null, null, null, null, null, null, null}; + dropTables(stmt); + createCharTable(); + populateCharSetObject(charValuesNull); + testChar(stmt, charValuesNull); + testChar(null, charValuesNull); + } + + /** + * Junit test case for char set null for null values + * + * @throws SQLException + */ + @Test + public void testCharSetNull() throws SQLException { + String[] charValuesNull = {null, null, null, null, null, null, null, null, null}; + dropTables(stmt); + createCharTable(); + populateCharNullCase(); + testChar(stmt, charValuesNull); + testChar(null, charValuesNull); + } + + /** + * Junit test case for binary set binary for binary values + * + * @throws SQLException + */ + @Test + public void testBinarySpecificSetter() throws SQLException { + LinkedList byteValues = createbinaryValues(false); + dropTables(stmt); + createBinaryTable(); + populateBinaryNormalCase(byteValues); + testBinary(stmt, byteValues); + testBinary(null, byteValues); + } + + /** + * Junit test case for binary set object for binary values + * + * @throws SQLException + */ + @Test + public void testBinarySetobject() throws SQLException { + byteValuesSetObject = createbinaryValues(false); + dropTables(stmt); + createBinaryTable(); + populateBinarySetObject(byteValuesSetObject); + testBinary(stmt, byteValuesSetObject); + testBinary(null, byteValuesSetObject); + } + + /** + * Junit test case for binary set null for binary values + * + * @throws SQLException + */ + @Test + public void testBinarySetNull() throws SQLException { + byteValuesNull = createbinaryValues(true); + dropTables(stmt); + createBinaryTable(); + populateBinaryNullCase(); + testBinary(stmt, byteValuesNull); + testBinary(null, byteValuesNull); + } + + /** + * Junit test case for binary set binary for null values + * + * @throws SQLException + */ + @Test + public void testBinarySpecificSetterNull() throws SQLException { + byteValuesNull = createbinaryValues(true); + dropTables(stmt); + createBinaryTable(); + populateBinaryNormalCase(null); + testBinary(stmt, byteValuesNull); + testBinary(null, byteValuesNull); + } + + /** + * Junit test case for binary set object for null values + * + * @throws SQLException + */ + @Test + public void testBinarysetObjectNull() throws SQLException { + byteValuesNull = createbinaryValues(true); + dropTables(stmt); + createBinaryTable(); + populateBinarySetObject(null); + testBinary(stmt, byteValuesNull); + testBinary(null, byteValuesNull); + } + + /** + * Junit test case for binary set object for jdbc type binary values + * + * @throws SQLException + */ + @Test + public void testBinarySetObjectWithJDBCTypes() throws SQLException { + skipTestForJava7(); + + byteValuesSetObject = createbinaryValues(false); + dropTables(stmt); + createBinaryTable(); + populateBinarySetObjectWithJDBCType(byteValuesSetObject); + testBinary(stmt, byteValuesSetObject); + testBinary(null, byteValuesSetObject); + } + + /** + * Junit test case for date set date for date values + * + * @throws SQLException + */ + @Test + public void testDateSpecificSetter() throws SQLException { + dateValues = createTemporalTypes(nullable); + dropTables(stmt); + createDateTable(); + populateDateNormalCase(dateValues); + testDate(stmt, dateValues); + testDate(null, dateValues); + } + + /** + * Junit test case for date set object for date values + * + * @throws SQLException + */ + @Test + public void testDateSetObject() throws SQLException { + dateValues = createTemporalTypes(nullable); + dropTables(stmt); + createDateTable(); + populateDateSetObject(dateValues, ""); + testDate(stmt, dateValues); + testDate(null, dateValues); + } + + /** + * Junit test case for date set object for java date values + * + * @throws SQLException + */ + @Test + public void testDateSetObjectWithJavaType() throws SQLException { + dateValues = createTemporalTypes(nullable); + dropTables(stmt); + createDateTable(); + populateDateSetObject(dateValues, "setwithJavaType"); + testDate(stmt, dateValues); + testDate(null, dateValues); + } + + /** + * Junit test case for date set object for jdbc date values + * + * @throws SQLException + */ + @Test + public void testDateSetObjectWithJDBCType() throws SQLException { + dateValues = createTemporalTypes(nullable); + dropTables(stmt); + createDateTable(); + populateDateSetObject(dateValues, "setwithJDBCType"); + testDate(stmt, dateValues); + testDate(null, dateValues); + } + + /** + * Junit test case for date set date for min/max date values + * + * @throws SQLException + */ + @Test + public void testDateSpecificSetterMinMaxValue() throws SQLException { + RandomData.returnMinMax = true; + dateValues = createTemporalTypes(nullable); + dropTables(stmt); + createDateTable(); + populateDateNormalCase(dateValues); + testDate(stmt, dateValues); + testDate(null, dateValues); + } + + /** + * Junit test case for date set date for null values + * + * @throws SQLException + */ + @Test + public void testDateSetNull() throws SQLException { + RandomData.returnNull = true; + nullable = true; + + dateValues = createTemporalTypes(nullable); + dropTables(stmt); + createDateTable(); + populateDateNullCase(); + testDate(stmt, dateValues); + testDate(null, dateValues); + + nullable = false; + RandomData.returnNull = false; + } + + /** + * Junit test case for date set object for null values + * + * @throws SQLException + */ + @Test + public void testDateSetObjectNull() throws SQLException { + RandomData.returnNull = true; + nullable = true; + + dateValues = createTemporalTypes(nullable); + dropTables(stmt); + createDateTable(); + populateDateSetObjectNull(); + testDate(stmt, dateValues); + testDate(null, dateValues); + + nullable = false; + RandomData.returnNull = false; + } + + /** + * Junit test case for numeric set numeric for numeric values + * + * @throws SQLException + */ + @Test + public void testNumericSpecificSetter() throws TestAbortedException, Exception { + numericValues = createNumericValues(nullable); + numericValues2 = new String[numericValues.length]; + System.arraycopy(numericValues, 0, numericValues2, 0, numericValues.length); + + dropTables(stmt); + createNumericTable(); + populateNumeric(numericValues); + testNumeric(stmt, numericValues, false); + testNumeric(null, numericValues2, false); + } + + /** + * Junit test case for numeric set object for numeric values + * + * @throws SQLException + */ + @Test + public void testNumericSetObject() throws SQLException { + numericValues = createNumericValues(nullable); + numericValues2 = new String[numericValues.length]; + System.arraycopy(numericValues, 0, numericValues2, 0, numericValues.length); + + dropTables(stmt); + createNumericTable(); + populateNumericSetObject(numericValues); + testNumeric(null, numericValues, false); + testNumeric(stmt, numericValues2, false); + } + + /** + * Junit test case for numeric set object for jdbc type numeric values + * + * @throws SQLException + */ + @Test + public void testNumericSetObjectWithJDBCTypes() throws SQLException { + skipTestForJava7(); + + numericValues = createNumericValues(nullable); + numericValues2 = new String[numericValues.length]; + System.arraycopy(numericValues, 0, numericValues2, 0, numericValues.length); + + dropTables(stmt); + createNumericTable(); + populateNumericSetObjectWithJDBCTypes(numericValues); + testNumeric(stmt, numericValues, false); + testNumeric(null, numericValues2, false); + } + + /** + * Junit test case for numeric set numeric for max numeric values + * + * @throws SQLException + */ + @Test + public void testNumericSpecificSetterMaxValue() throws SQLException { + String[] numericValuesBoundaryPositive = {"true", "255", "32767", "2147483647", "9223372036854775807", "1.79E308", "1.123", "3.4E38", + "999999999999999999", "12345.12345", "999999999999999999", "567812.78", "214748.3647", "922337203685477.5807", + "999999999999999999999999.9999", "999999999999999999999999.9999"}; + String[] numericValuesBoundaryPositive2 = {"true", "255", "32767", "2147483647", "9223372036854775807", "1.79E308", "1.123", "3.4E38", + "999999999999999999", "12345.12345", "999999999999999999", "567812.78", "214748.3647", "922337203685477.5807", + "999999999999999999999999.9999", "999999999999999999999999.9999"}; + + dropTables(stmt); + createNumericTable(); + populateNumeric(numericValuesBoundaryPositive); + testNumeric(stmt, numericValuesBoundaryPositive, false); + testNumeric(null, numericValuesBoundaryPositive2, false); + } + + /** + * Junit test case for numeric set numeric for min numeric values + * + * @throws SQLException + */ + @Test + public void testNumericSpecificSetterMinValue() throws SQLException { + String[] numericValuesBoundaryNegtive = {"false", "0", "-32768", "-2147483648", "-9223372036854775808", "-1.79E308", "1.123", "-3.4E38", + "999999999999999999", "12345.12345", "999999999999999999", "567812.78", "-214748.3648", "-922337203685477.5808", + "999999999999999999999999.9999", "999999999999999999999999.9999"}; + String[] numericValuesBoundaryNegtive2 = {"false", "0", "-32768", "-2147483648", "-9223372036854775808", "-1.79E308", "1.123", "-3.4E38", + "999999999999999999", "12345.12345", "999999999999999999", "567812.78", "-214748.3648", "-922337203685477.5808", + "999999999999999999999999.9999", "999999999999999999999999.9999"}; + + dropTables(stmt); + createNumericTable(); + populateNumeric(numericValuesBoundaryNegtive); + testNumeric(stmt, numericValuesBoundaryNegtive, false); + testNumeric(null, numericValuesBoundaryNegtive2, false); + } + + /** + * Junit test case for numeric set numeric for null values + * + * @throws SQLException + */ + @Test + public void testNumericSpecificSetterNull() throws SQLException { + nullable = true; + RandomData.returnNull = true; + numericValuesNull = createNumericValues(nullable); + numericValuesNull2 = new String[numericValuesNull.length]; + System.arraycopy(numericValuesNull, 0, numericValuesNull2, 0, numericValuesNull.length); + + dropTables(stmt); + createNumericTable(); + populateNumericNullCase(numericValuesNull); + testNumeric(stmt, numericValuesNull, true); + testNumeric(null, numericValuesNull2, true); + + nullable = false; + RandomData.returnNull = false; + } + + /** + * Junit test case for numeric set object for null values + * + * @throws SQLException + */ + @Test + public void testNumericSpecificSetterSetObjectNull() throws SQLException { + nullable = true; + RandomData.returnNull = true; + numericValuesNull = createNumericValues(nullable); + numericValuesNull2 = new String[numericValuesNull.length]; + System.arraycopy(numericValuesNull, 0, numericValuesNull2, 0, numericValuesNull.length); + + dropTables(stmt); + createNumericTable(); + populateNumericSetObjectNull(); + testNumeric(stmt, numericValuesNull, true); + testNumeric(null, numericValuesNull2, true); + + nullable = false; + RandomData.returnNull = false; + } + + /** + * Junit test case for numeric set numeric for null normalization values + * + * @throws SQLException + */ + @Test + public void testNumericNormalization() throws SQLException { + String[] numericValuesNormalization = {"true", "1", "127", "100", "100", "1.123", "1.123", "1.123", "123456789123456789", "12345.12345", + "987654321123456789", "567812.78", "7812.7812", "7812.7812", "999999999999999999999999.9999", "999999999999999999999999.9999"}; + String[] numericValuesNormalization2 = {"true", "1", "127", "100", "100", "1.123", "1.123", "1.123", "123456789123456789", "12345.12345", + "987654321123456789", "567812.78", "7812.7812", "7812.7812", "999999999999999999999999.9999", "999999999999999999999999.9999"}; + dropTables(stmt); + createNumericTable(); + populateNumericNormalCase(numericValuesNormalization); + testNumeric(stmt, numericValuesNormalization, false); + testNumeric(null, numericValuesNormalization2, false); + } + + private void testChar(SQLServerStatement stmt, + String[] values) throws SQLException { + String sql = "select * from " + charTable; + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + try(ResultSet rs = (stmt == null) ? pstmt.executeQuery() : stmt.executeQuery(sql)) { + int numberOfColumns = rs.getMetaData().getColumnCount(); + while (rs.next()) { + testGetString(rs, numberOfColumns, values); + testGetObject(rs, numberOfColumns, values); + } + } + } + } + + private void testBinary(SQLServerStatement stmt, + LinkedList values) throws SQLException { + String sql = "select * from " + binaryTable; + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + try(ResultSet rs = (stmt == null) ? pstmt.executeQuery() : stmt.executeQuery(sql)) { + int numberOfColumns = rs.getMetaData().getColumnCount(); + while (rs.next()) { + testGetStringForBinary(rs, numberOfColumns, values); + testGetBytes(rs, numberOfColumns, values); + testGetObjectForBinary(rs, numberOfColumns, values); + } + } + } + } + + private void testDate(SQLServerStatement stmt, + LinkedList values1) throws SQLException { + String sql = "select * from " + dateTable; + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + try(ResultSet rs = (stmt == null) ? pstmt.executeQuery() : stmt.executeQuery(sql)) { + int numberOfColumns = rs.getMetaData().getColumnCount(); + while (rs.next()) { + // testGetStringForDate(rs, numberOfColumns, values1); //TODO: Disabling, since getString throws verification error for zero temporal + // types + testGetObjectForTemporal(rs, numberOfColumns, values1); + testGetDate(rs, numberOfColumns, values1); + } + } + } + } + + private void testGetObject(ResultSet rs, + int numberOfColumns, + String[] values) throws SQLException { + int index = 0; + for (int i = 1; i <= numberOfColumns; i = i + 3) { + try { + String objectValue1 = ("" + rs.getObject(i)).trim(); + String objectValue2 = ("" + rs.getObject(i + 1)).trim(); + String objectValue3 = ("" + rs.getObject(i + 2)).trim(); + + boolean matches = objectValue1.equalsIgnoreCase("" + values[index]) && objectValue2.equalsIgnoreCase("" + values[index]) + && objectValue3.equalsIgnoreCase("" + values[index]); + + if (("" + values[index]).length() >= 1000) { + assertTrue(matches, "\nDecryption failed with getObject() at index: " + i + ", " + (i + 1) + ", " + (i + 2) + + ".\nExpected Value at index: " + index); + } + else { + assertTrue(matches, "\nDecryption failed with getObject(): " + objectValue1 + ", " + objectValue2 + ", " + objectValue3 + + ".\nExpected Value: " + values[index]); + } + } + finally { + index++; + } + } + } + + private void testGetObjectForTemporal(ResultSet rs, + int numberOfColumns, + LinkedList values) throws SQLException { + int index = 0; + for (int i = 1; i <= numberOfColumns; i = i + 3) { + try { + String objectValue1 = ("" + rs.getObject(i)).trim(); + String objectValue2 = ("" + rs.getObject(i + 1)).trim(); + String objectValue3 = ("" + rs.getObject(i + 2)).trim(); + + Object expected = null; + if (rs.getMetaData().getColumnTypeName(i).equalsIgnoreCase("smalldatetime")) { + expected = Util.roundSmallDateTimeValue(values.get(index)); + } + else if (rs.getMetaData().getColumnTypeName(i).equalsIgnoreCase("datetime")) { + expected = Util.roundDatetimeValue(values.get(index)); + } + else { + expected = values.get(index); + } + assertTrue( + objectValue1.equalsIgnoreCase("" + expected) && objectValue2.equalsIgnoreCase("" + expected) + && objectValue3.equalsIgnoreCase("" + expected), + "\nDecryption failed with getObject(): " + objectValue1 + ", " + objectValue2 + ", " + objectValue3 + ".\nExpected Value: " + + expected); + } + finally { + index++; + } + } + } + + private void testGetObjectForBinary(ResultSet rs, + int numberOfColumns, + LinkedList values) throws SQLException { + int index = 0; + for (int i = 1; i <= numberOfColumns; i = i + 3) { + byte[] objectValue1 = (byte[]) rs.getObject(i); + byte[] objectValue2 = (byte[]) rs.getObject(i + 1); + byte[] objectValue3 = (byte[]) rs.getObject(i + 2); + + byte[] expectedBytes = null; + + if (null != values.get(index)) { + expectedBytes = values.get(index); + } + + try { + if (null != values.get(index)) { + for (int j = 0; j < expectedBytes.length; j++) { + assertTrue(expectedBytes[j] == objectValue1[j] && expectedBytes[j] == objectValue2[j] && expectedBytes[j] == objectValue3[j], + "Decryption failed with getObject(): " + objectValue1 + ", " + objectValue2 + ", " + objectValue3 + ".\n"); + } + } + } + finally { + index++; + } + } + } + + private void testGetBigDecimal(ResultSet rs, + int numberOfColumns, + String[] values) throws SQLException { + + int index = 0; + for (int i = 1; i <= numberOfColumns; i = i + 3) { + + String decimalValue1 = "" + rs.getBigDecimal(i); + String decimalValue2 = "" + rs.getBigDecimal(i + 1); + String decimalValue3 = "" + rs.getBigDecimal(i + 2); + + if (decimalValue1.equalsIgnoreCase("0") && (values[index].equalsIgnoreCase("true") || values[index].equalsIgnoreCase("false"))) { + decimalValue1 = "false"; + decimalValue2 = "false"; + decimalValue3 = "false"; + } + else if (decimalValue1.equalsIgnoreCase("1") && (values[index].equalsIgnoreCase("true") || values[index].equalsIgnoreCase("false"))) { + decimalValue1 = "true"; + decimalValue2 = "true"; + decimalValue3 = "true"; + } + + if (null != values[index]) { + if (values[index].equalsIgnoreCase("1.79E308")) { + values[index] = "1.79E+308"; + } + else if (values[index].equalsIgnoreCase("3.4E38")) { + values[index] = "3.4E+38"; + } + + if (values[index].equalsIgnoreCase("-1.79E308")) { + values[index] = "-1.79E+308"; + } + else if (values[index].equalsIgnoreCase("-3.4E38")) { + values[index] = "-3.4E+38"; + } + } + + try { + assertTrue( + decimalValue1.equalsIgnoreCase("" + values[index]) && decimalValue2.equalsIgnoreCase("" + values[index]) + && decimalValue3.equalsIgnoreCase("" + values[index]), + "\nDecryption failed with getBigDecimal(): " + decimalValue1 + ", " + decimalValue2 + ", " + decimalValue3 + + ".\nExpected Value: " + values[index]); + } + finally { + index++; + } + } + } + + private void testGetString(ResultSet rs, + int numberOfColumns, + String[] values) throws SQLException { + + int index = 0; + for (int i = 1; i <= numberOfColumns; i = i + 3) { + String stringValue1 = ("" + rs.getString(i)).trim(); + String stringValue2 = ("" + rs.getString(i + 1)).trim(); + String stringValue3 = ("" + rs.getString(i + 2)).trim(); + + if (stringValue1.equalsIgnoreCase("0") && (values[index].equalsIgnoreCase("true") || values[index].equalsIgnoreCase("false"))) { + stringValue1 = "false"; + stringValue2 = "false"; + stringValue3 = "false"; + } + else if (stringValue1.equalsIgnoreCase("1") && (values[index].equalsIgnoreCase("true") || values[index].equalsIgnoreCase("false"))) { + stringValue1 = "true"; + stringValue2 = "true"; + stringValue3 = "true"; + } + try { + + boolean matches = stringValue1.equalsIgnoreCase("" + values[index]) && stringValue2.equalsIgnoreCase("" + values[index]) + && stringValue3.equalsIgnoreCase("" + values[index]); + + if (("" + values[index]).length() >= 1000) { + assertTrue(matches, "\nDecryption failed with getString() at index: " + i + ", " + (i + 1) + ", " + (i + 2) + + ".\nExpected Value at index: " + index); + + } + else { + assertTrue(matches, "\nDecryption failed with getString(): " + stringValue1 + ", " + stringValue2 + ", " + stringValue3 + + ".\nExpected Value: " + values[index]); + } + } + finally { + index++; + } + } + } + + // not testing this for now. + @SuppressWarnings("unused") + private void testGetStringForDate(ResultSet rs, + int numberOfColumns, + LinkedList values) throws SQLException { + + int index = 0; + for (int i = 1; i <= numberOfColumns; i = i + 3) { + String stringValue1 = ("" + rs.getString(i)).trim(); + String stringValue2 = ("" + rs.getString(i + 1)).trim(); + String stringValue3 = ("" + rs.getString(i + 2)).trim(); + + try { + if (index == 3) { + assertTrue( + stringValue1.contains("" + values.get(index)) && stringValue2.contains("" + values.get(index)) + && stringValue3.contains("" + values.get(index)), + "\nDecryption failed with getString(): " + stringValue1 + ", " + stringValue2 + ", " + stringValue3 + + ".\nExpected Value: " + values.get(index)); + } + else if (index == 4) // round value for datetime + { + Object datetimeValue = "" + Util.roundDatetimeValue(values.get(index)); + assertTrue( + stringValue1.equalsIgnoreCase("" + datetimeValue) && stringValue2.equalsIgnoreCase("" + datetimeValue) + && stringValue3.equalsIgnoreCase("" + datetimeValue), + "\nDecryption failed with getString(): " + stringValue1 + ", " + stringValue2 + ", " + stringValue3 + + ".\nExpected Value: " + datetimeValue); + } + else if (index == 5) // round value for smalldatetime + { + Object smalldatetimeValue = "" + Util.roundSmallDateTimeValue(values.get(index)); + assertTrue( + stringValue1.equalsIgnoreCase("" + smalldatetimeValue) && stringValue2.equalsIgnoreCase("" + smalldatetimeValue) + && stringValue3.equalsIgnoreCase("" + smalldatetimeValue), + "\nDecryption failed with getString(): " + stringValue1 + ", " + stringValue2 + ", " + stringValue3 + + ".\nExpected Value: " + smalldatetimeValue); + } + else { + assertTrue( + stringValue1.contains("" + values.get(index)) && stringValue2.contains("" + values.get(index)) + && stringValue3.contains("" + values.get(index)), + "\nDecryption failed with getString(): " + stringValue1 + ", " + stringValue2 + ", " + stringValue3 + + ".\nExpected Value: " + values.get(index)); + } + } + finally { + index++; + } + } + } + + private void testGetBytes(ResultSet rs, + int numberOfColumns, + LinkedList values) throws SQLException { + int index = 0; + for (int i = 1; i <= numberOfColumns; i = i + 3) { + byte[] b1 = rs.getBytes(i); + byte[] b2 = rs.getBytes(i + 1); + byte[] b3 = rs.getBytes(i + 2); + + byte[] expectedBytes = null; + + if (null != values.get(index)) { + expectedBytes = values.get(index); + } + + try { + if (null != values.get(index)) { + for (int j = 0; j < expectedBytes.length; j++) { + assertTrue(expectedBytes[j] == b1[j] && expectedBytes[j] == b2[j] && expectedBytes[j] == b3[j], + "Decryption failed with getObject(): " + b1 + ", " + b2 + ", " + b3 + ".\n"); + } + } + } + finally { + index++; + } + } + } + + private void testGetStringForBinary(ResultSet rs, + int numberOfColumns, + LinkedList values) throws SQLException { + + int index = 0; + for (int i = 1; i <= numberOfColumns; i = i + 3) { + String stringValue1 = ("" + rs.getString(i)).trim(); + String stringValue2 = ("" + rs.getString(i + 1)).trim(); + String stringValue3 = ("" + rs.getString(i + 2)).trim(); + + StringBuffer expected = new StringBuffer(); + String expectedStr = null; + + if (null != values.get(index)) { + for (byte b : values.get(index)) { + expected.append(String.format("%02X", b)); + } + expectedStr = "" + expected.toString(); + } + else { + expectedStr = "null"; + } + + try { + assertTrue(stringValue1.startsWith(expectedStr) && stringValue2.startsWith(expectedStr) && stringValue3.startsWith(expectedStr), + "\nDecryption failed with getString(): " + stringValue1 + ", " + stringValue2 + ", " + stringValue3 + ".\nExpected Value: " + + expectedStr); + } + finally { + index++; + } + } + } + + private void testGetDate(ResultSet rs, + int numberOfColumns, + LinkedList values) throws SQLException { + for (int i = 1; i <= numberOfColumns; i = i + 3) { + + if (rs instanceof SQLServerResultSet) { + + String stringValue1 = null; + String stringValue2 = null; + String stringValue3 = null; + String expected = null; + + switch (i) { + + case 1: + stringValue1 = "" + ((SQLServerResultSet) rs).getDate(i); + stringValue2 = "" + ((SQLServerResultSet) rs).getDate(i + 1); + stringValue3 = "" + ((SQLServerResultSet) rs).getDate(i + 2); + expected = "" + values.get(0); + break; + + case 4: + stringValue1 = "" + ((SQLServerResultSet) rs).getTimestamp(i); + stringValue2 = "" + ((SQLServerResultSet) rs).getTimestamp(i + 1); + stringValue3 = "" + ((SQLServerResultSet) rs).getTimestamp(i + 2); + expected = "" + values.get(1); + break; + + case 7: + stringValue1 = "" + ((SQLServerResultSet) rs).getDateTimeOffset(i); + stringValue2 = "" + ((SQLServerResultSet) rs).getDateTimeOffset(i + 1); + stringValue3 = "" + ((SQLServerResultSet) rs).getDateTimeOffset(i + 2); + expected = "" + values.get(2); + break; + + case 10: + stringValue1 = "" + ((SQLServerResultSet) rs).getTime(i); + stringValue2 = "" + ((SQLServerResultSet) rs).getTime(i + 1); + stringValue3 = "" + ((SQLServerResultSet) rs).getTime(i + 2); + expected = "" + values.get(3); + break; + + case 13: + stringValue1 = "" + ((SQLServerResultSet) rs).getDateTime(i); + stringValue2 = "" + ((SQLServerResultSet) rs).getDateTime(i + 1); + stringValue3 = "" + ((SQLServerResultSet) rs).getDateTime(i + 2); + expected = "" + Util.roundDatetimeValue(values.get(4)); + break; + + case 16: + stringValue1 = "" + ((SQLServerResultSet) rs).getSmallDateTime(i); + stringValue2 = "" + ((SQLServerResultSet) rs).getSmallDateTime(i + 1); + stringValue3 = "" + ((SQLServerResultSet) rs).getSmallDateTime(i + 2); + expected = "" + Util.roundSmallDateTimeValue(values.get(5)); + break; + + default: + fail("Switch case is not matched with data"); + } + + assertTrue( + stringValue1.equalsIgnoreCase(expected) && stringValue2.equalsIgnoreCase(expected) && stringValue3.equalsIgnoreCase(expected), + "\nDecryption failed with testGetDate: " + stringValue1 + ", " + stringValue2 + ", " + stringValue3 + ".\nExpected Value: " + + expected); + } + + else { + fail("Result set is not instance of SQLServerResultSet"); + } + } + } + + private void testNumeric(Statement stmt, + String[] numericValues, + boolean isNull) throws SQLException { + String sql = "select * from " + numericTable; + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + try(SQLServerResultSet rs = (stmt == null) ? (SQLServerResultSet) pstmt.executeQuery() : (SQLServerResultSet) stmt.executeQuery(sql)) { + int numberOfColumns = rs.getMetaData().getColumnCount(); + while (rs.next()) { + testGetString(rs, numberOfColumns, numericValues); + testGetObject(rs, numberOfColumns, numericValues); + testGetBigDecimal(rs, numberOfColumns, numericValues); + if (!isNull) + testWithSpecifiedtype(rs, numberOfColumns, numericValues); + else { + String[] nullNumericValues = {"false", "0", "0", "0", "0", "0.0", "0.0", "0.0", null, null, null, null, null, null, null, null}; + testWithSpecifiedtype(rs, numberOfColumns, nullNumericValues); + } + } + } + } + } + + private void testWithSpecifiedtype(SQLServerResultSet rs, + int numberOfColumns, + String[] values) throws SQLException { + + String value1, value2, value3, expectedValue = null; + int index = 0; + + // bit + value1 = "" + rs.getBoolean(1); + value2 = "" + rs.getBoolean(2); + value3 = "" + rs.getBoolean(3); + + expectedValue = values[index]; + Compare(expectedValue, value1, value2, value3); + index++; + + // tiny + value1 = "" + rs.getShort(4); + value2 = "" + rs.getShort(5); + value3 = "" + rs.getShort(6); + + expectedValue = values[index]; + Compare(expectedValue, value1, value2, value3); + index++; + + // smallint + value1 = "" + rs.getShort(7); + value2 = "" + rs.getShort(8); + value3 = "" + rs.getShort(8); + + expectedValue = values[index]; + Compare(expectedValue, value1, value2, value3); + index++; + + // int + value1 = "" + rs.getInt(10); + value2 = "" + rs.getInt(11); + value3 = "" + rs.getInt(12); + + expectedValue = values[index]; + Compare(expectedValue, value1, value2, value3); + index++; + + // bigint + value1 = "" + rs.getLong(13); + value2 = "" + rs.getLong(14); + value3 = "" + rs.getLong(15); + + expectedValue = values[index]; + Compare(expectedValue, value1, value2, value3); + index++; + + // float + value1 = "" + rs.getDouble(16); + value2 = "" + rs.getDouble(17); + value3 = "" + rs.getDouble(18); + + expectedValue = values[index]; + Compare(expectedValue, value1, value2, value3); + index++; + + // float(30) + value1 = "" + rs.getDouble(19); + value2 = "" + rs.getDouble(20); + value3 = "" + rs.getDouble(21); + + expectedValue = values[index]; + Compare(expectedValue, value1, value2, value3); + index++; + + // real + value1 = "" + rs.getFloat(22); + value2 = "" + rs.getFloat(23); + value3 = "" + rs.getFloat(24); + + expectedValue = values[index]; + Compare(expectedValue, value1, value2, value3); + index++; + + // decimal + value1 = "" + rs.getBigDecimal(25); + value2 = "" + rs.getBigDecimal(26); + value3 = "" + rs.getBigDecimal(27); + + expectedValue = values[index]; + Compare(expectedValue, value1, value2, value3); + index++; + + // decimal (10,5) + value1 = "" + rs.getBigDecimal(28); + value2 = "" + rs.getBigDecimal(29); + value3 = "" + rs.getBigDecimal(30); + + expectedValue = values[index]; + Compare(expectedValue, value1, value2, value3); + index++; + + // numeric + value1 = "" + rs.getBigDecimal(31); + value2 = "" + rs.getBigDecimal(32); + value3 = "" + rs.getBigDecimal(33); + + expectedValue = values[index]; + Compare(expectedValue, value1, value2, value3); + index++; + + // numeric (8,2) + value1 = "" + rs.getBigDecimal(34); + value2 = "" + rs.getBigDecimal(35); + value3 = "" + rs.getBigDecimal(36); + + expectedValue = values[index]; + Compare(expectedValue, value1, value2, value3); + index++; + + // smallmoney + value1 = "" + rs.getSmallMoney(37); + value2 = "" + rs.getSmallMoney(38); + value3 = "" + rs.getSmallMoney(39); + + expectedValue = values[index]; + Compare(expectedValue, value1, value2, value3); + index++; + + // money + value1 = "" + rs.getMoney(40); + value2 = "" + rs.getMoney(41); + value3 = "" + rs.getMoney(42); + + expectedValue = values[index]; + Compare(expectedValue, value1, value2, value3); + index++; + + // decimal(28,4) + value1 = "" + rs.getBigDecimal(43); + value2 = "" + rs.getBigDecimal(44); + value3 = "" + rs.getBigDecimal(45); + + expectedValue = values[index]; + Compare(expectedValue, value1, value2, value3); + index++; + + // numeric(28,4) + value1 = "" + rs.getBigDecimal(46); + value2 = "" + rs.getBigDecimal(47); + value3 = "" + rs.getBigDecimal(48); + + expectedValue = values[index]; + Compare(expectedValue, value1, value2, value3); + index++; + } + + private void Compare(String expectedValue, + String value1, + String value2, + String value3) { + + if (null != expectedValue) { + if (expectedValue.equalsIgnoreCase("1.79E+308")) { + expectedValue = "1.79E308"; + } + else if (expectedValue.equalsIgnoreCase("3.4E+38")) { + expectedValue = "3.4E38"; + } + + if (expectedValue.equalsIgnoreCase("-1.79E+308")) { + expectedValue = "-1.79E308"; + } + else if (expectedValue.equalsIgnoreCase("-3.4E+38")) { + expectedValue = "-3.4E38"; + } + } + + assertTrue( + value1.equalsIgnoreCase("" + expectedValue) && value2.equalsIgnoreCase("" + expectedValue) + && value3.equalsIgnoreCase("" + expectedValue), + "\nDecryption failed with getBigDecimal(): " + value1 + ", " + value2 + ", " + value3 + ".\nExpected Value: " + expectedValue); + } + +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/PrecisionScaleTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/PrecisionScaleTest.java new file mode 100644 index 000000000..4d7b8b3d0 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/PrecisionScaleTest.java @@ -0,0 +1,579 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.AlwaysEncrypted; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.TimeZone; + +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.jdbc.SQLServerResultSet; +import com.microsoft.sqlserver.testframework.util.Util; + +/** + * Tests datatypes that have precision and/or scale. + * + */ +@RunWith(JUnitPlatform.class) +public class PrecisionScaleTest extends AESetup { + private static SQLServerPreparedStatement pstmt = null; + + private static java.util.Date date = null; + private static int offsetFromGMT = 0; + private static final int offset = 60000; + private static String GMTDate = ""; + private static String GMTDateWithoutDate = ""; + private static String dateTimeOffsetExpectedValue = ""; + + static { + TimeZone tz = TimeZone.getDefault(); + offsetFromGMT = tz.getOffset(1450812362177L); + + // since the Date object already accounts for timezone, subtracting the timezone difference will always give us the + // GMT version of the Date object. I can't make this PST because there are datetimeoffset tests, so I have to use GMT. + date = new Date(1450812362177L - offsetFromGMT); + + // Cannot use date.toGMTString() here directly since the date object is used elsewhere for population of data. + GMTDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); + GMTDateWithoutDate = new SimpleDateFormat("HH:mm:ss").format(date); + + // datetimeoffset is aware of timezone as well as Date, so we must apply the timezone value twice. + dateTimeOffsetExpectedValue = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + .format(new Date(1450812362177L - offsetFromGMT - offsetFromGMT + offset)); + } + + @Test + public void testNumericPrecision8Scale2() throws Exception { + dropTables(stmt); + + String[] numeric = {"1.12345", "12345.12", "567.70"}; + + createNumericPrecisionTable(30, 8, 2); + populateNumericNormalCase(numeric, 8, 2); + populateNumericSetObject(numeric, 8, 2); + + testNumeric(numeric); + } + + @Test + public void testDateScale2() throws Exception { + dropTables(stmt); + + String[] dateNormalCase = {GMTDate + ".18", GMTDate + ".1770000", dateTimeOffsetExpectedValue + ".1770000 +00:01", + GMTDateWithoutDate + ".1770000", GMTDateWithoutDate + ".18", dateTimeOffsetExpectedValue + ".18 +00:01"}; + String[] dateSetObject = {GMTDate + ".18", GMTDate + ".177", dateTimeOffsetExpectedValue + ".177 +00:01", GMTDateWithoutDate, + GMTDateWithoutDate, dateTimeOffsetExpectedValue + ".18 +00:01"}; + + createDatePrecisionTable(2); + populateDateNormalCase(2); + populateDateSetObject(2); + + testDate(dateNormalCase, dateSetObject); + } + + @Test + public void testNumericPrecision8Scale0() throws Exception { + dropTables(stmt); + + String[] numeric2 = {"1.12345", "12345", "567"}; + + createNumericPrecisionTable(30, 8, 0); + populateNumericNormalCase(numeric2, 8, 0); + populateNumericSetObject(numeric2, 8, 0); + + testNumeric(numeric2); + } + + @Test + public void testDateScale0() throws Exception { + dropTables(stmt); + + String[] dateNormalCase2 = {GMTDate, GMTDate + ".1770000", dateTimeOffsetExpectedValue + ".1770000 +00:01", GMTDateWithoutDate + ".1770000", + GMTDateWithoutDate, dateTimeOffsetExpectedValue + " +00:01"}; + String[] dateSetObject2 = {GMTDate + ".0", GMTDate + ".177", dateTimeOffsetExpectedValue + ".177 +00:01", GMTDateWithoutDate, + GMTDateWithoutDate, dateTimeOffsetExpectedValue + " +00:01"}; + + createDatePrecisionTable(0); + populateDateNormalCase(0); + populateDateSetObject(0); + + testDate(dateNormalCase2, dateSetObject2); + } + + @Test + public void testNumericPrecision8Scale2Null() throws Exception { + dropTables(stmt); + + String[] numericNull = {"null", "null", "null"}; + + createNumericPrecisionTable(30, 8, 2); + populateNumericSetObjectNull(8, 2); + + testNumeric(numericNull); + } + + @Test + public void testDateScale2Null() throws Exception { + dropTables(stmt); + + String[] dateSetObjectNull = {"null", "null", "null", "null", "null", "null"}; + + createDatePrecisionTable(2); + populateDateSetObjectNull(2); + + testDate(dateSetObjectNull, dateSetObjectNull); + } + + @Test + public void testDateScale5Null() throws Exception { + dropTables(stmt); + + String[] dateSetObjectNull = {"null", "null", "null", "null", "null", "null"}; + + createDatePrecisionTable(5); + populateDateNormalCaseNull(5); + testDate(dateSetObjectNull, dateSetObjectNull); + } + + private void testNumeric(String[] numeric) throws SQLException { + + try(ResultSet rs = stmt.executeQuery("select * from " + numericTable)) { + int numberOfColumns = rs.getMetaData().getColumnCount(); + + ArrayList skipMax = new ArrayList<>(); + + while (rs.next()) { + testGetString(rs, numberOfColumns, skipMax, numeric); + testGetBigDecimal(rs, numberOfColumns, numeric); + testGetObject(rs, numberOfColumns, skipMax, numeric); + } + } + } + + private void testDate(String[] dateNormalCase, + String[] dateSetObject) throws Exception { + + try(ResultSet rs = stmt.executeQuery("select * from " + dateTable)) { + int numberOfColumns = rs.getMetaData().getColumnCount(); + + ArrayList skipMax = new ArrayList<>(); + + while (rs.next()) { + testGetString(rs, numberOfColumns, skipMax, dateNormalCase); + testGetObject(rs, numberOfColumns, skipMax, dateSetObject); + testGetDate(rs, numberOfColumns, dateSetObject); + } + } + } + + private void testGetString(ResultSet rs, + int numberOfColumns, + ArrayList skipMax, + String[] values) throws SQLException { + int index = 0; + for (int i = 1; i <= numberOfColumns; i = i + 3) { + if (skipMax.contains(i)) { + continue; + } + + String stringValue1 = "" + rs.getString(i); + String stringValue2 = "" + rs.getString(i + 1); + String stringValue3 = "" + rs.getString(i + 2); + + try { + if (rs.getMetaData().getColumnTypeName(i).equalsIgnoreCase("time")) { + assertTrue(stringValue2.equalsIgnoreCase("" + values[index]) && stringValue3.equalsIgnoreCase("" + values[index]), + "\nDecryption failed with getString(): " + stringValue1 + ", " + stringValue2 + ", " + stringValue3 + + ".\nExpected Value: " + values[index]); + } + else { + assertTrue( + values[index].contains(stringValue1) && stringValue2.equalsIgnoreCase("" + values[index]) + && stringValue3.equalsIgnoreCase("" + values[index]), + "\nDecryption failed with getString(): " + stringValue1 + ", " + stringValue2 + ", " + stringValue3 + + ".\nExpected Value: " + values[index]); + } + } + finally { + index++; + } + } + } + + private void testGetBigDecimal(ResultSet rs, + int numberOfColumns, + String[] values) throws SQLException { + int index = 0; + for (int i = 1; i <= numberOfColumns; i = i + 3) { + + String decimalValue1 = "" + rs.getBigDecimal(i); + String decimalValue2 = "" + rs.getBigDecimal(i + 1); + String decimalValue3 = "" + rs.getBigDecimal(i + 2); + + try { + assertTrue( + decimalValue1.equalsIgnoreCase(values[index]) && decimalValue2.equalsIgnoreCase(values[index]) + && decimalValue3.equalsIgnoreCase(values[index]), + "Decryption failed with getBigDecimal(): " + decimalValue1 + ", " + decimalValue2 + ", " + decimalValue3 + + "\nExpected value: " + values[index]); + + } + finally { + index++; + } + } + } + + private void testGetObject(ResultSet rs, + int numberOfColumns, + ArrayList skipMax, + String[] values) throws SQLException { + int index = 0; + for (int i = 1; i <= numberOfColumns; i = i + 3) { + if (skipMax.contains(i)) { + continue; + } + + try { + String objectValue1 = "" + rs.getObject(i); + String objectValue2 = "" + rs.getObject(i + 1); + String objectValue3 = "" + rs.getObject(i + 2); + + assertTrue( + objectValue1.equalsIgnoreCase(values[index]) && objectValue2.equalsIgnoreCase(values[index]) + && objectValue3.equalsIgnoreCase(values[index]), + "Decryption failed with getObject(): " + objectValue1 + ", " + objectValue2 + ", " + objectValue3 + "\nExpected value: " + + values[index]); + + } + finally { + index++; + } + } + } + + private void testGetDate(ResultSet rs, + int numberOfColumns, + String[] dates) throws Exception { + int index = 0; + for (int i = 1; i <= numberOfColumns; i = i + 3) { + + if (rs instanceof SQLServerResultSet) { + + String stringValue1 = null; + String stringValue2 = null; + String stringValue3 = null; + + switch (i) { + + case 1: + stringValue1 = "" + ((SQLServerResultSet) rs).getTimestamp(i); + stringValue2 = "" + ((SQLServerResultSet) rs).getTimestamp(i + 1); + stringValue3 = "" + ((SQLServerResultSet) rs).getTimestamp(i + 2); + break; + + case 4: + stringValue1 = "" + ((SQLServerResultSet) rs).getTimestamp(i); + stringValue2 = "" + ((SQLServerResultSet) rs).getTimestamp(i + 1); + stringValue3 = "" + ((SQLServerResultSet) rs).getTimestamp(i + 2); + break; + + case 7: + stringValue1 = "" + ((SQLServerResultSet) rs).getDateTimeOffset(i); + stringValue2 = "" + ((SQLServerResultSet) rs).getDateTimeOffset(i + 1); + stringValue3 = "" + ((SQLServerResultSet) rs).getDateTimeOffset(i + 2); + break; + + case 10: + stringValue1 = "" + ((SQLServerResultSet) rs).getTime(i); + stringValue2 = "" + ((SQLServerResultSet) rs).getTime(i + 1); + stringValue3 = "" + ((SQLServerResultSet) rs).getTime(i + 2); + break; + + case 13: + stringValue1 = "" + ((SQLServerResultSet) rs).getTime(i); + stringValue2 = "" + ((SQLServerResultSet) rs).getTime(i + 1); + stringValue3 = "" + ((SQLServerResultSet) rs).getTime(i + 2); + break; + + case 16: + stringValue1 = "" + ((SQLServerResultSet) rs).getDateTimeOffset(i); + stringValue2 = "" + ((SQLServerResultSet) rs).getDateTimeOffset(i + 1); + stringValue3 = "" + ((SQLServerResultSet) rs).getDateTimeOffset(i + 2); + break; + + default: + fail("Switch case is not matched with data"); + } + + try { + assertTrue( + stringValue1.equalsIgnoreCase(dates[index]) && stringValue2.equalsIgnoreCase(dates[index]) + && stringValue3.equalsIgnoreCase(dates[index]), + "Decryption failed with getString(): " + stringValue1 + ", " + stringValue2 + ", " + stringValue3 + "\nExpected value: " + + dates[index]); + } + finally { + index++; + } + } + + else { + throw new Exception("Result set is not instance of SQLServerResultSet"); + } + } + } + + private void populateDateNormalCase(int scale) throws SQLException { + String sql = "insert into " + dateTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // datetime2(5) + for (int i = 1; i <= 3; i++) { + pstmt.setTimestamp(i, new Timestamp(date.getTime()), scale); + } + + // datetime2 default + for (int i = 4; i <= 6; i++) { + pstmt.setTimestamp(i, new Timestamp(date.getTime())); + } + + // datetimeoffset default + for (int i = 7; i <= 9; i++) { + pstmt.setDateTimeOffset(i, microsoft.sql.DateTimeOffset.valueOf(new Timestamp(date.getTime()), 1)); + } + + // time default + for (int i = 10; i <= 12; i++) { + pstmt.setTime(i, new Time(date.getTime())); + } + + // time(3) + for (int i = 13; i <= 15; i++) { + pstmt.setTime(i, new Time(date.getTime()), scale); + } + + // datetimeoffset(2) + for (int i = 16; i <= 18; i++) { + pstmt.setDateTimeOffset(i, microsoft.sql.DateTimeOffset.valueOf(new Timestamp(date.getTime()), 1), scale); + } + + pstmt.execute(); + } + } + + private void populateDateNormalCaseNull(int scale) throws SQLException { + String sql = "insert into " + dateTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // datetime2(5) + for (int i = 1; i <= 3; i++) { + pstmt.setTimestamp(i, null, scale); + } + + // datetime2 default + for (int i = 4; i <= 6; i++) { + pstmt.setTimestamp(i, null); + } + + // datetimeoffset default + for (int i = 7; i <= 9; i++) { + pstmt.setDateTimeOffset(i, null); + } + + // time default + for (int i = 10; i <= 12; i++) { + pstmt.setTime(i, null); + } + + // time(3) + for (int i = 13; i <= 15; i++) { + pstmt.setTime(i, null, scale); + } + + // datetimeoffset(2) + for (int i = 16; i <= 18; i++) { + pstmt.setDateTimeOffset(i, null, scale); + } + + pstmt.execute(); + } + } + + private void populateNumericNormalCase(String[] numeric, + int precision, + int scale) throws SQLException { + String sql = "insert into " + numericTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // float(30) + for (int i = 1; i <= 3; i++) { + pstmt.setDouble(i, Double.valueOf(numeric[0])); + } + + // decimal(10,5) + for (int i = 4; i <= 6; i++) { + pstmt.setBigDecimal(i, new BigDecimal(numeric[1]), precision, scale); + } + + // numeric(8,2) + for (int i = 7; i <= 9; i++) { + pstmt.setBigDecimal(i, new BigDecimal(numeric[2]), precision, scale); + } + + pstmt.execute(); + } + } + + private void populateNumericSetObject(String[] numeric, + int precision, + int scale) throws SQLException { + String sql = "insert into " + numericTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // float(30) + for (int i = 1; i <= 3; i++) { + pstmt.setObject(i, Double.valueOf(numeric[0])); + + } + + // decimal(10,5) + for (int i = 4; i <= 6; i++) { + pstmt.setObject(i, new BigDecimal(numeric[1]), java.sql.Types.DECIMAL, precision, scale); + } + + // numeric(8,2) + for (int i = 7; i <= 9; i++) { + pstmt.setObject(i, new BigDecimal(numeric[2]), java.sql.Types.NUMERIC, precision, scale); + } + + pstmt.execute(); + } + } + + private void populateNumericSetObjectNull(int precision, + int scale) throws SQLException { + String sql = "insert into " + numericTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // float(30) + for (int i = 1; i <= 3; i++) { + pstmt.setObject(i, null, java.sql.Types.DOUBLE); + + } + + // decimal(10,5) + for (int i = 4; i <= 6; i++) { + pstmt.setObject(i, null, java.sql.Types.DECIMAL, precision, scale); + } + + // numeric(8,2) + for (int i = 7; i <= 9; i++) { + pstmt.setObject(i, null, java.sql.Types.NUMERIC, precision, scale); + } + + pstmt.execute(); + } + } + + private void populateDateSetObject(int scale) throws SQLException { + String sql = "insert into " + dateTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // datetime2(5) + for (int i = 1; i <= 3; i++) { + pstmt.setObject(i, new Timestamp(date.getTime()), java.sql.Types.TIMESTAMP, scale); + } + + // datetime2 default + for (int i = 4; i <= 6; i++) { + pstmt.setObject(i, new Timestamp(date.getTime()), java.sql.Types.TIMESTAMP); + } + + // datetimeoffset default + for (int i = 7; i <= 9; i++) { + pstmt.setObject(i, microsoft.sql.DateTimeOffset.valueOf(new Timestamp(date.getTime()), 1), microsoft.sql.Types.DATETIMEOFFSET); + } + + // time default + for (int i = 10; i <= 12; i++) { + pstmt.setObject(i, new Time(date.getTime()), java.sql.Types.TIME); + } + + // time(3) + for (int i = 13; i <= 15; i++) { + pstmt.setObject(i, new Time(date.getTime()), java.sql.Types.TIME, scale); + } + + // datetimeoffset(2) + for (int i = 16; i <= 18; i++) { + pstmt.setObject(i, microsoft.sql.DateTimeOffset.valueOf(new Timestamp(date.getTime()), 1), microsoft.sql.Types.DATETIMEOFFSET, scale); + } + + pstmt.execute(); + } + } + + private void populateDateSetObjectNull(int scale) throws SQLException { + String sql = "insert into " + dateTable + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + ")"; + + try(SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) Util.getPreparedStmt(con, sql, stmtColEncSetting)) { + + // datetime2(5) + for (int i = 1; i <= 3; i++) { + pstmt.setObject(i, null, java.sql.Types.TIMESTAMP, scale); + } + + // datetime2 default + for (int i = 4; i <= 6; i++) { + pstmt.setObject(i, null, java.sql.Types.TIMESTAMP); + } + + // datetimeoffset default + for (int i = 7; i <= 9; i++) { + pstmt.setObject(i, null, microsoft.sql.Types.DATETIMEOFFSET); + } + + // time default + for (int i = 10; i <= 12; i++) { + pstmt.setObject(i, null, java.sql.Types.TIME); + } + + // time(3) + for (int i = 13; i <= 15; i++) { + pstmt.setObject(i, null, java.sql.Types.TIME, scale); + } + + // datetimeoffset(2) + for (int i = 16; i <= 18; i++) { + pstmt.setObject(i, null, microsoft.sql.Types.DATETIMEOFFSET, scale); + } + + pstmt.execute(); + } + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/UtilTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/UtilTest.java new file mode 100644 index 000000000..85f27900b --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/UtilTest.java @@ -0,0 +1,32 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc; + +import static org.junit.Assert.assertEquals; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +/** + * Tests the Util class + * + */ +@RunWith(JUnitPlatform.class) +public class UtilTest { + + @Test + public void readGUIDtoUUID() throws SQLServerException { + UUID expected = UUID.fromString("6F9619FF-8B86-D011-B42D-00C04FC964FF"); + byte[] guid = new byte[] {-1, 25, -106, 111, -122, -117, 17, -48, -76, 45, 0, -64, 79, -55, 100, -1}; + assertEquals(expected, Util.readGUIDtoUUID(guid)); + } + +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyAllTypes.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyAllTypes.java new file mode 100644 index 000000000..7782a86b7 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyAllTypes.java @@ -0,0 +1,93 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.bulkCopy; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerBulkCopy; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.DBConnection; +import com.microsoft.sqlserver.testframework.DBStatement; +import com.microsoft.sqlserver.testframework.DBTable; +import com.microsoft.sqlserver.testframework.Utils; +import com.microsoft.sqlserver.testframework.util.ComparisonUtil; + +@RunWith(JUnitPlatform.class) +public class BulkCopyAllTypes extends AbstractTest { + + private static DBTable tableSrc = null; + private static DBTable tableDest = null; + + /** + * Test TVP with result set + * + * @throws SQLException + */ + @Test + public void testTVPResultSet() throws SQLException { + testBulkCopyResultSet(false, null, null); + testBulkCopyResultSet(true, null, null); + testBulkCopyResultSet(false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + testBulkCopyResultSet(false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE); + testBulkCopyResultSet(false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); + testBulkCopyResultSet(false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); + } + + private void testBulkCopyResultSet(boolean setSelectMethod, + Integer resultSetType, + Integer resultSetConcurrency) throws SQLException { + setupVariation(); + + try(Connection connnection = DriverManager.getConnection(connectionString + (setSelectMethod ? ";selectMethod=cursor;" : "")); + Statement statement = (null != resultSetType || null != resultSetConcurrency) ? + connnection.createStatement(resultSetType, resultSetConcurrency) : connnection.createStatement()){ + + ResultSet rs = statement.executeQuery("select * from " + tableSrc.getEscapedTableName()); + + SQLServerBulkCopy bcOperation = new SQLServerBulkCopy(connection); + bcOperation.setDestinationTableName(tableDest.getEscapedTableName()); + bcOperation.writeToServer(rs); + bcOperation.close(); + + ComparisonUtil.compareSrcTableAndDestTableIgnoreRowOrder(new DBConnection(connectionString), tableSrc, tableDest); + } + + terminateVariation(); + } + + private void setupVariation() throws SQLException { + try(DBConnection dbConnection = new DBConnection(connectionString); + DBStatement dbStmt = dbConnection.createStatement()) { + + tableSrc = new DBTable(true); + tableDest = tableSrc.cloneSchema(); + + dbStmt.createTable(tableSrc); + dbStmt.createTable(tableDest); + + dbStmt.populateTable(tableSrc); + } + } + + private void terminateVariation() throws SQLException { + try(Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement()) { + + Utils.dropTableIfExists(tableSrc.getEscapedTableName(), stmt); + Utils.dropTableIfExists(tableDest.getEscapedTableName(), stmt); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyCSVTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyCSVTest.java index 324748fc0..ebf311225 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyCSVTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyCSVTest.java @@ -11,8 +11,9 @@ import java.io.BufferedReader; import java.io.FileInputStream; +import java.io.InputStream; import java.io.InputStreamReader; -import java.net.URI; +import java.net.URL; import java.sql.Connection; import java.sql.ResultSet; import java.sql.ResultSetMetaData; @@ -29,12 +30,15 @@ import com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord; import com.microsoft.sqlserver.jdbc.SQLServerBulkCSVFileRecord; import com.microsoft.sqlserver.jdbc.SQLServerBulkCopy; +import com.microsoft.sqlserver.jdbc.SQLServerException; import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.DBConnection; import com.microsoft.sqlserver.testframework.DBResultSet; import com.microsoft.sqlserver.testframework.DBStatement; import com.microsoft.sqlserver.testframework.DBTable; +import com.microsoft.sqlserver.testframework.Utils; import com.microsoft.sqlserver.testframework.sqlType.SqlType; +import com.microsoft.sqlserver.testframework.util.ComparisonUtil; /** * Test bulkcopy with CSV file input @@ -49,6 +53,7 @@ public class BulkCopyCSVTest extends AbstractTest { static String inputFile = "BulkCopyCSVTestInput.csv"; + static String inputFileNoColumnName = "BulkCopyCSVTestInputNoColumnName.csv"; static String encoding = "UTF-8"; static String delimiter = ","; @@ -63,7 +68,7 @@ public class BulkCopyCSVTest extends AbstractTest { static void setUpConnection() { con = new DBConnection(connectionString); stmt = con.createStatement(); - filePath = getCurrentClassPath(); + filePath = Utils.getCurrentClassPath(); } /** @@ -72,56 +77,100 @@ static void setUpConnection() { @Test @DisplayName("Test SQLServerBulkCSVFileRecord") void testCSV() { + try (SQLServerBulkCSVFileRecord fileRecord = new SQLServerBulkCSVFileRecord(filePath + inputFile, encoding, delimiter, true)) { + testBulkCopyCSV(fileRecord, true); + } + catch (SQLServerException e) { + fail(e.getMessage()); + } + } + + /** + * test simple csv file for bulkcopy first line not being column name + */ + @Test + @DisplayName("Test SQLServerBulkCSVFileRecord First line not being column name") + void testCSVFirstLineNotColumnName() { + try (SQLServerBulkCSVFileRecord fileRecord = new SQLServerBulkCSVFileRecord(filePath + inputFileNoColumnName, encoding, delimiter, false)) { + testBulkCopyCSV(fileRecord, false); + } + catch (SQLServerException e) { + fail(e.getMessage()); + } + } + + /** + * test simple csv file for bulkcopy by passing a file from url + * + * @throws SQLException + */ + @Test + @DisplayName("Test SQLServerBulkCSVFileRecord with passing file from url") + void testCSVFromURL() throws SQLException { + try (InputStream csvFileInputStream = new URL( + "https://raw.githubusercontent.com/Microsoft/mssql-jdbc/master/src/test/resources/BulkCopyCSVTestInput.csv").openStream(); + SQLServerBulkCSVFileRecord fileRecord = new SQLServerBulkCSVFileRecord(csvFileInputStream, encoding, delimiter, true)) { + testBulkCopyCSV(fileRecord, true); + } + catch (Exception e) { + fail(e.getMessage()); + } + } + + private void testBulkCopyCSV(SQLServerBulkCSVFileRecord fileRecord, + boolean firstLineIsColumnNames) { DBTable destTable = null; - try { - BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath + inputFile), encoding)); + try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath + inputFile), encoding))) { // read the first line from csv and parse it to get datatypes to create destination column String[] columnTypes = br.readLine().substring(1)/* Skip the Byte order mark */.split(delimiter, -1); br.close(); int numberOfColumns = columnTypes.length; destTable = new DBTable(false); - SQLServerBulkCSVFileRecord fileRecord = new SQLServerBulkCSVFileRecord(filePath + inputFile, encoding, delimiter, true); - SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy((Connection) con.product()); - bulkCopy.setDestinationTableName(destTable.getEscapedTableName()); - - // add a column in destTable for each datatype in csv - for (int i = 0; i < numberOfColumns; i++) { - SqlType sqlType = null; - int precision = -1; - int scale = -1; - - String columnType = columnTypes[i].trim().toLowerCase(); - int indexOpenParenthesis = columnType.lastIndexOf("("); - // skip the parenthesis in case of precision and scale type - if (-1 != indexOpenParenthesis) { - String precision_scale = columnType.substring(indexOpenParenthesis + 1, columnType.length() - 1); - columnType = columnType.substring(0, indexOpenParenthesis); - sqlType = SqlTypeMapping.valueOf(columnType.toUpperCase()).sqlType; - - // add scale if exist - int indexPrecisionScaleSeparator = precision_scale.indexOf("-"); - if (-1 != indexPrecisionScaleSeparator) { - scale = Integer.parseInt(precision_scale.substring(indexPrecisionScaleSeparator + 1)); - sqlType.setScale(scale); - precision_scale = precision_scale.substring(0, indexPrecisionScaleSeparator); - } - // add precision - precision = Integer.parseInt(precision_scale); - sqlType.setPrecision(precision); - } - else { - sqlType = SqlTypeMapping.valueOf(columnType.toUpperCase()).sqlType; - } - - destTable.addColumn(sqlType); - fileRecord.addColumnMetadata(i + 1, "", sqlType.getJdbctype().getVendorTypeNumber(), (-1 == precision) ? 0 : precision, - (-1 == scale) ? 0 : scale); + try (SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy((Connection) con.product())) { + bulkCopy.setDestinationTableName(destTable.getEscapedTableName()); + + // add a column in destTable for each datatype in csv + for (int i = 0; i < numberOfColumns; i++) { + SqlType sqlType = null; + int precision = -1; + int scale = -1; + + String columnType = columnTypes[i].trim().toLowerCase(); + int indexOpenParenthesis = columnType.lastIndexOf("("); + // skip the parenthesis in case of precision and scale type + if (-1 != indexOpenParenthesis) { + String precision_scale = columnType.substring(indexOpenParenthesis + 1, columnType.length() - 1); + columnType = columnType.substring(0, indexOpenParenthesis); + sqlType = SqlTypeMapping.valueOf(columnType.toUpperCase()).sqlType; + + // add scale if exist + int indexPrecisionScaleSeparator = precision_scale.indexOf("-"); + if (-1 != indexPrecisionScaleSeparator) { + scale = Integer.parseInt(precision_scale.substring(indexPrecisionScaleSeparator + 1)); + sqlType.setScale(scale); + precision_scale = precision_scale.substring(0, indexPrecisionScaleSeparator); + } + // add precision + precision = Integer.parseInt(precision_scale); + sqlType.setPrecision(precision); + } + else { + sqlType = SqlTypeMapping.valueOf(columnType.toUpperCase()).sqlType; + } + + destTable.addColumn(sqlType); + fileRecord.addColumnMetadata(i + 1, "", sqlType.getJdbctype().getVendorTypeNumber(), (-1 == precision) ? 0 : precision, + (-1 == scale) ? 0 : scale); + } + stmt.createTable(destTable); + bulkCopy.writeToServer((ISQLServerBulkRecord) fileRecord); } - stmt.createTable(destTable); - bulkCopy.writeToServer((ISQLServerBulkRecord) fileRecord); - bulkCopy.close(); - validateValuesFromCSV(destTable); + if (firstLineIsColumnNames) + validateValuesFromCSV(destTable, inputFile); + else + validateValuesFromCSV(destTable, inputFileNoColumnName); + } catch (Exception e) { fail(e.getMessage()); @@ -133,54 +182,35 @@ void testCSV() { } } - /** - * - * @return location of resource file - */ - static String getCurrentClassPath() { - - try { - String className = new Object() { - }.getClass().getEnclosingClass().getName(); - String location = Class.forName(className).getProtectionDomain().getCodeSource().getLocation().getPath()+ "/"; - URI uri = new URI(location.toString()); - return uri.getPath(); - } - catch (Exception e) { - fail("Failed to get CSV file path. " + e.getMessage()); - } - return null; - } - /** * validate value in csv and in destination table as string * * @param destinationTable */ - static void validateValuesFromCSV(DBTable destinationTable) { - BufferedReader br; - try { - br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath + inputFile), encoding)); - br.readLine(); // skip first line as it is header - - DBResultSet dstResultSet = stmt.executeQuery("SELECT * FROM " + destinationTable.getEscapedTableName() + ";"); - ResultSetMetaData destMeta = ((ResultSet) dstResultSet.product()).getMetaData(); - int totalColumns = destMeta.getColumnCount(); - while (dstResultSet.next()) { - String[] srcValues = br.readLine().split(delimiter); - if ((0 == srcValues.length) && (srcValues.length != totalColumns)) { - srcValues = new String[totalColumns]; - Arrays.fill(srcValues, null); - } - for (int i = 1; i <= totalColumns; i++) { - String srcValue = srcValues[i - 1]; - String dstValue = dstResultSet.getString(i); - srcValue = (null != srcValue) ? srcValue.trim() : srcValue; - dstValue = (null != dstValue) ? dstValue.trim() : dstValue; - - // get the value from csv as string and compare them - BulkCopyTestUtil.comapreSourceDest(java.sql.Types.VARCHAR, srcValue, dstValue); - } + static void validateValuesFromCSV(DBTable destinationTable, + String inputFile) { + try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath + inputFile), encoding))) { + if (inputFile.equalsIgnoreCase("BulkCopyCSVTestInput.csv")) + br.readLine(); // skip first line as it is header + + try (DBResultSet dstResultSet = stmt.executeQuery("SELECT * FROM " + destinationTable.getEscapedTableName() + ";")) { + ResultSetMetaData destMeta = ((ResultSet) dstResultSet.product()).getMetaData(); + int totalColumns = destMeta.getColumnCount(); + while (dstResultSet.next()) { + String[] srcValues = br.readLine().split(delimiter); + if ((0 == srcValues.length) && (srcValues.length != totalColumns)) { + srcValues = new String[totalColumns]; + Arrays.fill(srcValues, null); + } + for (int i = 1; i <= totalColumns; i++) { + String srcValue = srcValues[i - 1]; + String dstValue = dstResultSet.getString(i); + srcValue = (null != srcValue) ? srcValue.trim() : srcValue; + dstValue = (null != dstValue) ? dstValue.trim() : dstValue; + // get the value from csv as string and compare them + ComparisonUtil.compareExpectedAndActual(java.sql.Types.VARCHAR, srcValue, dstValue); + } + } } } catch (Exception e) { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyColumnMappingTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyColumnMappingTest.java index 6cab9e707..2de49b360 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyColumnMappingTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyColumnMappingTest.java @@ -26,6 +26,7 @@ import com.microsoft.sqlserver.testframework.DBStatement; import com.microsoft.sqlserver.testframework.DBTable; import com.microsoft.sqlserver.testframework.sqlType.SqlType; +import com.microsoft.sqlserver.testframework.util.ComparisonUtil; /** * Test BulkCopy Column Mapping @@ -331,31 +332,32 @@ void testInvalidCM() { private void validateValuesRepetativeCM(DBConnection con, DBTable sourceTable, DBTable destinationTable) throws SQLException { - DBStatement srcStmt = con.createStatement(); - DBStatement dstStmt = con.createStatement(); - DBResultSet srcResultSet = srcStmt.executeQuery("SELECT * FROM " + sourceTable.getEscapedTableName() + ";"); - DBResultSet dstResultSet = dstStmt.executeQuery("SELECT * FROM " + destinationTable.getEscapedTableName() + ";"); - ResultSetMetaData sourceMeta = ((ResultSet) srcResultSet.product()).getMetaData(); - int totalColumns = sourceMeta.getColumnCount(); - - // verify data from sourceType and resultSet - while (srcResultSet.next() && dstResultSet.next()) - for (int i = 1; i <= totalColumns; i++) { - // TODO: check row and column count in both the tables - - Object srcValue, dstValue; - srcValue = srcResultSet.getObject(i); - dstValue = dstResultSet.getObject(i); - BulkCopyTestUtil.comapreSourceDest(sourceMeta.getColumnType(i), srcValue, dstValue); - - // compare value of first column of source with extra column in destination - if (1 == i) { - Object srcValueFirstCol = srcResultSet.getObject(i); - Object dstValLastCol = dstResultSet.getObject(totalColumns + 1); - BulkCopyTestUtil.comapreSourceDest(sourceMeta.getColumnType(i), srcValueFirstCol, dstValLastCol); - } - } - + try(DBStatement srcStmt = con.createStatement(); + DBStatement dstStmt = con.createStatement(); + DBResultSet srcResultSet = srcStmt.executeQuery("SELECT * FROM " + sourceTable.getEscapedTableName() + ";"); + DBResultSet dstResultSet = dstStmt.executeQuery("SELECT * FROM " + destinationTable.getEscapedTableName() + ";")) { + ResultSetMetaData sourceMeta = ((ResultSet) srcResultSet.product()).getMetaData(); + int totalColumns = sourceMeta.getColumnCount(); + + // verify data from sourceType and resultSet + while (srcResultSet.next() && dstResultSet.next()) { + for (int i = 1; i <= totalColumns; i++) { + // TODO: check row and column count in both the tables + + Object srcValue, dstValue; + srcValue = srcResultSet.getObject(i); + dstValue = dstResultSet.getObject(i); + ComparisonUtil.compareExpectedAndActual(sourceMeta.getColumnType(i), srcValue, dstValue); + + // compare value of first column of source with extra column in destination + if (1 == i) { + Object srcValueFirstCol = srcResultSet.getObject(i); + Object dstValLastCol = dstResultSet.getObject(totalColumns + 1); + ComparisonUtil.compareExpectedAndActual(sourceMeta.getColumnType(i), srcValueFirstCol, dstValLastCol); + } + } + } + } } private void dropTable(String tableName) { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyConnectionTest.java index 0e2718fe3..f56039813 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyConnectionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyConnectionTest.java @@ -12,6 +12,7 @@ import java.lang.reflect.Method; import java.sql.Connection; +import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ThreadLocalRandom; @@ -44,7 +45,7 @@ public class BulkCopyConnectionTest extends BulkCopyTestSetUp { */ @TestFactory Stream generateBulkCopyConstructorTest() { - List testData = createTestData_testBulkCopyConstructor(); + List testData = createTestDatatestBulkCopyConstructor(); // had to avoid using lambdas as we need to test against java7 return testData.stream().map(new Function() { @Override @@ -66,7 +67,7 @@ public void execute() { */ @TestFactory Stream generateBulkCopyOptionsTest() { - List testData = createTestData_testBulkCopyOption(); + List testData = createTestDatatestBulkCopyOption(); return testData.stream().map(new Function() { @Override public DynamicTest apply(final BulkCopyTestWrapper datum) { @@ -85,12 +86,14 @@ public void execute() { */ @Test @DisplayName("BulkCopy:test uninitialized Connection") - void testInvalidConnection_1() { + void testInvalidConnection1() { assertThrows(SQLServerException.class, new org.junit.jupiter.api.function.Executable() { @Override - public void execute() throws SQLServerException { - Connection con = null; - SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + public void execute() throws SQLException { + try(Connection con = null; + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con)) { + //do nothing + } } }); } @@ -100,12 +103,14 @@ public void execute() throws SQLServerException { */ @Test @DisplayName("BulkCopy:test uninitialized SQLServerConnection") - void testInvalidConnection_2() { + void testInvalidConnection2() { assertThrows(SQLServerException.class, new org.junit.jupiter.api.function.Executable() { @Override public void execute() throws SQLServerException { - SQLServerConnection con = null; - SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + try(SQLServerConnection con = null; + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con)) { + //do nothing + } } }); } @@ -115,12 +120,14 @@ public void execute() throws SQLServerException { */ @Test @DisplayName("BulkCopy:test empty connenction string") - void testInvalidConnection_3() { + void testInvalidConnection3() { assertThrows(SQLServerException.class, new org.junit.jupiter.api.function.Executable() { @Override public void execute() throws SQLServerException { String connectionUrl = " "; - SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(connectionUrl); + try(SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(connectionUrl)) { + //do nothing + } } }); } @@ -130,12 +137,14 @@ public void execute() throws SQLServerException { */ @Test @DisplayName("BulkCopy:test null connenction string") - void testInvalidConnection_4() { + void testInvalidConnection4() { assertThrows(SQLServerException.class, new org.junit.jupiter.api.function.Executable() { @Override public void execute() throws SQLServerException { String connectionUrl = null; - SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(connectionUrl); + try(SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(connectionUrl)) { + //do nothing + } } }); } @@ -159,9 +168,9 @@ void testEmptyBulkCopyOptions() { * * @return */ - List createTestData_testBulkCopyConstructor() { + List createTestDatatestBulkCopyConstructor() { String testCaseName = "BulkCopyConstructor "; - List testData = new ArrayList(); + List testData = new ArrayList<>(); BulkCopyTestWrapper bulkWrapper1 = new BulkCopyTestWrapper(connectionString); bulkWrapper1.testName = testCaseName; bulkWrapper1.setUsingConnection(true); @@ -180,16 +189,16 @@ List createTestData_testBulkCopyConstructor() { * * @return */ - private List createTestData_testBulkCopyOption() { + private List createTestDatatestBulkCopyOption() { String testCaseName = "BulkCopyOption "; - List testData = new ArrayList(); + List testData = new ArrayList<>(); Class bulkOptions = SQLServerBulkCopyOptions.class; Method[] methods = bulkOptions.getDeclaredMethods(); - for (int i = 0; i < methods.length; i++) { + for (Method method : methods) { // set bulkCopy Option if return is void and input is boolean - if (0 != methods[i].getParameterTypes().length && boolean.class == methods[i].getParameterTypes()[0]) { + if (0 != method.getParameterTypes().length && boolean.class == method.getParameterTypes()[0]) { try { BulkCopyTestWrapper bulkWrapper = new BulkCopyTestWrapper(connectionString); @@ -197,16 +206,15 @@ private List createTestData_testBulkCopyOption() { bulkWrapper.setUsingConnection((0 == ThreadLocalRandom.current().nextInt(2)) ? true : false); SQLServerBulkCopyOptions option = new SQLServerBulkCopyOptions(); - if (!(methods[i].getName()).equalsIgnoreCase("setUseInternalTransaction") - && !(methods[i].getName()).equalsIgnoreCase("setAllowEncryptedValueModifications")) { - methods[i].invoke(option, true); + if (!(method.getName()).equalsIgnoreCase("setUseInternalTransaction") + && !(method.getName()).equalsIgnoreCase("setAllowEncryptedValueModifications")) { + method.invoke(option, true); bulkWrapper.useBulkCopyOptions(true); bulkWrapper.setBulkOptions(option); - bulkWrapper.testName += methods[i].getName() + ";"; + bulkWrapper.testName += method.getName() + ";"; testData.add(bulkWrapper); } - } - catch (Exception ex) { + } catch (Exception ex) { fail(ex.getMessage()); } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyISQLServerBulkRecordTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyISQLServerBulkRecordTest.java new file mode 100644 index 000000000..1be4ad850 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyISQLServerBulkRecordTest.java @@ -0,0 +1,204 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.bulkCopy; + +import java.sql.JDBCType; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.DBConnection; +import com.microsoft.sqlserver.testframework.DBStatement; +import com.microsoft.sqlserver.testframework.DBTable; +import com.microsoft.sqlserver.testframework.sqlType.SqlType; + +/** + * Test bulkcopy decimal sacle and precision + */ +@RunWith(JUnitPlatform.class) +@DisplayName("Test ISQLServerBulkRecord") +public class BulkCopyISQLServerBulkRecordTest extends AbstractTest { + + @Test + void testISQLServerBulkRecord() throws SQLException { + try (DBConnection con = new DBConnection(connectionString); + DBStatement stmt = con.createStatement()) { + DBTable dstTable = new DBTable(true); + stmt.createTable(dstTable); + BulkData Bdata = new BulkData(dstTable); + + BulkCopyTestWrapper bulkWrapper = new BulkCopyTestWrapper(connectionString); + bulkWrapper.setUsingConnection((0 == ThreadLocalRandom.current().nextInt(2)) ? true : false); + BulkCopyTestUtil.performBulkCopy(bulkWrapper, Bdata, dstTable); + } + } + + class BulkData implements ISQLServerBulkRecord { + + private class ColumnMetadata { + String columnName; + int columnType; + int precision; + int scale; + + ColumnMetadata(String name, + int type, + int precision, + int scale) { + columnName = name; + columnType = type; + this.precision = precision; + this.scale = scale; + } + } + + int totalColumn = 0; + int counter = 0; + int rowCount = 1; + Map columnMetadata; + List data; + + BulkData(DBTable dstTable) { + columnMetadata = new HashMap<>(); + totalColumn = dstTable.totalColumns(); + + // add metadata + for (int i = 0; i < totalColumn; i++) { + SqlType sqlType = dstTable.getSqlType(i); + int precision = sqlType.getPrecision(); + if (JDBCType.TIMESTAMP == sqlType.getJdbctype()) { + // TODO: update the test to use correct precision once bulkCopy is fixed + precision = 50; + } + columnMetadata.put(i + 1, + new ColumnMetadata(sqlType.getName(), sqlType.getJdbctype().getVendorTypeNumber(), precision, sqlType.getScale())); + } + + // add data + rowCount = dstTable.getTotalRows(); + data = new ArrayList<>(rowCount); + for (int i = 0; i < rowCount; i++) { + Object[] CurrentRow = new Object[totalColumn]; + for (int j = 0; j < totalColumn; j++) { + SqlType sqlType = dstTable.getSqlType(j); + if (JDBCType.BIT == sqlType.getJdbctype()) { + CurrentRow[j] = ((0 == ThreadLocalRandom.current().nextInt(2)) ? Boolean.FALSE : Boolean.TRUE); + } + else + { + CurrentRow[j] = sqlType.createdata(); + } + } + data.add(CurrentRow); + } + } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord#getColumnOrdinals() + */ + @Override + public Set getColumnOrdinals() { + return columnMetadata.keySet(); + } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord#getColumnName(int) + */ + @Override + public String getColumnName(int column) { + return columnMetadata.get(column).columnName; + } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord#getColumnType(int) + */ + @Override + public int getColumnType(int column) { + return columnMetadata.get(column).columnType; + } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord#getPrecision(int) + */ + @Override + public int getPrecision(int column) { + return columnMetadata.get(column).precision; + } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord#getScale(int) + */ + @Override + public int getScale(int column) { + return columnMetadata.get(column).scale; + } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord#isAutoIncrement(int) + */ + @Override + public boolean isAutoIncrement(int column) { + return false; + } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord#getRowData() + */ + @Override + public Object[] getRowData() throws SQLServerException { + return data.get(counter++); + } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord#next() + */ + @Override + public boolean next() throws SQLServerException { + if (counter < rowCount) + return true; + return false; + } + + /** + * reset the counter when using the interface for validating the data + */ + public void reset() { + counter = 0; + } + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyResultSetCursorTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyResultSetCursorTest.java new file mode 100644 index 000000000..774deff4a --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyResultSetCursorTest.java @@ -0,0 +1,212 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.bulkCopy; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Properties; +import java.util.TimeZone; + +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerBulkCopy; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; + +@RunWith(JUnitPlatform.class) +public class BulkCopyResultSetCursorTest extends AbstractTest { + + static BigDecimal[] expectedBigDecimals = {new BigDecimal("12345.12345"), new BigDecimal("125.123"), new BigDecimal("45.12345")}; + static String[] expectedBigDecimalStrings = {"12345.12345", "125.12300", "45.12345"}; + + static String[] expectedStrings = {"hello", "world", "!!!"}; + + static Timestamp[] expectedTimestamps = {new Timestamp(1433338533461L), new Timestamp(14917485583999L), new Timestamp(1491123533000L)}; + static String[] expectedTimestampStrings = {"2015-06-03 13:35:33.4610000", "2442-09-19 01:59:43.9990000", "2017-04-02 08:58:53.0000000"}; + + private static String srcTable = "BulkCopyResultSetCursorTest_SourceTable"; + private static String desTable = "BulkCopyResultSetCursorTest_DestinationTable"; + + /** + * Test a previous failure when using server cursor and using the same connection to create Bulk Copy and result set. + * + * @throws SQLException + */ + @Test + public void testServerCursors() throws SQLException { + serverCursorsTest(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + serverCursorsTest(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE); + serverCursorsTest(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); + serverCursorsTest(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); + } + + private void serverCursorsTest(int resultSetType, + int resultSetConcurrency) throws SQLException { + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement()) { + + dropTables(stmt); + createTables(stmt); + populateSourceTable(); + + try (ResultSet rs = conn.createStatement(resultSetType, resultSetConcurrency).executeQuery("select * from " + srcTable); + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(conn)) { + bulkCopy.setDestinationTableName(desTable); + bulkCopy.writeToServer(rs); + + verifyDestinationTableData(expectedBigDecimals.length); + } + } + } + + /** + * Test a previous failure when setting SelectMethod to cursor and using the same connection to create Bulk Copy and result set. + * + * @throws SQLException + */ + @Test + public void testSelectMethodSetToCursor() throws SQLException { + Properties info = new Properties(); + info.setProperty("SelectMethod", "cursor"); + try (Connection conn = DriverManager.getConnection(connectionString, info); + Statement stmt = conn.createStatement()) { + dropTables(stmt); + createTables(stmt); + populateSourceTable(); + + try (ResultSet rs = conn.createStatement().executeQuery("select * from " + srcTable); + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(conn)) { + bulkCopy.setDestinationTableName(desTable); + bulkCopy.writeToServer(rs); + + verifyDestinationTableData(expectedBigDecimals.length); + } + } + } + + /** + * test with multiple prepared statements and result sets + * + * @throws SQLException + */ + @Test + public void testMultiplePreparedStatementAndResultSet() throws SQLException { + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement()) { + + dropTables(stmt); + createTables(stmt); + populateSourceTable(); + + try (ResultSet rs = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE).executeQuery("select * from " + srcTable)) { + try (SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(conn)) { + bulkCopy.setDestinationTableName(desTable); + bulkCopy.writeToServer(rs); + verifyDestinationTableData(expectedBigDecimals.length); + } + + rs.beforeFirst(); + try (SQLServerBulkCopy bulkCopy1 = new SQLServerBulkCopy(conn)) { + bulkCopy1.setDestinationTableName(desTable); + bulkCopy1.writeToServer(rs); + verifyDestinationTableData(expectedBigDecimals.length * 2); + } + + rs.beforeFirst(); + try (SQLServerBulkCopy bulkCopy2 = new SQLServerBulkCopy(conn)) { + bulkCopy2.setDestinationTableName(desTable); + bulkCopy2.writeToServer(rs); + verifyDestinationTableData(expectedBigDecimals.length * 3); + } + + String sql = "insert into " + desTable + " values (?,?,?,?)"; + Calendar calGMT = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + try (SQLServerPreparedStatement pstmt1 = (SQLServerPreparedStatement) conn.prepareStatement(sql)) { + for (int i = 0; i < expectedBigDecimals.length; i++) { + pstmt1.setBigDecimal(1, expectedBigDecimals[i]); + pstmt1.setString(2, expectedStrings[i]); + pstmt1.setTimestamp(3, expectedTimestamps[i], calGMT); + pstmt1.setString(4, expectedStrings[i]); + pstmt1.execute(); + } + verifyDestinationTableData(expectedBigDecimals.length * 4); + } + try (ResultSet rs2 = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE).executeQuery("select * from " + srcTable); + SQLServerBulkCopy bulkCopy3 = new SQLServerBulkCopy(conn)) { + bulkCopy3.setDestinationTableName(desTable); + bulkCopy3.writeToServer(rs2); + verifyDestinationTableData(expectedBigDecimals.length * 5); + } + } + } + } + + private static void verifyDestinationTableData(int expectedNumberOfRows) throws SQLException { + try (Connection conn = DriverManager.getConnection(connectionString); + ResultSet rs = conn.createStatement().executeQuery("select * from " + desTable)) { + + int expectedArrayLength = expectedBigDecimals.length; + + int i = 0; + while (rs.next()) { + assertTrue(rs.getString(1).equals(expectedBigDecimalStrings[i % expectedArrayLength]), + "Expected Value:" + expectedBigDecimalStrings[i % expectedArrayLength] + ", Actual Value: " + rs.getString(1)); + assertTrue(rs.getString(2).trim().equals(expectedStrings[i % expectedArrayLength]), + "Expected Value:" + expectedStrings[i % expectedArrayLength] + ", Actual Value: " + rs.getString(2)); + assertTrue(rs.getString(3).equals(expectedTimestampStrings[i % expectedArrayLength]), + "Expected Value:" + expectedTimestampStrings[i % expectedArrayLength] + ", Actual Value: " + rs.getString(3)); + assertTrue(rs.getString(4).trim().equals(expectedStrings[i % expectedArrayLength]), + "Expected Value:" + expectedStrings[i % expectedArrayLength] + ", Actual Value: " + rs.getString(4)); + i++; + } + + assertTrue(i == expectedNumberOfRows); + } + } + + private static void populateSourceTable() throws SQLException { + String sql = "insert into " + srcTable + " values (?,?,?,?)"; + Calendar calGMT = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + + try (Connection conn = DriverManager.getConnection(connectionString); + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) conn.prepareStatement(sql)) { + + for (int i = 0; i < expectedBigDecimals.length; i++) { + pstmt.setBigDecimal(1, expectedBigDecimals[i]); + pstmt.setString(2, expectedStrings[i]); + pstmt.setTimestamp(3, expectedTimestamps[i], calGMT); + pstmt.setString(4, expectedStrings[i]); + pstmt.execute(); + } + } + } + + private static void dropTables(Statement stmt) throws SQLException { + Utils.dropTableIfExists(srcTable, stmt); + Utils.dropTableIfExists(desTable, stmt); + } + + private static void createTables(Statement stmt) throws SQLException { + String sql = "create table " + srcTable + " (c1 decimal(10,5) null, c2 nchar(50) null, c3 datetime2(7) null, c4 char(7000));"; + stmt.execute(sql); + + sql = "create table " + desTable + " (c1 decimal(10,5) null, c2 nchar(50) null, c3 datetime2(7) null, c4 char(7000));"; + stmt.execute(sql); + } +} \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyTestSetUp.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyTestSetUp.java index 30fbe46a0..f916336de 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyTestSetUp.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyTestSetUp.java @@ -7,6 +7,8 @@ */ package com.microsoft.sqlserver.jdbc.bulkCopy; +import java.sql.SQLException; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.platform.runner.JUnitPlatform; @@ -14,6 +16,7 @@ import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.DBConnection; +import com.microsoft.sqlserver.testframework.DBPreparedStatement; import com.microsoft.sqlserver.testframework.DBStatement; import com.microsoft.sqlserver.testframework.DBTable;; @@ -27,38 +30,28 @@ public class BulkCopyTestSetUp extends AbstractTest { /** * Create source table needed for testing bulk copy + * @throws SQLException */ @BeforeAll - static void setUpSourceTable() { - DBConnection con = null; - DBStatement stmt = null; - try { - con = new DBConnection(connectionString); - stmt = con.createStatement(); + static void setUpSourceTable() throws SQLException { + try (DBConnection con = new DBConnection(connectionString); + DBStatement stmt = con.createStatement(); + DBPreparedStatement pstmt = new DBPreparedStatement(con);) { sourceTable = new DBTable(true); stmt.createTable(sourceTable); - stmt.populateTable(sourceTable); - } - finally { - con.close(); + pstmt.populateTable(sourceTable); } } /** * drop source table after testing bulk copy + * @throws SQLException */ @AfterAll - static void dropSourceTable() { - DBConnection con = null; - DBStatement stmt = null; - try { - con = new DBConnection(connectionString); - stmt = con.createStatement(); + static void dropSourceTable() throws SQLException { + try (DBConnection con = new DBConnection(connectionString); + DBStatement stmt = con.createStatement()) { stmt.dropTable(sourceTable); } - finally { - con.close(); - } } - } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyTestUtil.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyTestUtil.java index c7d83f69f..0b902587e 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyTestUtil.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyTestUtil.java @@ -7,27 +7,20 @@ */ package com.microsoft.sqlserver.jdbc.bulkCopy; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import java.math.BigDecimal; import java.sql.Connection; -import java.sql.Date; -import java.sql.JDBCType; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; -import java.sql.Time; -import java.sql.Timestamp; -import java.util.Arrays; - +import com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord; import com.microsoft.sqlserver.jdbc.SQLServerBulkCopy; import com.microsoft.sqlserver.jdbc.bulkCopy.BulkCopyTestWrapper.ColumnMap; import com.microsoft.sqlserver.testframework.DBConnection; import com.microsoft.sqlserver.testframework.DBResultSet; import com.microsoft.sqlserver.testframework.DBStatement; import com.microsoft.sqlserver.testframework.DBTable; +import com.microsoft.sqlserver.testframework.util.ComparisonUtil; /** * Utility class @@ -68,57 +61,53 @@ static void performBulkCopy(BulkCopyTestWrapper wrapper, static void performBulkCopy(BulkCopyTestWrapper wrapper, DBTable sourceTable, boolean validateResult) { - DBConnection con = null; - DBStatement stmt = null; DBTable destinationTable = null; - try { - con = new DBConnection(wrapper.getConnectionString()); - stmt = con.createStatement(); + try (DBConnection con = new DBConnection(wrapper.getConnectionString()); + DBStatement stmt = con.createStatement()) { destinationTable = sourceTable.cloneSchema(); stmt.createTable(destinationTable); - DBResultSet srcResultSet = stmt.executeQuery("SELECT * FROM " + sourceTable.getEscapedTableName() + ";"); - SQLServerBulkCopy bulkCopy; - if (wrapper.isUsingConnection()) { - bulkCopy = new SQLServerBulkCopy((Connection) con.product()); - } - else { - bulkCopy = new SQLServerBulkCopy(wrapper.getConnectionString()); - } - if (wrapper.isUsingBulkCopyOptions()) { - bulkCopy.setBulkCopyOptions(wrapper.getBulkOptions()); + try (DBResultSet srcResultSet = stmt.executeQuery("SELECT * FROM " + sourceTable.getEscapedTableName() + ";"); + SQLServerBulkCopy bulkCopy = wrapper.isUsingConnection() ? + new SQLServerBulkCopy((Connection) con.product()) : + new SQLServerBulkCopy(wrapper.getConnectionString())) { + if (wrapper.isUsingBulkCopyOptions()) { + bulkCopy.setBulkCopyOptions(wrapper.getBulkOptions()); + } + bulkCopy.setDestinationTableName(destinationTable.getEscapedTableName()); + if (wrapper.isUsingColumnMapping()) { + for (int i = 0; i < wrapper.cm.size(); i++) { + ColumnMap currentMap = wrapper.cm.get(i); + if (currentMap.sourceIsInt && currentMap.destIsInt) { + bulkCopy.addColumnMapping(currentMap.srcInt, currentMap.destInt); + } + else if (currentMap.sourceIsInt && (!currentMap.destIsInt)) { + bulkCopy.addColumnMapping(currentMap.srcInt, currentMap.destString); + } + else if ((!currentMap.sourceIsInt) && currentMap.destIsInt) { + bulkCopy.addColumnMapping(currentMap.srcString, currentMap.destInt); + } + else if ((!currentMap.sourceIsInt) && (!currentMap.destIsInt)) { + bulkCopy.addColumnMapping(currentMap.srcString, currentMap.destString); + } + } + } + bulkCopy.writeToServer((ResultSet) srcResultSet.product()); + if (validateResult) { + validateValues(con, sourceTable, destinationTable); + } } - bulkCopy.setDestinationTableName(destinationTable.getEscapedTableName()); - if (wrapper.isUsingColumnMapping()) { - for (int i = 0; i < wrapper.cm.size(); i++) { - ColumnMap currentMap = wrapper.cm.get(i); - if (currentMap.sourceIsInt && currentMap.destIsInt) { - bulkCopy.addColumnMapping(currentMap.srcInt, currentMap.destInt); - } - else if (currentMap.sourceIsInt && (!currentMap.destIsInt)) { - bulkCopy.addColumnMapping(currentMap.srcInt, currentMap.destString); - } - else if ((!currentMap.sourceIsInt) && currentMap.destIsInt) { - bulkCopy.addColumnMapping(currentMap.srcString, currentMap.destInt); - } - else if ((!currentMap.sourceIsInt) && (!currentMap.destIsInt)) { - bulkCopy.addColumnMapping(currentMap.srcString, currentMap.destString); - } - } + catch (SQLException ex) { + fail(ex.getMessage()); } - bulkCopy.writeToServer((ResultSet) srcResultSet.product()); - bulkCopy.close(); - if (validateResult) { - validateValues(con, sourceTable, destinationTable); + finally { + stmt.dropTable(destinationTable); + con.close(); } } catch (SQLException ex) { fail(ex.getMessage()); } - finally { - stmt.dropTable(destinationTable); - con.close(); - } } /** @@ -133,20 +122,12 @@ static void performBulkCopy(BulkCopyTestWrapper wrapper, DBTable sourceTable, DBTable destinationTable, boolean validateResult) { - DBConnection con = null; - DBStatement stmt = null; - try { - con = new DBConnection(wrapper.getConnectionString()); - stmt = con.createStatement(); - - DBResultSet srcResultSet = stmt.executeQuery("SELECT * FROM " + sourceTable.getEscapedTableName() + ";"); - SQLServerBulkCopy bulkCopy; - if (wrapper.isUsingConnection()) { - bulkCopy = new SQLServerBulkCopy((Connection) con.product()); - } - else { - bulkCopy = new SQLServerBulkCopy(wrapper.getConnectionString()); - } + try (DBConnection con = new DBConnection(wrapper.getConnectionString()); + DBStatement stmt = con.createStatement(); + DBResultSet srcResultSet = stmt.executeQuery("SELECT * FROM " + sourceTable.getEscapedTableName() + ";"); + SQLServerBulkCopy bulkCopy = wrapper.isUsingConnection() ? + new SQLServerBulkCopy((Connection) con.product()) : + new SQLServerBulkCopy(wrapper.getConnectionString())) { if (wrapper.isUsingBulkCopyOptions()) { bulkCopy.setBulkCopyOptions(wrapper.getBulkOptions()); } @@ -169,18 +150,13 @@ else if ((!currentMap.sourceIsInt) && (!currentMap.destIsInt)) { } } bulkCopy.writeToServer((ResultSet) srcResultSet.product()); - bulkCopy.close(); if (validateResult) { validateValues(con, sourceTable, destinationTable); } - } + } catch (SQLException ex) { fail(ex.getMessage()); } - finally { - stmt.dropTable(destinationTable); - con.close(); - } } /** @@ -197,58 +173,54 @@ static void performBulkCopy(BulkCopyTestWrapper wrapper, DBTable destinationTable, boolean validateResult, boolean fail) { - DBConnection con = null; - DBStatement stmt = null; - try { - con = new DBConnection(wrapper.getConnectionString()); - stmt = con.createStatement(); - - DBResultSet srcResultSet = stmt.executeQuery("SELECT * FROM " + sourceTable.getEscapedTableName() + ";"); - SQLServerBulkCopy bulkCopy; - if (wrapper.isUsingConnection()) { - bulkCopy = new SQLServerBulkCopy((Connection) con.product()); - } - else { - bulkCopy = new SQLServerBulkCopy(wrapper.getConnectionString()); - } - if (wrapper.isUsingBulkCopyOptions()) { - bulkCopy.setBulkCopyOptions(wrapper.getBulkOptions()); - } - bulkCopy.setDestinationTableName(destinationTable.getEscapedTableName()); - if (wrapper.isUsingColumnMapping()) { - for (int i = 0; i < wrapper.cm.size(); i++) { - ColumnMap currentMap = wrapper.cm.get(i); - if (currentMap.sourceIsInt && currentMap.destIsInt) { - bulkCopy.addColumnMapping(currentMap.srcInt, currentMap.destInt); - } - else if (currentMap.sourceIsInt && (!currentMap.destIsInt)) { - bulkCopy.addColumnMapping(currentMap.srcInt, currentMap.destString); - } - else if ((!currentMap.sourceIsInt) && currentMap.destIsInt) { - bulkCopy.addColumnMapping(currentMap.srcString, currentMap.destInt); - } - else if ((!currentMap.sourceIsInt) && (!currentMap.destIsInt)) { - bulkCopy.addColumnMapping(currentMap.srcString, currentMap.destString); - } - } - } - bulkCopy.writeToServer((ResultSet) srcResultSet.product()); - if (fail) - fail("bulkCopy.writeToServer did not fail when it should have"); - bulkCopy.close(); - if (validateResult) { - validateValues(con, sourceTable, destinationTable); - } - } - catch (SQLException ex) { + try (DBConnection con = new DBConnection(wrapper.getConnectionString()); + DBStatement stmt = con.createStatement(); + DBResultSet srcResultSet = stmt.executeQuery("SELECT * FROM " + sourceTable.getEscapedTableName() + ";"); + SQLServerBulkCopy bulkCopy = wrapper.isUsingConnection() ? + new SQLServerBulkCopy((Connection) con.product()) : + new SQLServerBulkCopy(wrapper.getConnectionString())) { + try { + if (wrapper.isUsingBulkCopyOptions()) { + bulkCopy.setBulkCopyOptions(wrapper.getBulkOptions()); + } + bulkCopy.setDestinationTableName(destinationTable.getEscapedTableName()); + if (wrapper.isUsingColumnMapping()) { + for (int i = 0; i < wrapper.cm.size(); i++) { + ColumnMap currentMap = wrapper.cm.get(i); + if (currentMap.sourceIsInt && currentMap.destIsInt) { + bulkCopy.addColumnMapping(currentMap.srcInt, currentMap.destInt); + } + else if (currentMap.sourceIsInt && (!currentMap.destIsInt)) { + bulkCopy.addColumnMapping(currentMap.srcInt, currentMap.destString); + } + else if ((!currentMap.sourceIsInt) && currentMap.destIsInt) { + bulkCopy.addColumnMapping(currentMap.srcString, currentMap.destInt); + } + else if ((!currentMap.sourceIsInt) && (!currentMap.destIsInt)) { + bulkCopy.addColumnMapping(currentMap.srcString, currentMap.destString); + } + } + } + bulkCopy.writeToServer((ResultSet) srcResultSet.product()); + if (fail) + fail("bulkCopy.writeToServer did not fail when it should have"); + if (validateResult) { + validateValues(con, sourceTable, destinationTable); + } + } catch (SQLException ex) { + if (!fail) { + fail(ex.getMessage()); + } + } + finally { + stmt.dropTable(destinationTable); + con.close(); + } + } catch (SQLException e) { if (!fail) { - fail(ex.getMessage()); + fail(e.getMessage()); } - } - finally { - stmt.dropTable(destinationTable); - con.close(); - } + } } /** @@ -267,60 +239,59 @@ static void performBulkCopy(BulkCopyTestWrapper wrapper, boolean validateResult, boolean fail, boolean dropDest) { - DBConnection con = null; - DBStatement stmt = null; - try { - con = new DBConnection(wrapper.getConnectionString()); - stmt = con.createStatement(); - - DBResultSet srcResultSet = stmt.executeQuery("SELECT * FROM " + sourceTable.getEscapedTableName() + ";"); - SQLServerBulkCopy bulkCopy; - if (wrapper.isUsingConnection()) { - bulkCopy = new SQLServerBulkCopy((Connection) con.product()); - } - else { - bulkCopy = new SQLServerBulkCopy(wrapper.getConnectionString()); - } - if (wrapper.isUsingBulkCopyOptions()) { - bulkCopy.setBulkCopyOptions(wrapper.getBulkOptions()); - } - bulkCopy.setDestinationTableName(destinationTable.getEscapedTableName()); - if (wrapper.isUsingColumnMapping()) { - for (int i = 0; i < wrapper.cm.size(); i++) { - ColumnMap currentMap = wrapper.cm.get(i); - if (currentMap.sourceIsInt && currentMap.destIsInt) { - bulkCopy.addColumnMapping(currentMap.srcInt, currentMap.destInt); - } - else if (currentMap.sourceIsInt && (!currentMap.destIsInt)) { - bulkCopy.addColumnMapping(currentMap.srcInt, currentMap.destString); - } - else if ((!currentMap.sourceIsInt) && currentMap.destIsInt) { - bulkCopy.addColumnMapping(currentMap.srcString, currentMap.destInt); - } - else if ((!currentMap.sourceIsInt) && (!currentMap.destIsInt)) { - bulkCopy.addColumnMapping(currentMap.srcString, currentMap.destString); - } - } - } - bulkCopy.writeToServer((ResultSet) srcResultSet.product()); - if (fail) - fail("bulkCopy.writeToServer did not fail when it should have"); - bulkCopy.close(); - if (validateResult) { - validateValues(con, sourceTable, destinationTable); - } + try (DBConnection con = new DBConnection(wrapper.getConnectionString()); + DBStatement stmt = con.createStatement(); + DBResultSet srcResultSet = stmt.executeQuery("SELECT * FROM " + sourceTable.getEscapedTableName() + ";"); + SQLServerBulkCopy bulkCopy = wrapper.isUsingConnection() ? + new SQLServerBulkCopy((Connection) con.product()) : + new SQLServerBulkCopy(wrapper.getConnectionString())) { + try { + if (wrapper.isUsingBulkCopyOptions()) { + bulkCopy.setBulkCopyOptions(wrapper.getBulkOptions()); + } + bulkCopy.setDestinationTableName(destinationTable.getEscapedTableName()); + if (wrapper.isUsingColumnMapping()) { + for (int i = 0; i < wrapper.cm.size(); i++) { + ColumnMap currentMap = wrapper.cm.get(i); + if (currentMap.sourceIsInt && currentMap.destIsInt) { + bulkCopy.addColumnMapping(currentMap.srcInt, currentMap.destInt); + } + else if (currentMap.sourceIsInt && (!currentMap.destIsInt)) { + bulkCopy.addColumnMapping(currentMap.srcInt, currentMap.destString); + } + else if ((!currentMap.sourceIsInt) && currentMap.destIsInt) { + bulkCopy.addColumnMapping(currentMap.srcString, currentMap.destInt); + } + else if ((!currentMap.sourceIsInt) && (!currentMap.destIsInt)) { + bulkCopy.addColumnMapping(currentMap.srcString, currentMap.destString); + } + } + } + bulkCopy.writeToServer((ResultSet) srcResultSet.product()); + if (fail) + fail("bulkCopy.writeToServer did not fail when it should have"); + bulkCopy.close(); + if (validateResult) { + validateValues(con, sourceTable, destinationTable); + } + } + catch (SQLException ex) { + if (!fail) { + fail(ex.getMessage()); + } + } + finally { + if (dropDest) { + stmt.dropTable(destinationTable); + } + con.close(); + } } catch (SQLException ex) { if (!fail) { fail(ex.getMessage()); } } - finally { - if (dropDest) { - stmt.dropTable(destinationTable); - } - con.close(); - } } /** @@ -334,112 +305,93 @@ else if ((!currentMap.sourceIsInt) && (!currentMap.destIsInt)) { static void validateValues(DBConnection con, DBTable sourceTable, DBTable destinationTable) throws SQLException { - DBStatement srcStmt = con.createStatement(); - DBStatement dstStmt = con.createStatement(); - DBResultSet srcResultSet = srcStmt.executeQuery("SELECT * FROM " + sourceTable.getEscapedTableName() + ";"); - DBResultSet dstResultSet = dstStmt.executeQuery("SELECT * FROM " + destinationTable.getEscapedTableName() + ";"); - ResultSetMetaData destMeta = ((ResultSet) dstResultSet.product()).getMetaData(); - int totalColumns = destMeta.getColumnCount(); - - // verify data from sourceType and resultSet - while (srcResultSet.next() && dstResultSet.next()) - for (int i = 1; i <= totalColumns; i++) { - // TODO: check row and column count in both the tables - - Object srcValue, dstValue; - srcValue = srcResultSet.getObject(i); - dstValue = dstResultSet.getObject(i); - - comapreSourceDest(destMeta.getColumnType(i), srcValue, dstValue); - } + try (DBStatement srcStmt = con.createStatement(); + DBStatement dstStmt = con.createStatement(); + DBResultSet srcResultSet = srcStmt.executeQuery("SELECT * FROM " + sourceTable.getEscapedTableName() + ";"); + DBResultSet dstResultSet = dstStmt.executeQuery("SELECT * FROM " + destinationTable.getEscapedTableName() + ";")) { + ResultSetMetaData destMeta = ((ResultSet) dstResultSet.product()).getMetaData(); + int totalColumns = destMeta.getColumnCount(); + + // verify data from sourceType and resultSet + while (srcResultSet.next() && dstResultSet.next()) { + for (int i = 1; i <= totalColumns; i++) { + // TODO: check row and column count in both the tables + + Object srcValue, dstValue; + srcValue = srcResultSet.getObject(i); + dstValue = dstResultSet.getObject(i); + + ComparisonUtil.compareExpectedAndActual(destMeta.getColumnType(i), srcValue, dstValue); + } + } + } } /** - * validate if both expected and actual value are same * - * @param dataType - * @param expectedValue - * @param actualValue + * @param bulkWrapper + * @param srcData + * @param dstTable */ - static void comapreSourceDest(int dataType, - Object expectedValue, - Object actualValue) { - // Bulkcopy doesn't guarantee order of insertion - if we need to test several rows either use primary key or - // validate result based on sql JOIN - - if ((null == expectedValue) || (null == actualValue)) { - // if one value is null other should be null too - assertEquals(expectedValue, actualValue, "Expected null in source and destination"); + static void performBulkCopy(BulkCopyTestWrapper bulkWrapper, + ISQLServerBulkRecord srcData, + DBTable dstTable) { + try (DBConnection con = new DBConnection(bulkWrapper.getConnectionString()); + DBStatement stmt = con.createStatement(); + SQLServerBulkCopy bc = new SQLServerBulkCopy(bulkWrapper.getConnectionString());) { + bc.setDestinationTableName(dstTable.getEscapedTableName()); + bc.writeToServer(srcData); + validateValues(con, srcData, dstTable); + } + catch (Exception e) { + fail(e.getMessage()); + } + } + + /** + * + * @param con + * @param srcData + * @param destinationTable + * @throws Exception + */ + static void validateValues( + DBConnection con, + ISQLServerBulkRecord srcData, + DBTable destinationTable) throws Exception { + + try (DBStatement dstStmt = con.createStatement(); + DBResultSet dstResultSet = dstStmt.executeQuery("SELECT * FROM " + destinationTable.getEscapedTableName() + ";")) { + ResultSetMetaData destMeta = ((ResultSet) dstResultSet.product()).getMetaData(); + int totalColumns = destMeta.getColumnCount(); + + // reset the counter in ISQLServerBulkRecord, which was incremented during read by BulkCopy + java.lang.reflect.Method method = srcData.getClass().getMethod("reset"); + method.invoke(srcData); + + // verify data from sourceType and resultSet + while (srcData.next() && dstResultSet.next()) + { + Object[] srcValues = srcData.getRowData(); + for (int i = 1; i <= totalColumns; i++) { + + Object srcValue, dstValue; + srcValue = srcValues[i-1]; + if(srcValue.getClass().getName().equalsIgnoreCase("java.lang.Double")){ + // in case of SQL Server type Float (ie java type double), in float(n) if n is <=24 ie precsion is <=7 SQL Server type Real is returned(ie java type float) + if(destMeta.getPrecision(i) <8) + srcValue = new Float(((Double)srcValue)); + } + dstValue = dstResultSet.getObject(i); + int dstType = destMeta.getColumnType(i); + if(java.sql.Types.TIMESTAMP != dstType + && java.sql.Types.TIME != dstType + && microsoft.sql.Types.DATETIMEOFFSET != dstType){ + // skip validation for temporal types due to rounding eg 7986-10-21 09:51:15.114 is rounded as 7986-10-21 09:51:15.113 in server + ComparisonUtil.compareExpectedAndActual(dstType, srcValue, dstValue); + } + } + } } - else - switch (dataType) { - case java.sql.Types.BIGINT: - assertTrue((((Long) expectedValue).longValue() == ((Long) actualValue).longValue()), "Unexpected bigint value"); - break; - - case java.sql.Types.INTEGER: - assertTrue((((Integer) expectedValue).intValue() == ((Integer) actualValue).intValue()), "Unexpected int value"); - break; - - case java.sql.Types.SMALLINT: - case java.sql.Types.TINYINT: - assertTrue((((Short) expectedValue).shortValue() == ((Short) actualValue).shortValue()), "Unexpected smallint/tinyint value"); - break; - - case java.sql.Types.BIT: - assertTrue((((Boolean) expectedValue).booleanValue() == ((Boolean) actualValue).booleanValue()), "Unexpected bit value"); - break; - - case java.sql.Types.DECIMAL: - case java.sql.Types.NUMERIC: - assertTrue(0 == (((BigDecimal) expectedValue).compareTo((BigDecimal) actualValue)), - "Unexpected decimal/numeric/money/smallmoney value"); - break; - - case java.sql.Types.DOUBLE: - assertTrue((((Double) expectedValue).doubleValue() == ((Double) actualValue).doubleValue()), "Unexpected float value"); - break; - - case java.sql.Types.REAL: - assertTrue((((Float) expectedValue).floatValue() == ((Float) actualValue).floatValue()), "Unexpected real value"); - break; - - case java.sql.Types.VARCHAR: - case java.sql.Types.NVARCHAR: - assertTrue((((String) expectedValue).equals((String) actualValue)), "Unexpected varchar/nvarchar value "); - break; - - case java.sql.Types.CHAR: - case java.sql.Types.NCHAR: - assertTrue((((String) expectedValue).equals((String) actualValue)), "Unexpected char/nchar value "); - break; - - case java.sql.Types.BINARY: - case java.sql.Types.VARBINARY: - assertTrue(Arrays.equals(((byte[]) expectedValue), ((byte[]) actualValue)), "Unexpected bianry/varbinary value "); - break; - - case java.sql.Types.TIMESTAMP: - assertTrue((((Timestamp) expectedValue).getTime() == (((Timestamp) actualValue).getTime())), - "Unexpected datetime/smalldatetime/datetime2 value"); - break; - - case java.sql.Types.DATE: - assertTrue((((Date) expectedValue).getTime() == (((Date) actualValue).getTime())), "Unexpected datetime value"); - break; - - case java.sql.Types.TIME: - assertTrue(((Time) expectedValue).getTime() == ((Time) actualValue).getTime(), "Unexpected time value "); - break; - - case microsoft.sql.Types.DATETIMEOFFSET: - assertTrue(0 == ((microsoft.sql.DateTimeOffset) expectedValue).compareTo((microsoft.sql.DateTimeOffset) actualValue), - "Unexpected time value "); - break; - - default: - fail("Unhandled JDBCType " + JDBCType.valueOf(dataType)); - break; - } } } \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyTestWrapper.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyTestWrapper.java index 2eaa7f6ab..c6be7261c 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyTestWrapper.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyTestWrapper.java @@ -35,7 +35,7 @@ class BulkCopyTestWrapper { */ private boolean isUsingColumnMapping = false; - public LinkedList cm = new LinkedList(); + public LinkedList cm = new LinkedList<>(); private SQLServerBulkCopyOptions bulkOptions; diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyTimeoutTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyTimeoutTest.java index a2f5bdaac..3773e75fa 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyTimeoutTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyTimeoutTest.java @@ -38,13 +38,7 @@ public class BulkCopyTimeoutTest extends BulkCopyTestSetUp { @Test @DisplayName("BulkCopy:test zero timeout") void testZeroTimeOut() throws SQLServerException { - BulkCopyTestWrapper bulkWrapper = new BulkCopyTestWrapper(connectionString); - bulkWrapper.setUsingConnection((0 == ThreadLocalRandom.current().nextInt(2)) ? true : false); - SQLServerBulkCopyOptions option = new SQLServerBulkCopyOptions(); - option.setBulkCopyTimeout(0); - bulkWrapper.useBulkCopyOptions(true); - bulkWrapper.setBulkOptions(option); - BulkCopyTestUtil.performBulkCopy(bulkWrapper, sourceTable, false); + testBulkCopyWithTimeout(0); } /** @@ -58,14 +52,18 @@ void testNegativeTimeOut() throws SQLServerException { assertThrows(SQLServerException.class, new org.junit.jupiter.api.function.Executable() { @Override public void execute() throws SQLServerException { - BulkCopyTestWrapper bulkWrapper = new BulkCopyTestWrapper(connectionString); - bulkWrapper.setUsingConnection((0 == ThreadLocalRandom.current().nextInt(2)) ? true : false); - SQLServerBulkCopyOptions option = new SQLServerBulkCopyOptions(); - option.setBulkCopyTimeout(-1); - bulkWrapper.useBulkCopyOptions(true); - bulkWrapper.setBulkOptions(option); - BulkCopyTestUtil.performBulkCopy(bulkWrapper, sourceTable, false); + testBulkCopyWithTimeout(-1); } }); } + + private void testBulkCopyWithTimeout(int timeout) throws SQLServerException { + BulkCopyTestWrapper bulkWrapper = new BulkCopyTestWrapper(connectionString); + bulkWrapper.setUsingConnection((0 == ThreadLocalRandom.current().nextInt(2)) ? true : false); + SQLServerBulkCopyOptions option = new SQLServerBulkCopyOptions(); + option.setBulkCopyTimeout(timeout); + bulkWrapper.useBulkCopyOptions(true); + bulkWrapper.setBulkOptions(option); + BulkCopyTestUtil.performBulkCopy(bulkWrapper, sourceTable, false); + } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/ISQLServerBulkRecordIssuesTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/ISQLServerBulkRecordIssuesTest.java new file mode 100644 index 000000000..bbb83e148 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/ISQLServerBulkRecordIssuesTest.java @@ -0,0 +1,443 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.bulkCopy; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord; +import com.microsoft.sqlserver.jdbc.SQLServerBulkCopy; +import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; + +@RunWith(JUnitPlatform.class) +public class ISQLServerBulkRecordIssuesTest extends AbstractTest { + + static Statement stmt = null; + static PreparedStatement pStmt = null; + static String query; + static SQLServerConnection con = null; + static String srcTable = "sourceTable"; + static String destTable = "destTable"; + String variation; + + /** + * Testing that sending a bigger varchar(3) to varchar(2) is thowing the proper error message. + * + * @throws Exception + */ + @Test + public void testVarchar() throws Exception { + variation = "testVarchar"; + BulkData bData = new BulkData(variation); + query = "CREATE TABLE " + destTable + " (smallDATA varchar(2))"; + stmt.executeUpdate(query); + + try (SQLServerBulkCopy bcOperation = new SQLServerBulkCopy(connectionString)) { + bcOperation.setDestinationTableName(destTable); + bcOperation.writeToServer(bData); + bcOperation.close(); + fail("BulkCopy executed for testVarchar when it it was expected to fail"); + } + catch (Exception e) { + if (e instanceof SQLServerException) { + assertTrue(e.getMessage().contains("The given value of type"), "Invalid Error message: " + e.toString()); + } + else { + fail(e.getMessage()); + } + } + } + + /** + * Testing that setting scale and precision 0 in column meta data for smalldatetime should work + * + * @throws Exception + */ + @Test + public void testSmalldatetime() throws Exception { + variation = "testSmalldatetime"; + BulkData bData = new BulkData(variation); + String value = ("1954-05-22 02:44:00.0").toString(); + query = "CREATE TABLE " + destTable + " (smallDATA smalldatetime)"; + stmt.executeUpdate(query); + + try (SQLServerBulkCopy bcOperation = new SQLServerBulkCopy(connectionString)) { + bcOperation.setDestinationTableName(destTable); + bcOperation.writeToServer(bData); + + try (ResultSet rs = stmt.executeQuery("select * from " + destTable)) { + while (rs.next()) { + assertEquals(rs.getString(1), value); + } + } + } + } + + /** + * Testing that setting out of range value for small datetime is throwing the proper message + * + * @throws Exception + */ + @Test + public void testSmalldatetimeOutofRange() throws Exception { + variation = "testSmalldatetimeOutofRange"; + BulkData bData = new BulkData(variation); + + query = "CREATE TABLE " + destTable + " (smallDATA smalldatetime)"; + stmt.executeUpdate(query); + + try (SQLServerBulkCopy bcOperation = new SQLServerBulkCopy(connectionString)) { + bcOperation.setDestinationTableName(destTable); + bcOperation.writeToServer(bData); + fail("BulkCopy executed for testSmalldatetimeOutofRange when it it was expected to fail"); + } + catch (Exception e) { + if (e instanceof SQLServerException) { + assertTrue(e.getMessage().contains("Conversion failed when converting character string to smalldatetime data type"), + "Invalid Error message: " + e.toString()); + } + else { + fail(e.getMessage()); + } + } + } + + /** + * Test binary out of length (sending length of 6 to binary (5)) + * + * @throws Exception + */ + @Test + public void testBinaryColumnAsByte() throws Exception { + variation = "testBinaryColumnAsByte"; + BulkData bData = new BulkData(variation); + query = "CREATE TABLE " + destTable + " (col1 binary(5))"; + stmt.executeUpdate(query); + + try (SQLServerBulkCopy bcOperation = new SQLServerBulkCopy(connectionString)) { + bcOperation.setDestinationTableName(destTable); + bcOperation.writeToServer(bData); + fail("BulkCopy executed for testBinaryColumnAsByte when it it was expected to fail"); + } + catch (Exception e) { + if (e instanceof SQLServerException) { + assertTrue(e.getMessage().contains("The given value of type"), "Invalid Error message: " + e.toString()); + } + else { + fail(e.getMessage()); + } + } + } + + /** + * Test sending longer value for binary column while data is sent as string format + * + * @throws Exception + */ + @Test + public void testBinaryColumnAsString() throws Exception { + variation = "testBinaryColumnAsString"; + BulkData bData = new BulkData(variation); + query = "CREATE TABLE " + destTable + " (col1 binary(5))"; + stmt.executeUpdate(query); + + try (SQLServerBulkCopy bcOperation = new SQLServerBulkCopy(connectionString)) { + bcOperation.setDestinationTableName(destTable); + bcOperation.writeToServer(bData); + fail("BulkCopy executed for testBinaryColumnAsString when it it was expected to fail"); + } + catch (Exception e) { + if (e instanceof SQLServerException) { + assertTrue(e.getMessage().contains("The given value of type"), "Invalid Error message: " + e.toString()); + } + else { + fail(e.getMessage()); + } + } + } + + /** + * Verify that sending valid value in string format for binary column is successful + * + * @throws Exception + */ + @Test + public void testSendValidValueforBinaryColumnAsString() throws Exception { + variation = "testSendValidValueforBinaryColumnAsString"; + BulkData bData = new BulkData(variation); + query = "CREATE TABLE " + destTable + " (col1 binary(5))"; + stmt.executeUpdate(query); + + try (SQLServerBulkCopy bcOperation = new SQLServerBulkCopy(connectionString)) { + bcOperation.setDestinationTableName(destTable); + bcOperation.writeToServer(bData); + + try (ResultSet rs = stmt.executeQuery("select * from " + destTable)) { + while (rs.next()) { + assertEquals(rs.getString(1), "0101010000"); + } + } + } + catch (Exception e) { + fail(e.getMessage()); + } + } + + /** + * Prepare test + * + * @throws SQLException + * @throws SecurityException + * @throws IOException + */ + @BeforeAll + public static void setupHere() throws SQLException, SecurityException, IOException { + con = (SQLServerConnection) DriverManager.getConnection(connectionString); + stmt = con.createStatement(); + Utils.dropTableIfExists(destTable, stmt); + Utils.dropTableIfExists(srcTable, stmt); + } + + /** + * Clean up + * + * @throws SQLException + */ + @AfterEach + public void afterEachTests() throws SQLException { + Utils.dropTableIfExists(destTable, stmt); + Utils.dropTableIfExists(srcTable, stmt); + } + + @AfterAll + public static void afterAllTests() throws SQLException { + if (null != stmt) { + stmt.close(); + } + if (null != con) { + con.close(); + } + } + +} + +class BulkData implements ISQLServerBulkRecord { + boolean isStringData = false; + + private class ColumnMetadata { + String columnName; + int columnType; + int precision; + int scale; + + ColumnMetadata(String name, + int type, + int precision, + int scale) { + columnName = name; + columnType = type; + this.precision = precision; + this.scale = scale; + } + } + + Map columnMetadata; + ArrayList dateData; + ArrayList stringData; + ArrayList byteData; + + int counter = 0; + int rowCount = 1; + + BulkData(String variation) { + if (variation.equalsIgnoreCase("testVarchar")) { + isStringData = true; + columnMetadata = new HashMap<>(); + + columnMetadata.put(1, new ColumnMetadata("varchar(2)", java.sql.Types.VARCHAR, 0, 0)); + + stringData = new ArrayList<>(); + stringData.add(new String("aaa")); + rowCount = stringData.size(); + } + else if (variation.equalsIgnoreCase("testSmalldatetime")) { + isStringData = false; + columnMetadata = new HashMap<>(); + + columnMetadata.put(1, new ColumnMetadata("smallDatetime", java.sql.Types.TIMESTAMP, 0, 0)); + + dateData = new ArrayList<>(); + dateData.add(Timestamp.valueOf("1954-05-22 02:43:37.123")); + rowCount = dateData.size(); + } + else if (variation.equalsIgnoreCase("testSmalldatetimeOutofRange")) { + isStringData = false; + columnMetadata = new HashMap<>(); + + columnMetadata.put(1, new ColumnMetadata("smallDatetime", java.sql.Types.TIMESTAMP, 0, 0)); + + dateData = new ArrayList<>(); + dateData.add(Timestamp.valueOf("1954-05-22 02:43:37.1234")); + rowCount = dateData.size(); + + } + else if (variation.equalsIgnoreCase("testBinaryColumnAsByte")) { + isStringData = false; + columnMetadata = new HashMap<>(); + + columnMetadata.put(1, new ColumnMetadata("binary(5)", java.sql.Types.BINARY, 5, 0)); + + byteData = new ArrayList<>(); + byteData.add("helloo".getBytes()); + rowCount = byteData.size(); + + } + else if (variation.equalsIgnoreCase("testBinaryColumnAsString")) { + isStringData = true; + columnMetadata = new HashMap<>(); + + columnMetadata.put(1, new ColumnMetadata("binary(5)", java.sql.Types.BINARY, 5, 0)); + + stringData = new ArrayList<>(); + stringData.add("616368697412"); + rowCount = stringData.size(); + + } + + else if (variation.equalsIgnoreCase("testSendValidValueforBinaryColumnAsString")) { + isStringData = true; + columnMetadata = new HashMap<>(); + + columnMetadata.put(1, new ColumnMetadata("binary(5)", java.sql.Types.BINARY, 5, 0)); + + stringData = new ArrayList<>(); + stringData.add("010101"); + rowCount = stringData.size(); + + } + counter = 0; + + } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord#getColumnOrdinals() + */ + @Override + public Set getColumnOrdinals() { + return columnMetadata.keySet(); + } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord#getColumnName(int) + */ + @Override + public String getColumnName(int column) { + return columnMetadata.get(column).columnName; + } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord#getColumnType(int) + */ + @Override + public int getColumnType(int column) { + return columnMetadata.get(column).columnType; + } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord#getPrecision(int) + */ + @Override + public int getPrecision(int column) { + return columnMetadata.get(column).precision; + } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord#getScale(int) + */ + @Override + public int getScale(int column) { + return columnMetadata.get(column).scale; + } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord#isAutoIncrement(int) + */ + @Override + public boolean isAutoIncrement(int column) { + return false; + } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord#getRowData() + */ + @Override + public Object[] getRowData() throws SQLServerException { + Object[] dataRow = new Object[columnMetadata.size()]; + if (isStringData) + dataRow[0] = stringData.get(counter); + else { + if (null != dateData) + dataRow[0] = dateData.get(counter); + else if (null != byteData) + dataRow[0] = byteData.get(counter); + } + counter++; + return dataRow; + } + + /* + * (non-Javadoc) + * + * @see com.microsoft.sqlserver.jdbc.ISQLServerBulkRecord#next() + */ + @Override + public boolean next() throws SQLServerException { + if (counter < rowCount) { + return true; + } + return false; + } + +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bvt/bvtTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/bvt/bvtTest.java index 42f99478e..eaad85365 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/bvt/bvtTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bvt/bvtTest.java @@ -33,10 +33,6 @@ @DisplayName("BVT Test") public class bvtTest extends bvtTestSetup { private static String driverNamePattern = "Microsoft JDBC Driver \\d.\\d for SQL Server"; - private static DBResultSet rs = null; - private static DBPreparedStatement pstmt = null; - private static DBConnection conn = null; - private static DBStatement stmt = null; /** * Connect to specified server and close the connection @@ -46,13 +42,7 @@ public class bvtTest extends bvtTestSetup { @Test @DisplayName("test connection") public void testConnection() throws SQLException { - try { - conn = new DBConnection(connectionString); - conn.close(); - } - finally { - terminateVariation(); - } + try (DBConnection conn = new DBConnection(connectionString)) {} } /** @@ -62,15 +52,11 @@ public void testConnection() throws SQLException { */ @Test public void testConnectionIsClosed() throws SQLException { - try { - conn = new DBConnection(connectionString); + try (DBConnection conn = new DBConnection(connectionString)) { assertTrue(!conn.isClosed(), "BVT connection should not be closed"); conn.close(); assertTrue(conn.isClosed(), "BVT connection should not be open"); } - finally { - terminateVariation(); - } } /** @@ -80,8 +66,7 @@ public void testConnectionIsClosed() throws SQLException { */ @Test public void testDriverNameAndDriverVersion() throws SQLException { - try { - conn = new DBConnection(connectionString); + try (DBConnection conn = new DBConnection(connectionString)) { DatabaseMetaData metaData = conn.getMetaData(); Pattern p = Pattern.compile(driverNamePattern); Matcher m = p.matcher(metaData.getDriverName()); @@ -90,9 +75,6 @@ public void testDriverNameAndDriverVersion() throws SQLException { if (parts.length != 4) assertTrue(true, "Driver version number should be four parts! "); } - finally { - terminateVariation(); - } } /** @@ -103,16 +85,12 @@ public void testDriverNameAndDriverVersion() throws SQLException { @Test public void testCreateStatement() throws SQLException { - try { - conn = new DBConnection(connectionString); - stmt = conn.createStatement(); - String query = "SELECT * FROM " + table1.getEscapedTableName() + ";"; - rs = stmt.executeQuery(query); + String query = "SELECT * FROM " + table1.getEscapedTableName() + ";"; + + try (DBConnection conn = new DBConnection(connectionString); + DBStatement stmt = conn.createStatement(); + DBResultSet rs = stmt.executeQuery(query)) { rs.verify(table1); - rs.close(); - } - finally { - terminateVariation(); } } @@ -124,14 +102,10 @@ public void testCreateStatement() throws SQLException { @Test public void testCreateStatementWithQueryTimeout() throws SQLException { - try { - conn = new DBConnection(connectionString + ";querytimeout=10"); - stmt = conn.createStatement(); + try (DBConnection conn = new DBConnection(connectionString + ";querytimeout=10"); + DBStatement stmt = conn.createStatement()) { assertEquals(10, stmt.getQueryTimeout()); } - finally { - terminateVariation(); - } } /** @@ -144,11 +118,11 @@ public void testCreateStatementWithQueryTimeout() throws SQLException { @Test public void testStmtForwardOnlyReadOnly() throws SQLException, ClassNotFoundException { - try { - conn = new DBConnection(connectionString); - stmt = conn.createStatement(DBResultSetTypes.TYPE_FORWARD_ONLY_CONCUR_READ_ONLY); - String query = "SELECT * FROM " + table1.getEscapedTableName(); - rs = stmt.executeQuery(query); + String query = "SELECT * FROM " + table1.getEscapedTableName(); + + try (DBConnection conn = new DBConnection(connectionString); + DBStatement stmt = conn.createStatement(DBResultSetTypes.TYPE_FORWARD_ONLY_CONCUR_READ_ONLY); + DBResultSet rs = stmt.executeQuery(query)) { rs.next(); rs.verifyCurrentRow(table1); @@ -164,9 +138,6 @@ public void testStmtForwardOnlyReadOnly() throws SQLException, ClassNotFoundExce } rs.verify(table1); } - finally { - terminateVariation(); - } } /** @@ -178,12 +149,9 @@ public void testStmtForwardOnlyReadOnly() throws SQLException, ClassNotFoundExce */ @Test public void testStmtScrollInsensitiveReadOnly() throws SQLException, ClassNotFoundException { - try { - conn = new DBConnection(connectionString); - stmt = conn.createStatement(DBResultSetTypes.TYPE_SCROLL_INSENSITIVE_CONCUR_READ_ONLY); - - String query = "SELECT * FROM" + table1.getEscapedTableName(); - rs = stmt.executeQuery(query); + try (DBConnection conn = new DBConnection(connectionString); + DBStatement stmt = conn.createStatement(DBResultSetTypes.TYPE_SCROLL_INSENSITIVE_CONCUR_READ_ONLY); + DBResultSet rs = stmt.selectAll(table1)) { rs.next(); rs.verifyCurrentRow(table1); rs.afterLast(); @@ -191,9 +159,6 @@ public void testStmtScrollInsensitiveReadOnly() throws SQLException, ClassNotFou rs.verifyCurrentRow(table1); rs.verify(table1); } - finally { - terminateVariation(); - } } /** @@ -205,12 +170,11 @@ public void testStmtScrollInsensitiveReadOnly() throws SQLException, ClassNotFou @Test public void testStmtScrollSensitiveReadOnly() throws SQLException { - try { - conn = new DBConnection(connectionString); - stmt = conn.createStatement(DBResultSetTypes.TYPE_SCROLL_SENSITIVE_CONCUR_READ_ONLY); - - String query = "SELECT * FROM " + table1.getEscapedTableName(); - rs = stmt.executeQuery(query); + String query = "SELECT * FROM " + table1.getEscapedTableName(); + + try (DBConnection conn = new DBConnection(connectionString); + DBStatement stmt = conn.createStatement(DBResultSetTypes.TYPE_SCROLL_SENSITIVE_CONCUR_READ_ONLY); + DBResultSet rs = stmt.executeQuery(query)) { rs.next(); rs.next(); rs.verifyCurrentRow(table1); @@ -220,9 +184,6 @@ public void testStmtScrollSensitiveReadOnly() throws SQLException { rs.verify(table1); } - finally { - terminateVariation(); - } } /** @@ -233,13 +194,12 @@ public void testStmtScrollSensitiveReadOnly() throws SQLException { */ @Test public void testStmtForwardOnlyUpdateable() throws SQLException { + + String query = "SELECT * FROM " + table1.getEscapedTableName(); - try { - conn = new DBConnection(connectionString); - stmt = conn.createStatement(DBResultSetTypes.TYPE_FORWARD_ONLY_CONCUR_UPDATABLE); - - String query = "SELECT * FROM " + table1.getEscapedTableName(); - rs = stmt.executeQuery(query); + try (DBConnection conn = new DBConnection(connectionString); + DBStatement stmt = conn.createStatement(DBResultSetTypes.TYPE_FORWARD_ONLY_CONCUR_UPDATABLE); + DBResultSet rs = stmt.executeQuery(query)) { rs.next(); // Verify resultset behavior @@ -256,9 +216,6 @@ public void testStmtForwardOnlyUpdateable() throws SQLException { } rs.verify(table1); } - finally { - terminateVariation(); - } } /** @@ -270,12 +227,11 @@ public void testStmtForwardOnlyUpdateable() throws SQLException { @Test public void testStmtScrollSensitiveUpdatable() throws SQLException { - try { - conn = new DBConnection(connectionString); - stmt = conn.createStatement(DBResultSetTypes.TYPE_SCROLL_SENSITIVE_CONCUR_UPDATABLE); - - String query = "SELECT * FROM " + table1.getEscapedTableName(); - rs = stmt.executeQuery(query); + String query = "SELECT * FROM " + table1.getEscapedTableName(); + + try (DBConnection conn = new DBConnection(connectionString); + DBStatement stmt = conn.createStatement(DBResultSetTypes.TYPE_SCROLL_SENSITIVE_CONCUR_UPDATABLE); + DBResultSet rs = stmt.executeQuery(query)) { // Verify resultset behavior rs.next(); @@ -286,9 +242,6 @@ public void testStmtScrollSensitiveUpdatable() throws SQLException { rs.absolute(1); rs.verify(table1); } - finally { - terminateVariation(); - } } /** @@ -297,14 +250,11 @@ public void testStmtScrollSensitiveUpdatable() throws SQLException { * @throws SQLException */ @Test - public void testStmtSS_ScrollDynamicOptimistic_CC() throws SQLException { + public void testStmtSSScrollDynamicOptimisticCC() throws SQLException { - try { - conn = new DBConnection(connectionString); - stmt = conn.createStatement(DBResultSetTypes.TYPE_DYNAMIC_CONCUR_OPTIMISTIC); - - String query = "SELECT * FROM " + table1.getEscapedTableName(); - rs = stmt.executeQuery(query); + try (DBConnection conn = new DBConnection(connectionString); + DBStatement stmt = conn.createStatement(DBResultSetTypes.TYPE_DYNAMIC_CONCUR_OPTIMISTIC); + DBResultSet rs = stmt.selectAll(table1)) { // Verify resultset behavior rs.next(); @@ -312,9 +262,6 @@ public void testStmtSS_ScrollDynamicOptimistic_CC() throws SQLException { rs.previous(); rs.verify(table1); } - finally { - terminateVariation(); - } } /** @@ -323,25 +270,18 @@ public void testStmtSS_ScrollDynamicOptimistic_CC() throws SQLException { * @throws SQLException */ @Test - public void testStmtSS_SEVER_CURSOR_FORWARD_ONLY() throws SQLException { - - try { - conn = new DBConnection(connectionString); - DBResultSetTypes rsType = DBResultSetTypes.TYPE_FORWARD_ONLY_CONCUR_READ_ONLY; - stmt = conn.createStatement(rsType.resultsetCursor, rsType.resultSetConcurrency); - - String query = "SELECT * FROM " + table1.getEscapedTableName(); - - rs = stmt.executeQuery(query); - + public void testStmtSserverCursorForwardOnly() throws SQLException { + + DBResultSetTypes rsType = DBResultSetTypes.TYPE_FORWARD_ONLY_CONCUR_READ_ONLY; + String query = "SELECT * FROM " + table1.getEscapedTableName(); + + try (DBConnection conn = new DBConnection(connectionString); + DBStatement stmt = conn.createStatement(rsType.resultsetCursor, rsType.resultSetConcurrency); + DBResultSet rs = stmt.executeQuery(query)) { // Verify resultset behavior rs.next(); rs.verify(table1); } - finally { - terminateVariation(); - } - } /** @@ -352,21 +292,17 @@ public void testStmtSS_SEVER_CURSOR_FORWARD_ONLY() throws SQLException { @Test public void testCreatepreparedStatement() throws SQLException { - try { - conn = new DBConnection(connectionString); - String colName = table1.getColumnName(7); - String value = table1.getRowData(7, 0).toString(); + String colName = table1.getColumnName(7); + String value = table1.getRowData(7, 0).toString(); + String query = "SELECT * from " + table1.getEscapedTableName() + " where [" + colName + "] = ? "; + + try (DBConnection conn = new DBConnection(connectionString); + DBPreparedStatement pstmt = conn.prepareStatement(query)) { - String query = "SELECT * from " + table1.getEscapedTableName() + " where [" + colName + "] = ? "; - - pstmt = conn.prepareStatement(query); pstmt.setObject(1, new BigDecimal(value)); - - rs = pstmt.executeQuery(); + DBResultSet rs = pstmt.executeQuery(); rs.verify(table1); - } - finally { - terminateVariation(); + rs.close(); } } @@ -378,19 +314,14 @@ public void testCreatepreparedStatement() throws SQLException { @Test public void testResultSet() throws SQLException { - try { - conn = new DBConnection(connectionString); - stmt = conn.createStatement(); - - String query = "SELECT * FROM " + table1.getEscapedTableName(); - rs = stmt.executeQuery(query); - + String query = "SELECT * FROM " + table1.getEscapedTableName(); + + try (DBConnection conn = new DBConnection(connectionString); + DBStatement stmt = conn.createStatement(); + DBResultSet rs = stmt.executeQuery(query)) { // verify resultSet rs.verify(table1); } - finally { - terminateVariation(); - } } /** @@ -401,12 +332,11 @@ public void testResultSet() throws SQLException { @Test public void testResultSetAndClose() throws SQLException { - try { - conn = new DBConnection(connectionString); - stmt = conn.createStatement(); - - String query = "SELECT * FROM " + table1.getEscapedTableName(); - rs = stmt.executeQuery(query); + String query = "SELECT * FROM " + table1.getEscapedTableName(); + + try (DBConnection conn = new DBConnection(connectionString); + DBStatement stmt = conn.createStatement(); + DBResultSet rs = stmt.executeQuery(query)) { try { if (null != rs) @@ -416,9 +346,6 @@ public void testResultSetAndClose() throws SQLException { fail(e.toString()); } } - finally { - terminateVariation(); - } } /** @@ -428,22 +355,17 @@ public void testResultSetAndClose() throws SQLException { */ @Test public void testTwoResultsetsDifferentStmt() throws SQLException { + + String query = "SELECT * FROM " + table1.getEscapedTableName(); + String query2 = "SELECT * FROM " + table2.getEscapedTableName(); - DBStatement stmt1 = null; - DBStatement stmt2 = null; - DBResultSet rs1 = null; - DBResultSet rs2 = null; - try { - conn = new DBConnection(connectionString); - stmt1 = conn.createStatement(); - stmt2 = conn.createStatement(); - - String query = "SELECT * FROM " + table1.getEscapedTableName(); - rs1 = stmt1.executeQuery(query); - - String query2 = "SELECT * FROM " + table2.getEscapedTableName(); - rs2 = stmt2.executeQuery(query2); + try (DBConnection conn = new DBConnection(connectionString); + DBStatement stmt1 = conn.createStatement(); + DBStatement stmt2 = conn.createStatement()) { + DBResultSet rs1 = stmt1.executeQuery(query); + DBResultSet rs2 = stmt2.executeQuery(query2); + // Interleave resultset calls rs1.next(); rs1.verifyCurrentRow(table1); @@ -455,21 +377,7 @@ public void testTwoResultsetsDifferentStmt() throws SQLException { rs1.close(); rs2.next(); rs2.verify(table2); - } - finally { - if (null != rs1) { - rs1.close(); - } - if (null != rs2) { - rs2.close(); - } - if (null != stmt1) { - stmt1.close(); - } - if (null != stmt2) { - stmt2.close(); - } - terminateVariation(); + rs2.close(); } } @@ -481,18 +389,14 @@ public void testTwoResultsetsDifferentStmt() throws SQLException { @Test public void testTwoResultsetsSameStmt() throws SQLException { - DBResultSet rs1 = null; - DBResultSet rs2 = null; - try { - conn = new DBConnection(connectionString); - stmt = conn.createStatement(); - - String query = "SELECT * FROM " + table1.getEscapedTableName(); - rs1 = stmt.executeQuery(query); - - String query2 = "SELECT * FROM " + table2.getEscapedTableName(); - rs2 = stmt.executeQuery(query2); + String query = "SELECT * FROM " + table1.getEscapedTableName(); + String query2 = "SELECT * FROM " + table2.getEscapedTableName(); + + try (DBConnection conn = new DBConnection(connectionString); + DBStatement stmt = conn.createStatement()) { + DBResultSet rs1 = stmt.executeQuery(query); + DBResultSet rs2 = stmt.executeQuery(query2); // Interleave resultset calls. rs is expected to be closed try { rs1.next(); @@ -511,15 +415,7 @@ public void testTwoResultsetsSameStmt() throws SQLException { rs1.close(); rs2.next(); rs2.verify(table2); - } - finally { - if (null != rs1) { - rs1.close(); - } - if (null != rs2) { - rs2.close(); - } - terminateVariation(); + rs2.close(); } } @@ -530,12 +426,10 @@ public void testTwoResultsetsSameStmt() throws SQLException { */ @Test public void testResultSetAndCloseStmt() throws SQLException { - try { - conn = new DBConnection(connectionString); - stmt = conn.createStatement(); - - String query = "SELECT * FROM " + table1.getEscapedTableName(); - rs = stmt.executeQuery(query); + String query = "SELECT * FROM " + table1.getEscapedTableName(); + try (DBConnection conn = new DBConnection(connectionString); + DBStatement stmt = conn.createStatement(); + DBResultSet rs = stmt.executeQuery(query)) { stmt.close(); // this should close the resultSet try { @@ -544,10 +438,7 @@ public void testResultSetAndCloseStmt() throws SQLException { catch (SQLException e) { assertEquals(e.toString(), "com.microsoft.sqlserver.jdbc.SQLServerException: The result set is closed."); } - assertTrue(true, "Previouse one should have thrown exception!"); - } - finally { - terminateVariation(); + assertTrue(true, "Previous one should have thrown exception!"); } } @@ -559,18 +450,12 @@ public void testResultSetAndCloseStmt() throws SQLException { @Test public void testResultSetSelectMethod() throws SQLException { - try { - conn = new DBConnection(connectionString + ";selectMethod=cursor;"); - stmt = conn.createStatement(); - - String query = "SELECT * FROM " + table1.getEscapedTableName(); - rs = stmt.executeQuery(query); - + String query = "SELECT * FROM " + table1.getEscapedTableName(); + try (DBConnection conn = new DBConnection(connectionString + ";selectMethod=cursor;"); + DBStatement stmt = conn.createStatement(); + DBResultSet rs = stmt.executeQuery(query)) { rs.verify(table1); } - finally { - terminateVariation(); - } } /** @@ -581,34 +466,10 @@ public void testResultSetSelectMethod() throws SQLException { @AfterAll public static void terminate() throws SQLException { - try { - conn = new DBConnection(connectionString); - stmt = conn.createStatement(); + try (DBConnection conn = new DBConnection(connectionString); + DBStatement stmt = conn.createStatement()) { stmt.execute("if object_id('" + table1.getEscapedTableName() + "','U') is not null" + " drop table " + table1.getEscapedTableName()); stmt.execute("if object_id('" + table2.getEscapedTableName() + "','U') is not null" + " drop table " + table2.getEscapedTableName()); } - finally { - terminateVariation(); - } - } - - /** - * cleanup after tests - * - * @throws SQLException - */ - public static void terminateVariation() throws SQLException { - if (conn != null && !conn.isClosed()) { - try { - conn.close(); - } - finally { - if (null != rs) - rs.close(); - if (null != stmt) - stmt.close(); - } - } } - } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bvt/bvtTestSetup.java b/src/test/java/com/microsoft/sqlserver/jdbc/bvt/bvtTestSetup.java index 684bfd81d..0306d6f1d 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/bvt/bvtTestSetup.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bvt/bvtTestSetup.java @@ -24,18 +24,14 @@ */ @RunWith(JUnitPlatform.class) public class bvtTestSetup extends AbstractTest { - private static DBConnection conn = null; - private static DBStatement stmt = null; static DBTable table1; static DBTable table2; @BeforeAll public static void init() throws SQLException { - try { - conn = new DBConnection(connectionString); - stmt = conn.createStatement(); - + try (DBConnection conn = new DBConnection(connectionString); + DBStatement stmt = conn.createStatement()) { // create tables table1 = new DBTable(true); stmt.createTable(table1); @@ -44,14 +40,5 @@ public static void init() throws SQLException { stmt.createTable(table2); stmt.populateTable(table2); } - finally { - if (null != stmt) { - stmt.close(); - } - if (null != conn && !conn.isClosed()) { - conn.close(); - } - } } - } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java new file mode 100644 index 000000000..dad9c195f --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java @@ -0,0 +1,209 @@ +package com.microsoft.sqlserver.jdbc.callablestatement; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; +import java.util.UUID; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerCallableStatement; +import com.microsoft.sqlserver.jdbc.SQLServerDataSource; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; + +/** + * Test CallableStatement + */ +@RunWith(JUnitPlatform.class) +public class CallableStatementTest extends AbstractTest { + private static String tableNameGUID = "uniqueidentifier_Table"; + private static String outputProcedureNameGUID = "uniqueidentifier_SP"; + private static String setNullProcedureName = "CallableStatementTest_setNull_SP"; + private static String inputParamsProcedureName = "CallableStatementTest_inputParams_SP"; + + private static Connection connection = null; + private static Statement stmt = null; + + /** + * Setup before test + * + * @throws SQLException + */ + @BeforeAll + public static void setupTest() throws SQLException { + connection = DriverManager.getConnection(connectionString); + stmt = connection.createStatement(); + + Utils.dropTableIfExists(tableNameGUID, stmt); + Utils.dropProcedureIfExists(outputProcedureNameGUID, stmt); + Utils.dropProcedureIfExists(setNullProcedureName, stmt); + Utils.dropProcedureIfExists(inputParamsProcedureName, stmt); + + createGUIDTable(stmt); + createGUIDStoredProcedure(stmt); + createSetNullPreocedure(stmt); + createInputParamsProcedure(stmt); + } + + /** + * Tests CallableStatement.getString() with uniqueidentifier parameter + * + * @throws SQLException + */ + @Test + public void getStringGUIDTest() throws SQLException { + + String sql = "{call " + outputProcedureNameGUID + "(?)}"; + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) connection.prepareCall(sql)) { + + UUID originalValue = UUID.randomUUID(); + + callableStatement.registerOutParameter(1, microsoft.sql.Types.GUID); + callableStatement.setObject(1, originalValue.toString(), microsoft.sql.Types.GUID); + callableStatement.execute(); + + String retrievedValue = callableStatement.getString(1); + + assertEquals(originalValue.toString().toLowerCase(), retrievedValue.toLowerCase()); + + } + } + + /** + * test for setNull(index, varchar) to behave as setNull(index, nvarchar) when SendStringParametersAsUnicode is true + * + * @throws SQLException + */ + @Test + public void getSetNullWithTypeVarchar() throws SQLException { + String polishchar = "\u0143"; + + SQLServerDataSource ds = new SQLServerDataSource(); + ds.setURL(connectionString); + ds.setSendStringParametersAsUnicode(true); + String sql = "{? = call " + setNullProcedureName + " (?,?)}"; + try (Connection connection = ds.getConnection(); + SQLServerCallableStatement cs = (SQLServerCallableStatement) connection.prepareCall(sql); + SQLServerCallableStatement cs2 = (SQLServerCallableStatement) connection.prepareCall(sql)){ + + cs.registerOutParameter(1, Types.INTEGER); + cs.setString(2, polishchar); + cs.setString(3, null); + cs.registerOutParameter(3, Types.VARCHAR); + cs.execute(); + + String expected = cs.getString(3); + + cs2.registerOutParameter(1, Types.INTEGER); + cs2.setString(2, polishchar); + cs2.setNull(3, Types.VARCHAR); + cs2.registerOutParameter(3, Types.NVARCHAR); + cs2.execute(); + + String actual = cs2.getString(3); + + assertEquals(expected, actual); + } + } + + + /** + * recognize parameter names with and without leading '@' + * + * @throws SQLException + */ + @Test + public void inputParamsTest() throws SQLException { + String call = "{CALL " + inputParamsProcedureName + " (?,?)}"; + ResultSet rs = null; + + // the historical way: no leading '@', parameter names respected (not positional) + CallableStatement cs1 = connection.prepareCall(call); + cs1.setString("p2", "bar"); + cs1.setString("p1", "foo"); + rs = cs1.executeQuery(); + rs.next(); + assertEquals("foobar", rs.getString(1)); + + // the "new" way: leading '@', parameter names still respected (not positional) + CallableStatement cs2 = connection.prepareCall(call); + cs2.setString("@p2", "world!"); + cs2.setString("@p1", "Hello "); + rs = cs2.executeQuery(); + rs.next(); + assertEquals("Hello world!", rs.getString(1)); + + // sanity check: unrecognized parameter name + CallableStatement cs3 = connection.prepareCall(call); + try { + cs3.setString("@whatever", "junk"); + fail("SQLServerException should have been thrown"); + } catch (SQLServerException sse) { + if (!sse.getMessage().startsWith("Parameter @whatever was not defined")) { + fail("Unexpected content in exception message"); + } + } + + } + + /** + * Cleanup after test + * + * @throws SQLException + */ + @AfterAll + public static void cleanup() throws SQLException { + Utils.dropTableIfExists(tableNameGUID, stmt); + Utils.dropProcedureIfExists(outputProcedureNameGUID, stmt); + Utils.dropProcedureIfExists(setNullProcedureName, stmt); + Utils.dropProcedureIfExists(inputParamsProcedureName, stmt); + + if (null != stmt) { + stmt.close(); + } + if (null != connection) { + connection.close(); + } + } + + private static void createGUIDStoredProcedure(Statement stmt) throws SQLException { + String sql = "CREATE PROCEDURE " + outputProcedureNameGUID + "(@p1 uniqueidentifier OUTPUT) AS SELECT @p1 = c1 FROM " + tableNameGUID + ";"; + stmt.execute(sql); + } + + private static void createGUIDTable(Statement stmt) throws SQLException { + String sql = "CREATE TABLE " + tableNameGUID + " (c1 uniqueidentifier null)"; + stmt.execute(sql); + } + + private static void createSetNullPreocedure(Statement stmt) throws SQLException { + stmt.execute("create procedure " + setNullProcedureName + " (@p1 nvarchar(255), @p2 nvarchar(255) output) as select @p2=@p1 return 0"); + } + + private static void createInputParamsProcedure(Statement stmt) throws SQLException { + String sql = + "CREATE PROCEDURE [dbo].[CallableStatementTest_inputParams_SP] " + + " @p1 nvarchar(max) = N'parameter1', " + + " @p2 nvarchar(max) = N'parameter2' " + + "AS " + + "BEGIN " + + " SET NOCOUNT ON; " + + " SELECT @p1 + @p2 AS result; " + + "END "; + stmt.execute(sql); + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/ConnectionDriverTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/ConnectionDriverTest.java index 78703e45c..963bc9bc0 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/ConnectionDriverTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/ConnectionDriverTest.java @@ -19,13 +19,17 @@ import java.sql.SQLFeatureNotSupportedException; import java.sql.Statement; import java.util.Properties; +import java.util.UUID; import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.logging.Logger; import javax.sql.ConnectionEvent; import javax.sql.PooledConnection; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; @@ -62,28 +66,28 @@ public void testConnectionDriver() throws SQLServerException { url.append("jdbc:sqlserver://" + randomServer + ";packetSize=512;"); // test defaults DriverPropertyInfo[] infoArray = d.getPropertyInfo(url.toString(), info); - for (int i = 0; i < infoArray.length; i++) { - logger.fine(infoArray[i].name); - logger.fine(infoArray[i].description); - logger.fine(new Boolean(infoArray[i].required).toString()); - logger.fine(infoArray[i].value); + for (DriverPropertyInfo anInfoArray1 : infoArray) { + logger.fine(anInfoArray1.name); + logger.fine(anInfoArray1.description); + logger.fine(new Boolean(anInfoArray1.required).toString()); + logger.fine(anInfoArray1.value); } url.append("encrypt=true; trustStore=someStore; trustStorePassword=somepassword;"); url.append("hostNameInCertificate=someHost; trustServerCertificate=true"); infoArray = d.getPropertyInfo(url.toString(), info); - for (int i = 0; i < infoArray.length; i++) { - if (infoArray[i].name.equals("encrypt")) { - assertTrue(infoArray[i].value.equals("true"), "Values are different"); + for (DriverPropertyInfo anInfoArray : infoArray) { + if (anInfoArray.name.equals("encrypt")) { + assertTrue(anInfoArray.value.equals("true"), "Values are different"); } - if (infoArray[i].name.equals("trustStore")) { - assertTrue(infoArray[i].value.equals("someStore"), "Values are different"); + if (anInfoArray.name.equals("trustStore")) { + assertTrue(anInfoArray.value.equals("someStore"), "Values are different"); } - if (infoArray[i].name.equals("trustStorePassword")) { - assertTrue(infoArray[i].value.equals("somepassword"), "Values are different"); + if (anInfoArray.name.equals("trustStorePassword")) { + assertTrue(anInfoArray.value.equals("somepassword"), "Values are different"); } - if (infoArray[i].name.equals("hostNameInCertificate")) { - assertTrue(infoArray[i].value.equals("someHost"), "Values are different"); + if (anInfoArray.name.equals("hostNameInCertificate")) { + assertTrue(anInfoArray.value.equals("someHost"), "Values are different"); } } } @@ -119,8 +123,7 @@ public void testEncryptedConnection() throws SQLException { ds.setEncrypt(true); ds.setTrustServerCertificate(true); ds.setPacketSize(8192); - Connection con = ds.getConnection(); - con.close(); + try(Connection con = ds.getConnection()) {} } @Test @@ -168,25 +171,25 @@ public void testConnectionEvents() throws SQLException { // Attach the Event listener and listen for connection events. MyEventListener myE = new MyEventListener(); - pooledConnection.addConnectionEventListener(myE); // ConnectionListener - // implements - // ConnectionEventListener - Connection con = pooledConnection.getConnection(); - Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); - - boolean exceptionThrown = false; - try { - // raise a severe exception and make sure that the connection is not - // closed. - stmt.executeUpdate("RAISERROR ('foo', 20,1) WITH LOG"); + pooledConnection.addConnectionEventListener(myE); // ConnectionListener implements ConnectionEventListener + + try(Connection con = pooledConnection.getConnection(); + Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE)) { + + boolean exceptionThrown = false; + try { + // raise a severe exception and make sure that the connection is not + // closed. + stmt.executeUpdate("RAISERROR ('foo', 20,1) WITH LOG"); + } + catch (Exception e) { + exceptionThrown = true; + } + assertTrue(exceptionThrown, "Expected exception is not thrown."); + + // Check to see if error occurred. + assertTrue(myE.errorOccurred, "Error occurred is not called."); } - catch (Exception e) { - exceptionThrown = true; - } - assertTrue(exceptionThrown, "Expected exception is not thrown."); - - // Check to see if error occurred. - assertTrue(myE.errorOccurred, "Error occurred is not called."); // make sure that connection is closed. } @@ -200,23 +203,18 @@ public void testConnectionPoolGetTwice() throws SQLException { // Attach the Event listener and listen for connection events. MyEventListener myE = new MyEventListener(); - pooledConnection.addConnectionEventListener(myE); // ConnectionListener - // implements - // ConnectionEventListener + pooledConnection.addConnectionEventListener(myE); // ConnectionListener implements ConnectionEventListener - Connection con = pooledConnection.getConnection(); - Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); - - // raise a non severe exception and make sure that the connection is not - // closed. + Connection con = pooledConnection.getConnection(); + Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); + // raise a non severe exception and make sure that the connection is not closed. stmt.executeUpdate("RAISERROR ('foo', 3,1) WITH LOG"); - // not a serious error there should not be any errors. assertTrue(!myE.errorOccurred, "Error occurred is called."); // check to make sure that connection is not closed. - assertTrue(!con.isClosed(), "Connection is closed."); - - con.close(); + assertTrue(!con.isClosed(), "Connection is closed."); + stmt.close(); + con.close(); // check to make sure that connection is closed. assertTrue(con.isClosed(), "Connection is not closed."); } @@ -238,36 +236,34 @@ public void testConnectionClosed() throws SQLException { exceptionThrown = true; } assertTrue(exceptionThrown, "Expected exception is not thrown."); - + // check to make sure that connection is closed. assertTrue(con.isClosed(), "Connection is not closed."); } @Test public void testIsWrapperFor() throws SQLException, ClassNotFoundException { - Connection conn = DriverManager.getConnection(connectionString); - SQLServerConnection ssconn = (SQLServerConnection) conn; - boolean isWrapper; - isWrapper = ssconn.isWrapperFor(ssconn.getClass()); - assertTrue(isWrapper, "SQLServerConnection supports unwrapping"); - assertEquals(ssconn.TRANSACTION_SNAPSHOT, ssconn.TRANSACTION_SNAPSHOT, "Cant access the TRANSACTION_SNAPSHOT "); - - isWrapper = ssconn.isWrapperFor(Class.forName("com.microsoft.sqlserver.jdbc.ISQLServerConnection")); - assertTrue(isWrapper, "ISQLServerConnection supports unwrapping"); - ISQLServerConnection iSql = (ISQLServerConnection) ssconn.unwrap(Class.forName("com.microsoft.sqlserver.jdbc.ISQLServerConnection")); - assertEquals(iSql.TRANSACTION_SNAPSHOT, iSql.TRANSACTION_SNAPSHOT, "Cant access the TRANSACTION_SNAPSHOT "); - - ssconn.unwrap(Class.forName("java.sql.Connection")); - - conn.close(); + try(Connection conn = DriverManager.getConnection(connectionString); + SQLServerConnection ssconn = (SQLServerConnection) conn) { + boolean isWrapper; + isWrapper = ssconn.isWrapperFor(ssconn.getClass()); + assertTrue(isWrapper, "SQLServerConnection supports unwrapping"); + assertEquals(ssconn.TRANSACTION_SNAPSHOT, ssconn.TRANSACTION_SNAPSHOT, "Cant access the TRANSACTION_SNAPSHOT "); + + isWrapper = ssconn.isWrapperFor(Class.forName("com.microsoft.sqlserver.jdbc.ISQLServerConnection")); + assertTrue(isWrapper, "ISQLServerConnection supports unwrapping"); + ISQLServerConnection iSql = (ISQLServerConnection) ssconn.unwrap(Class.forName("com.microsoft.sqlserver.jdbc.ISQLServerConnection")); + assertEquals(iSql.TRANSACTION_SNAPSHOT, iSql.TRANSACTION_SNAPSHOT, "Cant access the TRANSACTION_SNAPSHOT "); + + ssconn.unwrap(Class.forName("java.sql.Connection")); + } } @Test public void testNewConnection() throws SQLException { - SQLServerConnection conn = (SQLServerConnection) DriverManager.getConnection(connectionString); - assertTrue(conn.isValid(0), "Newly created connection should be valid"); - - conn.close(); + try(SQLServerConnection conn = (SQLServerConnection) DriverManager.getConnection(connectionString)) { + assertTrue(conn.isValid(0), "Newly created connection should be valid"); + } } @Test @@ -279,23 +275,22 @@ public void testClosedConnection() throws SQLException { @Test public void testNegativeTimeout() throws Exception { - SQLServerConnection conn = (SQLServerConnection) DriverManager.getConnection(connectionString); - try { - conn.isValid(-42); - throw new Exception("No exception thrown with negative timeout"); - } - catch (SQLException e) { - assertEquals(e.getMessage(), "The query timeout value -42 is not valid.", "Wrong exception message"); + try (SQLServerConnection conn = (SQLServerConnection) DriverManager.getConnection(connectionString)) { + try { + conn.isValid(-42); + throw new Exception("No exception thrown with negative timeout"); + } + catch (SQLException e) { + assertEquals(e.getMessage(), "The query timeout value -42 is not valid.", "Wrong exception message"); + } } - - conn.close(); } @Test public void testDeadConnection() throws SQLException { assumeTrue(!DBConnection.isSqlAzure(DriverManager.getConnection(connectionString)), "Skipping test case on Azure SQL."); - connectionString += "connectRetryCount=0"; + connectionString += ";connectRetryCount=0"; SQLServerConnection conn = (SQLServerConnection) DriverManager.getConnection(connectionString + ";responseBuffering=adaptive"); Statement stmt = null; @@ -460,6 +455,7 @@ public void testInvalidCombination() throws SQLServerException { } @Test + @Tag("slow") public void testIncorrectDatabaseWithFailoverPartner() throws SQLServerException { long timerStart = 0; long timerEnd = 0; @@ -511,4 +507,46 @@ public void testGetSchema() throws SQLException { SQLServerConnection conn = (SQLServerConnection) DriverManager.getConnection(connectionString); conn.getSchema(); } + + static Boolean isInterrupted = false; + + /** + * Test thread's interrupt status is not cleared. + * + * @throws InterruptedException + */ + @Test + @Tag("slow") + public void testThreadInterruptedStatus() throws InterruptedException { + Runnable runnable = new Runnable() { + public void run() { + SQLServerDataSource ds = new SQLServerDataSource(); + + ds.setURL(connectionString); + ds.setServerName("invalidServerName" + UUID.randomUUID()); + ds.setLoginTimeout(5); + + try { + ds.getConnection(); + } + catch (SQLException e) { + isInterrupted = Thread.currentThread().isInterrupted(); + } + } + }; + + ExecutorService executor = Executors.newFixedThreadPool(1); + Future future = executor.submit(runnable); + + Thread.sleep(1000); + + // interrupt the thread in the Runnable + future.cancel(true); + + Thread.sleep(8000); + + executor.shutdownNow(); + + assertTrue(isInterrupted, "Thread's interrupt status is not set."); + } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/DBMetadataTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/DBMetadataTest.java index 9645d5b95..f71da3f7f 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/DBMetadataTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/DBMetadataTest.java @@ -11,13 +11,13 @@ import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; import com.microsoft.sqlserver.jdbc.SQLServerDataSource; -import com.microsoft.sqlserver.jdbc.SQLServerException; import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.DBTable; import com.microsoft.sqlserver.testframework.util.RandomUtil; @@ -32,26 +32,27 @@ public void testDatabaseMetaData() throws SQLException { SQLServerDataSource ds = new SQLServerDataSource(); ds.setURL(connectionString); - Connection con = ds.getConnection(); - - // drop function String sqlDropFunction = "if exists (select * from dbo.sysobjects where id = object_id(N'[dbo]." + functionName + "')" + "and xtype in (N'FN', N'IF', N'TF'))" + "drop function " + functionName; - con.createStatement().execute(sqlDropFunction); - - // create function String sqlCreateFunction = "CREATE FUNCTION " + functionName + " (@text varchar(8000), @delimiter varchar(20) = ' ') RETURNS @Strings TABLE " + "(position int IDENTITY PRIMARY KEY, value varchar(8000)) AS BEGIN INSERT INTO @Strings VALUES ('DDD') RETURN END "; - con.createStatement().execute(sqlCreateFunction); - - DatabaseMetaData md = con.getMetaData(); - ResultSet arguments = md.getProcedureColumns(null, null, null, "@TABLE_RETURN_VALUE"); - - if (arguments.next()) { - arguments.getString("COLUMN_NAME"); - arguments.getString("DATA_TYPE"); // call this function to make sure it does not crash + + try (Connection con = ds.getConnection(); + Statement stmt = con.createStatement()) { + // drop function + stmt.execute(sqlDropFunction); + // create function + stmt.execute(sqlCreateFunction); + + DatabaseMetaData md = con.getMetaData(); + try (ResultSet arguments = md.getProcedureColumns(null, null, null, "@TABLE_RETURN_VALUE")) { + + if (arguments.next()) { + arguments.getString("COLUMN_NAME"); + arguments.getString("DATA_TYPE"); // call this function to make sure it does not crash + } + } + stmt.execute(sqlDropFunction); } - - con.createStatement().execute(sqlDropFunction); } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/DriverVersionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/DriverVersionTest.java new file mode 100644 index 000000000..911120ebb --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/DriverVersionTest.java @@ -0,0 +1,107 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.connection; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import java.util.Random; + +import javax.xml.bind.DatatypeConverter; + +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.testframework.AbstractTest; + +/** + * This test validates PR #342. In this PR, DatatypeConverter#parseHexBinary is replaced with type casting. This tests validates if the behavior + * reminds the same. + * + * The valid length of driver version byte array has to be 4. Otherwise, connection won't be established. Therefore, the valid value for the original + * method is between 0 and 255 (inclusive). + * + */ +@RunWith(JUnitPlatform.class) +public class DriverVersionTest extends AbstractTest { + Random rand = new Random(); + int major = rand.nextInt(256); + int minor = rand.nextInt(256); + int patch = rand.nextInt(256); + int build = rand.nextInt(256); + + /** + * validates version byte array generated by the original method and type casting reminds the same. + */ + @Test + public void testConnectionDriver() { + // the original way to create version byte array + String interfaceLibVersion = generateInterfaceLibVersion(); + byte originalVersionBytes[] = DatatypeConverter.parseHexBinary(interfaceLibVersion); + + String originalBytes = Arrays.toString(originalVersionBytes); + + // the new way to create version byte array + byte newVersionBytes[] = {(byte) build, (byte) patch, (byte) minor, (byte) major}; + + String newBytes = Arrays.toString(newVersionBytes); + + assertEquals(originalBytes, newBytes, "Original: " + originalBytes + "; New: " + newBytes); + } + + /** + * the original method that converts version number to hex string + * + * @return + */ + private String generateInterfaceLibVersion() { + StringBuilder outputInterfaceLibVersion = new StringBuilder(); + + String interfaceLibMajor = Integer.toHexString(major); + String interfaceLibMinor = Integer.toHexString(minor); + String interfaceLibPatch = Integer.toHexString(patch); + String interfaceLibBuild = Integer.toHexString(build); + + // build the interface lib name + // 2 characters reserved for build + // 2 characters reserved for patch + // 2 characters reserved for minor + // 2 characters reserved for major + if (2 == interfaceLibBuild.length()) { + outputInterfaceLibVersion.append(interfaceLibBuild); + } + else { + outputInterfaceLibVersion.append("0"); + outputInterfaceLibVersion.append(interfaceLibBuild); + } + if (2 == interfaceLibPatch.length()) { + outputInterfaceLibVersion.append(interfaceLibPatch); + } + else { + outputInterfaceLibVersion.append("0"); + outputInterfaceLibVersion.append(interfaceLibPatch); + } + if (2 == interfaceLibMinor.length()) { + outputInterfaceLibVersion.append(interfaceLibMinor); + } + else { + outputInterfaceLibVersion.append("0"); + outputInterfaceLibVersion.append(interfaceLibMinor); + } + if (2 == interfaceLibMajor.length()) { + outputInterfaceLibVersion.append(interfaceLibMajor); + } + else { + outputInterfaceLibVersion.append("0"); + outputInterfaceLibVersion.append(interfaceLibMajor); + } + + return outputInterfaceLibVersion.toString(); + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/NativeMSSQLDataSourceTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/NativeMSSQLDataSourceTest.java index 08ba2a58a..57803a32e 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/NativeMSSQLDataSourceTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/NativeMSSQLDataSourceTest.java @@ -43,36 +43,34 @@ public void testNativeMSSQLDataSource() throws SQLException { @Test public void testSerialization() throws IOException { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - ObjectOutput objectOutput = new ObjectOutputStream(outputStream); - - SQLServerDataSource ds = new SQLServerDataSource(); - ds.setLogWriter(new PrintWriter(new ByteArrayOutputStream())); - - objectOutput.writeObject(ds); - objectOutput.flush(); + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ObjectOutput objectOutput = new ObjectOutputStream(outputStream)) { + SQLServerDataSource ds = new SQLServerDataSource(); + ds.setLogWriter(new PrintWriter(new ByteArrayOutputStream())); + + objectOutput.writeObject(ds); + objectOutput.flush(); + } } @Test - public void testDSNormal() throws SQLServerException, ClassNotFoundException, IOException { + public void testDSNormal() throws ClassNotFoundException, IOException, SQLException { SQLServerDataSource ds = new SQLServerDataSource(); ds.setURL(connectionString); - Connection conn = ds.getConnection(); + try (Connection conn = ds.getConnection()) {} ds = testSerial(ds); - conn = ds.getConnection(); + try (Connection conn = ds.getConnection()) {} } @Test - public void testDSTSPassword() throws SQLServerException, ClassNotFoundException, IOException { + public void testDSTSPassword() throws ClassNotFoundException, IOException, SQLException { SQLServerDataSource ds = new SQLServerDataSource(); System.setProperty("java.net.preferIPv6Addresses", "true"); ds.setURL(connectionString); ds.setTrustStorePassword("wrong_password"); - Connection conn = ds.getConnection(); + try (Connection conn = ds.getConnection()) {} ds = testSerial(ds); - try { - conn = ds.getConnection(); - } + try (Connection conn = ds.getConnection()) {} catch (SQLServerException e) { assertEquals("The DataSource trustStore password needs to be set.", e.getMessage()); } @@ -106,13 +104,16 @@ public void testInterfaceWrapping() throws ClassNotFoundException, SQLException } private SQLServerDataSource testSerial(SQLServerDataSource ds) throws IOException, ClassNotFoundException { - java.io.ByteArrayOutputStream outputStream = new java.io.ByteArrayOutputStream(); - java.io.ObjectOutput objectOutput = new java.io.ObjectOutputStream(outputStream); - objectOutput.writeObject(ds); - objectOutput.flush(); - ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(outputStream.toByteArray())); - SQLServerDataSource dtn; - dtn = (SQLServerDataSource) in.readObject(); - return dtn; + try (java.io.ByteArrayOutputStream outputStream = new java.io.ByteArrayOutputStream(); + java.io.ObjectOutput objectOutput = new java.io.ObjectOutputStream(outputStream)) { + objectOutput.writeObject(ds); + objectOutput.flush(); + + try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(outputStream.toByteArray()))) { + SQLServerDataSource dtn; + dtn = (SQLServerDataSource) in.readObject(); + return dtn; + } + } } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/PoolingTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/PoolingTest.java index 1c254b54b..1a51deb34 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/PoolingTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/PoolingTest.java @@ -59,17 +59,15 @@ public void testPooling() throws SQLException { XADataSource1.setDatabaseName("tempdb"); PooledConnection pc = XADataSource1.getPooledConnection(); - Connection conn = pc.getConnection(); - - // create table in tempdb database - conn.createStatement().execute("create table [" + tempTableName + "] (myid int)"); - conn.createStatement().execute("insert into [" + tempTableName + "] values (1)"); - conn.close(); - - conn = pc.getConnection(); + try (Connection conn = pc.getConnection()) { + + // create table in tempdb database + conn.createStatement().execute("create table [" + tempTableName + "] (myid int)"); + conn.createStatement().execute("insert into [" + tempTableName + "] values (1)"); + } boolean tempTableFileRemoved = false; - try { + try (Connection conn = pc.getConnection()) { conn.createStatement().executeQuery("select * from [" + tempTableName + "]"); } catch (SQLServerException e) { @@ -109,12 +107,12 @@ public void testConnectionPoolConnFunctions() throws SQLException { ds.setURL(connectionString); PooledConnection pc = ds.getPooledConnection(); - Connection con = pc.getConnection(); - - Statement statement = con.createStatement(); - statement.execute(sql1); - statement.execute(sql2); - con.clearWarnings(); + try (Connection con = pc.getConnection(); + Statement statement = con.createStatement()) { + statement.execute(sql1); + statement.execute(sql2); + con.clearWarnings(); + } pc.close(); } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/SSLProtocolTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/SSLProtocolTest.java new file mode 100644 index 000000000..5d06220b8 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/SSLProtocolTest.java @@ -0,0 +1,104 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.connection; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.Statement; + +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.StringUtils; +import com.microsoft.sqlserver.testframework.AbstractTest; + +/** + * Tests new connection property sslProtocol + */ +@RunWith(JUnitPlatform.class) +public class SSLProtocolTest extends AbstractTest { + + Connection con = null; + Statement stmt = null; + + /** + * Connect with supported protocol + * + * @param sslProtocol + * @throws Exception + */ + public void testWithSupportedProtocols(String sslProtocol) throws Exception { + String url = connectionString + ";sslProtocol=" + sslProtocol; + try { + con = DriverManager.getConnection(url); + DatabaseMetaData dbmd = con.getMetaData(); + assertNotNull(dbmd); + assertTrue(!StringUtils.isEmpty(dbmd.getDatabaseProductName())); + } + catch (SQLServerException e) { + // Some older versions of SQLServer might not have all the TLS protocol versions enabled. + // Example, if the highest TLS version enabled in the server is TLSv1.1, + // the connection will fail if we enable only TLSv1.2 + assertTrue(e.getMessage().contains("protocol version is not enabled or not supported by the client.")); + } + } + + + /** + * Connect with unsupported protocol + * + * @param sslProtocol + * @throws Exception + */ + public void testWithUnSupportedProtocols(String sslProtocol) throws Exception { + try { + String url = connectionString + ";sslProtocol=" + sslProtocol; + con = DriverManager.getConnection(url); + assertFalse(true, "Any protocol other than TLSv1, TLSv1.1 & TLSv1.2 should throw Exception"); + } + catch (SQLServerException e) { + assertTrue(true, "Should throw exception"); + String errMsg = "SSL Protocol " + sslProtocol + " label is not valid. Only TLS, TLSv1, TLSv1.1 & TLSv1.2 are supported."; + assertTrue(errMsg.equals(e.getMessage()), "Message should be from SQL Server resources : " + e.getMessage()); + } + } + + /** + * Test with unsupported protocols. + * + * @throws Exception + */ + @Test + public void testConnectWithWrongProtocols() throws Exception { + String[] wrongProtocols = {"SSLv1111", "SSLv2222", "SSLv3111", "SSLv2Hello1111", "TLSv1.11", "TLSv2.4", "random"}; + for (String wrongProtocol : wrongProtocols) { + testWithUnSupportedProtocols(wrongProtocol); + } + } + + /** + * Test with supported protocols. + * + * @throws Exception + */ + @Test + public void testConnectWithSupportedProtocols() throws Exception { + String[] supportedProtocols = {"TLS", "TLSv1", "TLSv1.1", "TLSv1.2"}; + for (String supportedProtocol : supportedProtocols) { + testWithSupportedProtocols(supportedProtocol); + } + } +} + diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java index 9ebdd8ba9..1fc8c479b 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java @@ -9,11 +9,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.sql.DriverManager; import java.sql.SQLException; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; @@ -21,6 +23,7 @@ import com.microsoft.sqlserver.jdbc.SQLServerConnection; import com.microsoft.sqlserver.jdbc.SQLServerException; import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; import com.microsoft.sqlserver.testframework.util.RandomUtil; @RunWith(JUnitPlatform.class) @@ -30,6 +33,7 @@ public class TimeoutTest extends AbstractTest { final int waitForDelaySeconds = 10; @Test + @Tag("slow") public void testDefaultLoginTimeout() { long timerStart = 0; long timerEnd = 0; @@ -87,6 +91,10 @@ public void testFOInstanceResolution2() throws SQLServerException { assertTrue(timeDiff > 14000); } + /** + * When query timeout occurs, the connection is still usable. + * @throws Exception + */ @Test public void testQueryTimeout() throws Exception { SQLServerConnection conn = (SQLServerConnection) DriverManager.getConnection(connectionString); @@ -106,8 +114,17 @@ public void testQueryTimeout() throws Exception { } assertEquals(e.getMessage(), "The query has timed out.", "Invalid exception message"); } + try{ + conn.createStatement().execute("SELECT @@version"); + }catch (Exception e) { + fail("Unexpected error message occured! "+ e.toString() ); + } } + /** + * When socketTimeout occurs, the connection will be marked as closed. + * @throws Exception + */ @Test @Disabled public void testSocketTimeout() throws Exception { @@ -118,7 +135,7 @@ public void testSocketTimeout() throws Exception { createWaitForDelayPreocedure(conn); // cancel connection resilience to test socketTimeout - connectionString += "connectRetryCount=0"; + connectionString += ";connectRetryCount=0"; conn = (SQLServerConnection) DriverManager.getConnection(connectionString + ";socketTimeout=" + (waitForDelaySeconds * 1000 / 2) + ";"); try { @@ -131,12 +148,15 @@ public void testSocketTimeout() throws Exception { } assertEquals(e.getMessage(), "Read timed out", "Invalid exception message"); } + try{ + conn.createStatement().execute("SELECT @@version"); + }catch (SQLServerException e) { + assertEquals(e.getMessage(), "The connection is closed.", "Invalid exception message"); + } } private void dropWaitForDelayProcedure(SQLServerConnection conn) throws SQLException { - String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + waitForDelaySPName - + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + waitForDelaySPName; - conn.createStatement().execute(sql); + Utils.dropProcedureIfExists(waitForDelaySPName, conn.createStatement()); } private void createWaitForDelayPreocedure(SQLServerConnection conn) throws SQLException { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataForeignKeyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataForeignKeyTest.java new file mode 100644 index 000000000..b498a6afb --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataForeignKeyTest.java @@ -0,0 +1,241 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.databasemetadata; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.sql.DriverManager; +import java.sql.SQLException; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.SQLServerDatabaseMetaData; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.SQLServerResultSet; +import com.microsoft.sqlserver.jdbc.SQLServerStatement; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; + +/** + * Test class for testing DatabaseMetaData with foreign keys. + */ +@RunWith(JUnitPlatform.class) +public class DatabaseMetaDataForeignKeyTest extends AbstractTest { + private static SQLServerConnection conn = null; + private static SQLServerStatement stmt = null; + + private static String table1 = "DatabaseMetaDataForeignKeyTest_table_1"; + private static String table2 = "DatabaseMetaDataForeignKeyTest_table_2"; + private static String table3 = "DatabaseMetaDataForeignKeyTest_table_3"; + private static String table4 = "DatabaseMetaDataForeignKeyTest_table_4"; + private static String table5 = "DatabaseMetaDataForeignKeyTest_table_5"; + + private static String schema = null; + private static String catalog = null; + + private static final String EXPECTED_ERROR_MESSAGE = "An object or column name is missing or empty."; + private static final String EXPECTED_ERROR_MESSAGE2 = "The database name component of the object qualifier must be the name of the current database."; + + + @BeforeAll + private static void setupVariation() throws SQLException { + conn = (SQLServerConnection) DriverManager.getConnection(connectionString); + SQLServerStatement stmt = (SQLServerStatement) conn.createStatement(); + + catalog = conn.getCatalog(); + schema = conn.getSchema(); + + connection.createStatement().executeUpdate("if object_id('" + table1 + "','U') is not null drop table " + table1); + + connection.createStatement().executeUpdate("if object_id('" + table2 + "','U') is not null drop table " + table2); + stmt.execute("Create table " + table2 + " (c21 int NOT NULL PRIMARY KEY)"); + + connection.createStatement().executeUpdate("if object_id('" + table3 + "','U') is not null drop table " + table3); + stmt.execute("Create table " + table3 + " (c31 int NOT NULL PRIMARY KEY)"); + + connection.createStatement().executeUpdate("if object_id('" + table4 + "','U') is not null drop table " + table4); + stmt.execute("Create table " + table4 + " (c41 int NOT NULL PRIMARY KEY)"); + + connection.createStatement().executeUpdate("if object_id('" + table5 + "','U') is not null drop table " + table5); + stmt.execute("Create table " + table5 + " (c51 int NOT NULL PRIMARY KEY)"); + + connection.createStatement().executeUpdate("if object_id('" + table1 + "','U') is not null drop table " + table1); + stmt.execute("Create table " + table1 + " (c11 int primary key," + + " c12 int FOREIGN KEY REFERENCES " + table2 + "(c21) ON DELETE no action ON UPDATE set default," + + " c13 int FOREIGN KEY REFERENCES " + table3 + "(c31) ON DELETE cascade ON UPDATE set null," + + " c14 int FOREIGN KEY REFERENCES " + table4 + "(c41) ON DELETE set null ON UPDATE cascade," + + " c15 int FOREIGN KEY REFERENCES " + table5 + "(c51) ON DELETE set default ON UPDATE no action," + + ")"); + } + + @AfterAll + private static void terminateVariation() throws SQLException { + conn = (SQLServerConnection) DriverManager.getConnection(connectionString); + stmt = (SQLServerStatement) conn.createStatement(); + + Utils.dropTableIfExists(table1, stmt); + Utils.dropTableIfExists(table2, stmt); + Utils.dropTableIfExists(table3, stmt); + Utils.dropTableIfExists(table4, stmt); + Utils.dropTableIfExists(table5, stmt); + } + + /** + * test getImportedKeys() methods + * + * @throws SQLServerException + */ + @Test + public void testGetImportedKeys() throws SQLServerException { + SQLServerDatabaseMetaData dmd = (SQLServerDatabaseMetaData) connection.getMetaData(); + + SQLServerResultSet rs1 = (SQLServerResultSet) dmd.getImportedKeys(null, null, table1); + validateGetImportedKeysResults(rs1); + + SQLServerResultSet rs2 = (SQLServerResultSet) dmd.getImportedKeys(catalog, schema, table1); + validateGetImportedKeysResults(rs2); + + SQLServerResultSet rs3 = (SQLServerResultSet) dmd.getImportedKeys(catalog, "", table1); + validateGetImportedKeysResults(rs3); + + try { + dmd.getImportedKeys("", schema, table1); + fail("Exception is not thrown."); + } + catch (SQLServerException e) { + assertTrue(e.getMessage().startsWith(EXPECTED_ERROR_MESSAGE)); + } + } + + private void validateGetImportedKeysResults(SQLServerResultSet rs) throws SQLServerException { + int expectedRowCount = 4; + int rowCount = 0; + + rs.next(); + assertEquals(4, rs.getInt("UPDATE_RULE")); + assertEquals(3, rs.getInt("DELETE_RULE")); + rowCount++; + + rs.next(); + assertEquals(2, rs.getInt("UPDATE_RULE")); + assertEquals(0, rs.getInt("DELETE_RULE")); + rowCount++; + + rs.next(); + assertEquals(0, rs.getInt("UPDATE_RULE")); + assertEquals(2, rs.getInt("DELETE_RULE")); + rowCount++; + + rs.next(); + assertEquals(3, rs.getInt("UPDATE_RULE")); + assertEquals(4, rs.getInt("DELETE_RULE")); + rowCount++; + + if(expectedRowCount != rowCount) { + assertEquals(expectedRowCount, rowCount, "number of foreign key entries is incorrect."); + } + } + + /** + * test getExportedKeys() methods + * + * @throws SQLServerException + */ + @Test + public void testGetExportedKeys() throws SQLServerException { + String[] tableNames = {table2, table3, table4, table5}; + int[][] values = { + // expected UPDATE_RULE, expected DELETE_RULE + {4, 3}, + {2, 0}, + {0, 2}, + {3, 4} + }; + + SQLServerDatabaseMetaData dmd = (SQLServerDatabaseMetaData) connection.getMetaData(); + + for (int i = 0; i < tableNames.length; i++) { + String pkTable = tableNames[i]; + SQLServerResultSet rs1 = (SQLServerResultSet) dmd.getExportedKeys(null, null, pkTable); + rs1.next(); + assertEquals(values[i][0], rs1.getInt("UPDATE_RULE")); + assertEquals(values[i][1], rs1.getInt("DELETE_RULE")); + + SQLServerResultSet rs2 = (SQLServerResultSet) dmd.getExportedKeys(catalog, schema, pkTable); + rs2.next(); + assertEquals(values[i][0], rs2.getInt("UPDATE_RULE")); + assertEquals(values[i][1], rs2.getInt("DELETE_RULE")); + + SQLServerResultSet rs3 = (SQLServerResultSet) dmd.getExportedKeys(catalog, "", pkTable); + rs3.next(); + assertEquals(values[i][0], rs3.getInt("UPDATE_RULE")); + assertEquals(values[i][1], rs3.getInt("DELETE_RULE")); + + try { + dmd.getExportedKeys("", schema, pkTable); + fail("Exception is not thrown."); + } + catch (SQLServerException e) { + assertTrue(e.getMessage().startsWith(EXPECTED_ERROR_MESSAGE)); + } + } + } + + /** + * test getCrossReference() methods + * + * @throws SQLServerException + */ + @Test + public void testGetCrossReference() throws SQLServerException { + String fkTable = table1; + String[] tableNames = {table2, table3, table4, table5}; + int[][] values = { + // expected UPDATE_RULE, expected DELETE_RULE + {4, 3}, + {2, 0}, + {0, 2}, + {3, 4} + }; + + SQLServerDatabaseMetaData dmd = (SQLServerDatabaseMetaData) connection.getMetaData(); + + for (int i = 0; i < tableNames.length; i++) { + String pkTable = tableNames[i]; + SQLServerResultSet rs1 = (SQLServerResultSet) dmd.getCrossReference(null, null, pkTable, null, null, fkTable); + rs1.next(); + assertEquals(values[i][0], rs1.getInt("UPDATE_RULE")); + assertEquals(values[i][1], rs1.getInt("DELETE_RULE")); + + SQLServerResultSet rs2 = (SQLServerResultSet) dmd.getCrossReference(catalog, schema, pkTable, catalog, schema, fkTable); + rs2.next(); + assertEquals(values[i][0], rs2.getInt("UPDATE_RULE")); + assertEquals(values[i][1], rs2.getInt("DELETE_RULE")); + + SQLServerResultSet rs3 = (SQLServerResultSet) dmd.getCrossReference(catalog, "", pkTable, catalog, "", fkTable); + rs3.next(); + assertEquals(values[i][0], rs3.getInt("UPDATE_RULE")); + assertEquals(values[i][1], rs3.getInt("DELETE_RULE")); + + try { + dmd.getCrossReference("", schema, pkTable, "", schema, fkTable); + fail("Exception is not thrown."); + } + catch (SQLServerException e) { + assertEquals(EXPECTED_ERROR_MESSAGE2, e.getMessage()); + } + } + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java index d822c8229..9db744faa 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java @@ -6,23 +6,42 @@ * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. */ package com.microsoft.sqlserver.jdbc.databasemetadata; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.DriverManager; +import java.sql.ResultSet; import java.sql.SQLException; +import java.util.jar.Attributes; +import java.util.jar.Manifest; import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; +import com.microsoft.sqlserver.jdbc.SQLServerDatabaseMetaData; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.StringUtils; import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; +/** + * Test class for testing DatabaseMetaData. + */ @RunWith(JUnitPlatform.class) public class DatabaseMetaDataTest extends AbstractTest { - + /** * Verify DatabaseMetaData#isWrapperFor and DatabaseMetaData#unwrap. * @@ -37,4 +56,287 @@ public void testDatabaseMetaDataWrapper() throws SQLException { } } + /** + * Testing if driver version is matching with manifest file or not. Will be useful while releasing preview / RTW release. + * + * //TODO: OSGI: Test for capability 1.7 for JDK 1.7 and 1.8 for 1.8 //Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.8))" //String + * capability = attributes.getValue("Require-Capability"); + * + * @throws SQLServerException + * Our Wrapped Exception + * @throws SQLException + * SQL Exception + * @throws IOException + * IOExcption + */ + @Test + public void testDriverVersion() throws SQLServerException, SQLException, IOException { + String manifestFile = Utils.getCurrentClassPath() + "META-INF/MANIFEST.MF"; + manifestFile = manifestFile.replace("test-classes", "classes"); + + File f = new File(manifestFile); + + assumeTrue(f.exists(), "Manifest file does not exist on classpath so ignoring test"); + + InputStream in = new BufferedInputStream(new FileInputStream(f)); + Manifest manifest = new Manifest(in); + Attributes attributes = manifest.getMainAttributes(); + String buildVersion = attributes.getValue("Bundle-Version"); + + DatabaseMetaData dbmData = connection.getMetaData(); + + String driverVersion = dbmData.getDriverVersion(); + + boolean isSnapshot = buildVersion.contains("SNAPSHOT"); + + // Removing all dots & chars easy for comparing. + driverVersion = driverVersion.replaceAll("[^0-9]", ""); + buildVersion = buildVersion.replaceAll("[^0-9]", ""); + + // Not comparing last build number. We will compare only major.minor.patch + driverVersion = driverVersion.substring(0, 3); + buildVersion = buildVersion.substring(0, 3); + + int intBuildVersion = Integer.valueOf(buildVersion); + int intDriverVersion = Integer.valueOf(driverVersion); + + if (isSnapshot) { + assertTrue(intDriverVersion < intBuildVersion, + "In case of SNAPSHOT version, build version should be always greater than BuildVersion"); + } + else { + assertTrue(intDriverVersion == intBuildVersion, "For NON SNAPSHOT versions build & driver versions should match."); + } + + } + + /** + * Your password should not be in getURL method. + * + * @throws SQLServerException + * @throws SQLException + */ + @Test + public void testGetURL() throws SQLServerException, SQLException { + DatabaseMetaData databaseMetaData = connection.getMetaData(); + String url = databaseMetaData.getURL(); + url = url.toLowerCase(); + assertFalse(url.contains("password"), "Get URL should not have password attribute / property."); + } + + /** + * Test getUsername. + * + * @throws SQLServerException + * @throws SQLException + */ + @Test + public void testDBUserLogin() throws SQLServerException, SQLException { + DatabaseMetaData databaseMetaData = connection.getMetaData(); + + String connectionString = getConfiguredProperty("mssql_jdbc_test_connection_properties"); + + connectionString = connectionString.toLowerCase(); + + int startIndex = 0; + int endIndex = 0; + + if (connectionString.contains("username")) { + startIndex = connectionString.indexOf("username="); + endIndex = connectionString.indexOf(";", startIndex); + startIndex = startIndex + "username=".length(); + } + else if (connectionString.contains("user")) { + startIndex = connectionString.indexOf("user="); + endIndex = connectionString.indexOf(";", startIndex); + startIndex = startIndex + "user=".length(); + } + + String userFromConnectionString = connectionString.substring(startIndex, endIndex); + String userName = databaseMetaData.getUserName(); + + assertNotNull(userName, "databaseMetaData.getUserName() should not be null"); + + assertTrue(userName.equalsIgnoreCase(userFromConnectionString), + "databaseMetaData.getUserName() should match with UserName from Connection String."); + } + + /** + * Testing of {@link SQLServerDatabaseMetaData#getSchemas()} + * @throws SQLServerException + * @throws SQLException + */ + @Test + public void testDBSchema() throws SQLServerException, SQLException { + DatabaseMetaData databaseMetaData = connection.getMetaData(); + + ResultSet rs = databaseMetaData.getSchemas(); + + while (rs.next()) { + assertTrue(!StringUtils.isEmpty(rs.getString(1)), "Schema Name should not be Empty"); + } + } + + /** + * Get All Tables. + * + * @throws SQLServerException + * @throws SQLException + */ + @Test + public void testDBTables() throws SQLServerException, SQLException { + DatabaseMetaData databaseMetaData = connection.getMetaData(); + + ResultSet rsCatalog = databaseMetaData.getCatalogs(); + + assertTrue(rsCatalog.next(), "We should get atleast one catalog"); + + String[] types = {"TABLE"}; + ResultSet rs = databaseMetaData.getTables(rsCatalog.getString("TABLE_CAT"), null, "%", types); + + while (rs.next()) { + assertTrue(!StringUtils.isEmpty(rs.getString("TABLE_NAME")),"Table Name should not be Empty"); + } + } + + /** + * Testing DB Columns.

    + * We can Improve this test scenario by following way. + *

      + *
    • Create table with appropriate column size, data types,auto increment, NULLABLE etc. + *
    • Then get databasemetatadata.getColumns to see if there is any mismatch. + *
    + * @throws SQLServerException + * @throws SQLException + */ + @Test + public void testGetDBColumn() throws SQLServerException, SQLException { + DatabaseMetaData databaseMetaData = connection.getMetaData(); + String[] types = {"TABLE"}; + ResultSet rs = databaseMetaData.getTables(null, null, "%", types); + + //Fetch one table + assertTrue(rs.next(), "At least one table should be found"); + + //Go through all columns. + ResultSet rs1 = databaseMetaData.getColumns(null, null, rs.getString("TABLE_NAME"), "%"); + + while (rs1.next()) { + assertTrue(!StringUtils.isEmpty(rs1.getString("TABLE_CAT")), "Category Name should not be Empty"); // 1 + assertTrue(!StringUtils.isEmpty(rs1.getString("TABLE_SCHEM")), "SCHEMA Name should not be Empty"); + assertTrue(!StringUtils.isEmpty(rs1.getString("TABLE_NAME")), "Table Name should not be Empty"); + assertTrue(!StringUtils.isEmpty(rs1.getString("COLUMN_NAME")), "COLUMN NAME should not be Empty"); + assertTrue(!StringUtils.isEmpty(rs1.getString("DATA_TYPE")), "Data Type should not be Empty"); + assertTrue(!StringUtils.isEmpty(rs1.getString("TYPE_NAME")), "Data Type Name should not be Empty"); // 6 + assertTrue(!StringUtils.isEmpty(rs1.getString("COLUMN_SIZE")), "Column Size should not be Empty"); // 7 + assertTrue(!StringUtils.isEmpty(rs1.getString("NULLABLE")), "Nullable value should not be Empty"); // 11 + assertTrue(!StringUtils.isEmpty(rs1.getString("IS_NULLABLE")), "Nullable value should not be Empty"); // 18 + assertTrue(!StringUtils.isEmpty(rs1.getString("IS_AUTOINCREMENT")), "Nullable value should not be Empty"); // 22 + } + } + + /** + * We can improve this test case by following manner: + *
      + *
    • We can check if PRIVILEGE is in between CRUD / REFERENCES / SELECT / INSERT etc. + *
    • IS_GRANTABLE can have only 2 values YES / NO + *
    + * @throws SQLServerException + * @throws SQLException + */ + @Test + public void testGetColumnPrivileges() throws SQLServerException, SQLException { + DatabaseMetaData databaseMetaData = connection.getMetaData(); + String[] types = {"TABLE"}; + ResultSet rsTables = databaseMetaData.getTables(null, null, "%", types); + + //Fetch one table + assertTrue(rsTables.next(), "At least one table should be found"); + + //Go through all columns. + ResultSet rs1 = databaseMetaData.getColumnPrivileges(null, null, rsTables.getString("TABLE_NAME"), "%"); + + while(rs1.next()) { + assertTrue(!StringUtils.isEmpty(rs1.getString("TABLE_CAT")),"Category Name should not be Empty"); //1 + assertTrue(!StringUtils.isEmpty(rs1.getString("TABLE_SCHEM")),"SCHEMA Name should not be Empty"); + assertTrue(!StringUtils.isEmpty(rs1.getString("TABLE_NAME")),"Table Name should not be Empty"); + assertTrue(!StringUtils.isEmpty(rs1.getString("COLUMN_NAME")),"COLUMN NAME should not be Empty"); + assertTrue(!StringUtils.isEmpty(rs1.getString("GRANTOR")),"GRANTOR should not be Empty"); + assertTrue(!StringUtils.isEmpty(rs1.getString("GRANTEE")),"GRANTEE should not be Empty"); + assertTrue(!StringUtils.isEmpty(rs1.getString("PRIVILEGE")),"PRIVILEGE should not be Empty"); + assertTrue(!StringUtils.isEmpty(rs1.getString("IS_GRANTABLE")),"IS_GRANTABLE should be YES / NO"); + + } + } + + /** + * TODO: Check JDBC Specs: Can we have any tables/functions without category? + * + * Testing {@link SQLServerDatabaseMetaData#getFunctions(String, String, String)} with sending wrong category. + * @throws SQLServerException + * @throws SQLException + */ + @Test + public void testGetFunctionsWithWrongParams() throws SQLServerException, SQLException { + try { + DatabaseMetaData databaseMetaData = connection.getMetaData(); + databaseMetaData.getFunctions("", null, "xp_%"); + assertTrue(false,"As we are not supplying schema it should fail."); + }catch(Exception ae) { + + } + } + + /** + * Test {@link SQLServerDatabaseMetaData#getFunctions(String, String, String)} + * @throws SQLServerException + * @throws SQLException + */ + @Test + public void testGetFunctions() throws SQLServerException, SQLException { + DatabaseMetaData databaseMetaData = connection.getMetaData(); + ResultSet rs = databaseMetaData.getFunctions(null, null, "xp_%"); + + while(rs.next()) { + assertTrue(!StringUtils.isEmpty(rs.getString("FUNCTION_CAT")),"FUNCTION_CAT should not be NULL"); + assertTrue(!StringUtils.isEmpty(rs.getString("FUNCTION_SCHEM")),"FUNCTION_SCHEM should not be NULL"); + assertTrue(!StringUtils.isEmpty(rs.getString("FUNCTION_NAME")),"FUNCTION_NAME should not be NULL"); + assertTrue(!StringUtils.isEmpty(rs.getString("NUM_INPUT_PARAMS")),"NUM_INPUT_PARAMS should not be NULL"); + assertTrue(!StringUtils.isEmpty(rs.getString("NUM_OUTPUT_PARAMS")),"NUM_OUTPUT_PARAMS should not be NULL"); + assertTrue(!StringUtils.isEmpty(rs.getString("NUM_RESULT_SETS")),"NUM_RESULT_SETS should not be NULL"); + assertTrue(!StringUtils.isEmpty(rs.getString("FUNCTION_TYPE")),"FUNCTION_TYPE should not be NULL"); + } + rs.close(); + } + + /** + * Te + * @throws SQLServerException + * @throws SQLException + */ + @Test + public void testGetFunctionColumns() throws SQLServerException, SQLException{ + DatabaseMetaData databaseMetaData = connection.getMetaData(); + ResultSet rsFunctions = databaseMetaData.getFunctions(null, null, "%"); + + //Fetch one Function + assertTrue(rsFunctions.next(), "At least one function should be found"); + + //Go through all columns. + ResultSet rs = databaseMetaData.getFunctionColumns(null, null, rsFunctions.getString("FUNCTION_NAME"), "%"); + + while(rs.next()) { + assertTrue(!StringUtils.isEmpty(rs.getString("FUNCTION_CAT")),"FUNCTION_CAT should not be NULL"); + assertTrue(!StringUtils.isEmpty(rs.getString("FUNCTION_SCHEM")),"FUNCTION_SCHEM should not be NULL"); + assertTrue(!StringUtils.isEmpty(rs.getString("FUNCTION_NAME")),"FUNCTION_NAME should not be NULL"); + assertTrue(!StringUtils.isEmpty(rs.getString("COLUMN_NAME")),"COLUMN_NAME should not be NULL"); + assertTrue(!StringUtils.isEmpty(rs.getString("COLUMN_TYPE")),"COLUMN_TYPE should not be NULL"); + assertTrue(!StringUtils.isEmpty(rs.getString("DATA_TYPE")),"DATA_TYPE should not be NULL"); + assertTrue(!StringUtils.isEmpty(rs.getString("TYPE_NAME")),"TYPE_NAME should not be NULL"); + assertTrue(!StringUtils.isEmpty(rs.getString("NULLABLE")),"NULLABLE should not be NULL"); //12 + assertTrue(!StringUtils.isEmpty(rs.getString("IS_NULLABLE")),"IS_NULLABLE should not be NULL"); //19 + } + + } + } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/BulkCopyWithSqlVariantTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/BulkCopyWithSqlVariantTest.java new file mode 100644 index 000000000..2c4a41980 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/BulkCopyWithSqlVariantTest.java @@ -0,0 +1,717 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.datatypes; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.math.BigDecimal; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerBulkCopy; +import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.SQLServerResultSet; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; + +/** + * Test Bulkcopy with sql_variant datatype, testing all underlying supported datatypes + * + */ +@RunWith(JUnitPlatform.class) +public class BulkCopyWithSqlVariantTest extends AbstractTest { + + static SQLServerConnection con = null; + static Statement stmt = null; + static String tableName = "sqlVariantTestSrcTable"; + static String destTableName = "sqlVariantDestTable"; + static SQLServerResultSet rs = null; + + /** + * Test integer value + * + * @throws SQLException + */ + @Test + public void bulkCopyTestInt() throws SQLException { + int col1Value = 5; + beforeEachSetup("int", col1Value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals(rs.getInt(1), 5); + } + } + + /** + * Test smallInt value + * + * @throws SQLException + */ + @Test + public void bulkCopyTestSmallInt() throws SQLException { + int col1Value = 5; + beforeEachSetup("smallint", col1Value); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals(rs.getShort(1), 5); + } + bulkCopy.close(); + } + + /** + * Test tinyInt value + * + * @throws SQLException + */ + @Test + public void bulkCopyTestTinyint() throws SQLException { + int col1Value = 5; + beforeEachSetup("tinyint", col1Value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals(rs.getByte(1), 5); + } + bulkCopy.close(); + } + + /** + * test Bigint value + * + * @throws SQLException + */ + @Test + public void bulkCopyTestBigint() throws SQLException { + int col1Value = 5; + beforeEachSetup("bigint", col1Value); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals(rs.getLong(1), col1Value); + } + } + + /** + * test float value + * + * @throws SQLException + */ + @Test + public void bulkCopyTestFloat() throws SQLException { + int col1Value = 5; + beforeEachSetup("float", col1Value); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals(rs.getDouble(1), col1Value); + } + } + + /** + * test real value + * + * @throws SQLException + */ + @Test + public void bulkCopyTestReal() throws SQLException { + int col1Value = 5; + beforeEachSetup("real", col1Value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals(rs.getFloat(1), col1Value); + } + + } + + /** + * test money value + * + * @throws SQLException + */ + @Test + public void bulkCopyTestMoney() throws SQLException { + String col1Value = "126.1230"; + beforeEachSetup("money", col1Value); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals(rs.getMoney(1), new BigDecimal(col1Value)); + } + + } + + /** + * test smallmoney + * + * @throws SQLException + */ + @Test + public void bulkCopyTestSmallmoney() throws SQLException { + String col1Value = "126.1230"; + String destTableName = "dest_sqlVariant"; + Utils.dropTableIfExists(tableName, stmt); + Utils.dropTableIfExists(destTableName, stmt); + stmt.executeUpdate("create table " + tableName + " (col1 sql_variant)"); + stmt.executeUpdate("INSERT into " + tableName + "(col1) values (CAST (" + col1Value + " AS " + "smallmoney" + ") )"); + stmt.executeUpdate("create table " + destTableName + " (col1 sql_variant)"); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals(rs.getSmallMoney(1), new BigDecimal(col1Value)); + } + + } + + /** + * test date value + * + * @throws SQLException + */ + @Test + public void bulkCopyTestDate() throws SQLException { + String col1Value = "2015-05-05"; + beforeEachSetup("date", "'" + col1Value + "'"); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals("" + rs.getDate(1), col1Value); + } + + } + + /** + * Test bulkcoping two column with sql_variant datatype + * + * @throws SQLException + */ + @Test + public void bulkCopyTestTwoCols() throws SQLException { + String col1Value = "2015-05-05"; + String col2Value = "126.1230"; + String destTableName = "dest_sqlVariant"; + Utils.dropTableIfExists(tableName, stmt); + Utils.dropTableIfExists(destTableName, stmt); + stmt.executeUpdate("create table " + tableName + " (col1 sql_variant, col2 sql_variant)"); + stmt.executeUpdate("INSERT into " + tableName + "(col1, col2) values (CAST ('" + col1Value + "' AS " + "date" + ")" + ",CAST (" + col2Value + + " AS " + "smallmoney" + ") )"); + stmt.executeUpdate("create table " + destTableName + " (col1 sql_variant, col2 sql_variant)"); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals("" + rs.getDate(1), col1Value); + assertEquals(rs.getSmallMoney(2), new BigDecimal(col2Value)); + } + + } + + /** + * test time with scale value + * + * @throws SQLException + */ + @Test + public void bulkCopyTestTimeWithScale() throws SQLException { + String col1Value = "'12:26:27.1452367'"; + beforeEachSetup("time(2)", col1Value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals("" + rs.getString(1), "12:26:27.15"); // getTime does not work + } + + } + + /** + * test char value + * + * @throws SQLException + */ + @Test + public void bulkCopyTestChar() throws SQLException { + String col1Value = "'sample'"; + + beforeEachSetup("char", col1Value); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals("'" + rs.getString(1).trim() + "'", col1Value); // adds space between + } + + } + + /** + * test nchar value + * + * @throws SQLException + */ + @Test + public void bulkCopyTestNchar() throws SQLException { + String col1Value = "'a'"; + + beforeEachSetup("nchar", col1Value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals("'" + rs.getNString(1).trim() + "'", col1Value); + } + + } + + /** + * test varchar value + * + * @throws SQLException + */ + @Test + public void bulkCopyTestVarchar() throws SQLException { + String col1Value = "'hello'"; + + beforeEachSetup("varchar", col1Value); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals("'" + rs.getString(1).trim() + "'", col1Value); + } + + } + + /** + * test nvarchar value + * + * @throws SQLException + */ + @Test + public void bulkCopyTestNvarchar() throws SQLException { + String col1Value = "'hello'"; + beforeEachSetup("nvarchar", col1Value); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals("'" + rs.getString(1).trim() + "'", col1Value); + } + + } + + /** + * test Binary value + * + * @throws SQLException + */ + @Test + public void bulkCopyTestBinary20() throws SQLException { + String col1Value = "hello"; + beforeEachSetup("binary(20)", "'" + col1Value + "'"); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertTrue(Utils.parseByte(rs.getBytes(1), col1Value.getBytes())); + } + } + + /** + * test varbinary value + * + * @throws SQLException + */ + @Test + public void bulkCopyTestVarbinary20() throws SQLException { + String col1Value = "hello"; + + beforeEachSetup("varbinary(20)", "'" + col1Value + "'"); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertTrue(Utils.parseByte(rs.getBytes(1), col1Value.getBytes())); + } + } + + /** + * test varbinary8000 + * + * @throws SQLException + */ + @Test + public void bulkCopyTestVarbinary8000() throws SQLException { + String col1Value = "hello"; + beforeEachSetup("binary(8000)", "'" + col1Value + "'"); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertTrue(Utils.parseByte(rs.getBytes(1), col1Value.getBytes())); + } + } + + /** + * test null value for underlying bit data type + * + * @throws SQLException + */ + @Test // TODO: check bitnull + public void bulkCopyTestBitNull() throws SQLException { + beforeEachSetup("bit", null); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals(rs.getBoolean(1), false); + } + } + + /** + * test bit value + * + * @throws SQLException + */ + @Test + public void bulkCopyTestBit() throws SQLException { + int col1Value = 5000; + beforeEachSetup("bit", col1Value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals(rs.getBoolean(1), true); + } + } + + /** + * test datetime value + * + * @throws SQLException + */ + @Test + public void bulkCopyTestDatetime() throws SQLException { + String col1Value = "2015-05-08 12:26:24.0"; + beforeEachSetup("datetime", "'" + col1Value + "'"); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals("" + rs.getDateTime(1), col1Value); + + } + + } + + /** + * test smalldatetime + * + * @throws SQLException + */ + @Test + public void bulkCopyTestSmalldatetime() throws SQLException { + String col1Value = "2015-05-08 12:26:24"; + beforeEachSetup("smalldatetime", "'" + col1Value + "'"); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals("" + rs.getSmallDateTime(1), "2015-05-08 12:26:00.0"); + } + + } + + /** + * test datetime2 + * + * @throws SQLException + */ + @Test + public void bulkCopyTestDatetime2() throws SQLException { + String col1Value = "2015-05-08 12:26:24.12645"; + beforeEachSetup("datetime2(2)", "'" + col1Value + "'"); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals("" + rs.getTimestamp(1), "2015-05-08 12:26:24.13"); + } + + } + + /** + * test time + * + * @throws SQLException + */ + @Test + public void bulkCopyTestTime() throws SQLException { + String col1Value = "'12:26:27.1452367'"; + String destTableName = "dest_sqlVariant"; + Utils.dropTableIfExists(tableName, stmt); + Utils.dropTableIfExists(destTableName, stmt); + stmt.executeUpdate("create table " + tableName + " (col1 sql_variant)"); + stmt.executeUpdate("INSERT into " + tableName + "(col1) values (CAST (" + col1Value + " AS " + "time(2)" + ") )"); + stmt.executeUpdate("create table " + destTableName + " (col1 sql_variant)"); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + rs.next(); + assertEquals("" + rs.getObject(1).toString(), "12:26:27"); + } + + /** + * Read GUID stored in SqlVariant + * + * @throws SQLException + */ + @Test + public void bulkCopyTestReadGUID() throws SQLException { + String col1Value = "1AE740A2-2272-4B0F-8086-3DDAC595BC11"; + beforeEachSetup("uniqueidentifier", "'" + col1Value + "'"); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals("" + rs.getUniqueIdentifier(1), col1Value); + + } + } + + /** + * Read VarChar8000 from SqlVariant + * + * @throws SQLException + */ + @Test + public void bulkCopyTestVarChar8000() throws SQLException { + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < 8000; i++) { + buffer.append("a"); + } + String col1Value = buffer.toString(); + beforeEachSetup("varchar(8000)", "'" + col1Value + "'"); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + bulkCopy.setDestinationTableName(destTableName); + bulkCopy.writeToServer(rs); + bulkCopy.close(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTableName); + while (rs.next()) { + assertEquals(rs.getString(1), col1Value); + } + } + + private void beforeEachSetup(String colType, + Object colValue) throws SQLException { + Utils.dropTableIfExists(tableName, stmt); + Utils.dropTableIfExists(destTableName, stmt); + stmt.executeUpdate("create table " + tableName + " (col1 sql_variant)"); + stmt.executeUpdate("INSERT into " + tableName + "(col1) values (CAST (" + colValue + " AS " + colType + ") )"); + stmt.executeUpdate("create table " + destTableName + " (col1 sql_variant)"); + } + + /** + * Prepare test + * + * @throws SQLException + * @throws SecurityException + * @throws IOException + */ + @BeforeAll + public static void setupHere() throws SQLException, SecurityException, IOException { + con = (SQLServerConnection) DriverManager.getConnection(connectionString); + stmt = con.createStatement(); + } + + /** + * drop the tables + * + * @throws SQLException + */ + @AfterAll + public static void afterAll() throws SQLException { + Utils.dropTableIfExists(tableName, stmt); + Utils.dropTableIfExists(destTableName, stmt); + + if (null != stmt) { + stmt.close(); + } + + if (null != rs) { + rs.close(); + } + + if (null != con) { + con.close(); + } + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/SQLVariantResultSetTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/SQLVariantResultSetTest.java new file mode 100644 index 000000000..5d4740305 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/SQLVariantResultSetTest.java @@ -0,0 +1,921 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.datatypes; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.math.BigDecimal; +import java.sql.CallableStatement; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.jdbc.SQLServerResultSet; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; +import com.microsoft.sqlserver.testframework.util.RandomData; + +/** + * Tests for supporting sqlVariant + * + */ +@RunWith(JUnitPlatform.class) +public class SQLVariantResultSetTest extends AbstractTest { + + static SQLServerConnection con = null; + static Statement stmt = null; + static String tableName = "sqlVariantTestSrcTable"; + static String inputProc = "sqlVariantProc"; + static SQLServerResultSet rs = null; + static SQLServerPreparedStatement pstmt = null; + + /** + * Read int value + * + * @throws SQLException + * @throws SecurityException + * @throws IOException + */ + @Test + public void readInt() throws SQLException, SecurityException, IOException { + int value = 2; + createAndPopulateTable("int", value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getString(1), "" + value); + } + + /** + * Read money type stored in SqlVariant + * + * @throws SQLException + * + */ + @Test + public void readMoney() throws SQLException { + Double value = 123.12; + createAndPopulateTable("Money", value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getObject(1), new BigDecimal("123.1200")); + } + + /** + * Reading smallmoney from SqlVariant + * + * @throws SQLException + */ + @Test + public void readSmallMoney() throws SQLException { + Double value = 123.12; + createAndPopulateTable("smallmoney", value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getObject(1), new BigDecimal("123.1200")); + } + + /** + * Read GUID stored in SqlVariant + * + * @throws SQLException + */ + @Test + public void readGUID() throws SQLException { + String value = "1AE740A2-2272-4B0F-8086-3DDAC595BC11"; + createAndPopulateTable("uniqueidentifier", "'" + value + "'"); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getUniqueIdentifier(1), value); + } + + /** + * Reading date stored in SqlVariant + * + * @throws SQLException + */ + @Test + public void readDate() throws SQLException { + String value = "'2015-05-08'"; + createAndPopulateTable("date", value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals("" + rs.getObject(1), "2015-05-08"); + } + + /** + * Read time from SqlVariant + * + * @throws SQLException + */ + @Test + public void readTime() throws SQLException { + String value = "'12:26:27.123345'"; + createAndPopulateTable("time(3)", value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals("" + rs.getObject(1).toString(), "12:26:27"); + } + + /** + * Read datetime from SqlVariant + * + * @throws SQLException + */ + @Test + public void readDateTime() throws SQLException { + String value = "'2015-05-08 12:26:24'"; + createAndPopulateTable("datetime", value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals("" + rs.getObject(1), "2015-05-08 12:26:24.0"); + } + + /** + * Read smalldatetime from SqlVariant + * + * @throws SQLException + */ + @Test + public void readSmallDateTime() throws SQLException { + String value = "'2015-05-08 12:26:24'"; + createAndPopulateTable("smalldatetime", value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals("" + rs.getObject(1), "2015-05-08 12:26:00.0"); + } + + /** + * Read VarChar8000 from SqlVariant + * + * @throws SQLException + */ + @Test + public void readVarChar8000() throws SQLException { + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < 8000; i++) { + buffer.append("a"); + } + String value = "'" + buffer.toString() + "'"; + createAndPopulateTable("VARCHAR(8000)", value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getObject(1), buffer.toString()); + } + + /** + * Read float from SqlVariant + * + * @throws SQLException + */ + @Test + public void readFloat() throws SQLException { + float value = 5; + createAndPopulateTable("float", value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getObject(1), Double.valueOf("5.0")); + } + + /** + * Read bigint from SqlVariant + * + * @throws SQLException + */ + @Test + public void readBigInt() throws SQLException { + long value = 5; + createAndPopulateTable("bigint", value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getObject(1), value); + } + + /** + * read smallint from SqlVariant + * + * @throws SQLException + */ + @Test + public void readSmallInt() throws SQLException { + short value = 5; + createAndPopulateTable("smallint", value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getObject(1), value); + } + + /** + * Read tinyint from SqlVariant + * + * @throws SQLException + */ + @Test + public void readTinyInt() throws SQLException { + short value = 5; + createAndPopulateTable("tinyint", value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getObject(1), value); + } + + /** + * read bit from SqlVariant + * + * @throws SQLException + */ + @Test + public void readBit() throws SQLException { + int value = 50000; + createAndPopulateTable("bit", value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getObject(1), true); + } + + /** + * Read float from SqlVariant + * + * @throws SQLException + */ + @Test + public void readReal() throws SQLException { + float value = 5; + createAndPopulateTable("Real", value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getObject(1), Float.valueOf("5.0")); + } + + /** + * Read nchar from SqlVariant + * + * @throws SQLException + */ + @Test + public void readNChar() throws SQLException, SecurityException, IOException { + String value = "a"; + createAndPopulateTable("nchar(5)", "'" + value + "'"); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getNString(1).trim(), value); + } + + /** + * Read nVarChar + * + * @throws SQLException + * @throws SecurityException + * @throws IOException + */ + @Test + public void readNVarChar() throws SQLException, SecurityException, IOException { + String value = "nvarchar"; + createAndPopulateTable("nvarchar(10)", "'" + value + "'"); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getObject(1), value); + } + + /** + * readBinary + * + * @throws SQLException + * @throws SecurityException + * @throws IOException + */ + @Test + public void readBinary20() throws SQLException, SecurityException, IOException { + String value = "hi"; + createAndPopulateTable("binary(20)", "'" + value + "'"); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertTrue(parseByte((byte[]) rs.getObject(1), (byte[]) value.getBytes())); + } + + /** + * read varBinary + * + * @throws SQLException + * @throws SecurityException + * @throws IOException + */ + @Test + public void readVarBinary20() throws SQLException, SecurityException, IOException { + String value = "hi"; + createAndPopulateTable("varbinary(20)", "'" + value + "'"); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertTrue(parseByte((byte[]) rs.getObject(1), (byte[]) value.getBytes())); + } + + /** + * read Binary512 + * + * @throws SQLException + * @throws SecurityException + * @throws IOException + */ + @Test + public void readBinary512() throws SQLException, SecurityException, IOException { + String value = "hi"; + createAndPopulateTable("binary(512)", "'" + value + "'"); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertTrue(parseByte((byte[]) rs.getObject(1), (byte[]) value.getBytes())); + } + + /** + * read Binary(8000) + * + * @throws SQLException + * @throws SecurityException + * @throws IOException + */ + @Test + public void readBinary8000() throws SQLException, SecurityException, IOException { + String value = "hi"; + createAndPopulateTable("binary(8000)", "'" + value + "'"); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertTrue(parseByte((byte[]) rs.getObject(1), (byte[]) value.getBytes())); + } + + /** + * read varBinary(8000) + * + * @throws SQLException + * @throws SecurityException + * @throws IOException + */ + @Test + public void readvarBinary8000() throws SQLException, SecurityException, IOException { + String value = "hi"; + createAndPopulateTable("varbinary(8000)", "'" + value + "'"); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertTrue(parseByte((byte[]) rs.getObject(1), (byte[]) value.getBytes())); + } + + /** + * Read SqlVariantProperty + * + * @throws SQLException + * @throws SecurityException + * @throws IOException + */ + @Test + public void readSQLVariantProperty() throws SQLException, SecurityException, IOException { + String value = "hi"; + createAndPopulateTable("binary(8000)", "'" + value + "'"); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT SQL_VARIANT_PROPERTY(col1,'BaseType') AS 'Base Type'," + + " SQL_VARIANT_PROPERTY(col1,'Precision') AS 'Precision' from " + tableName); + rs.next(); + assertTrue(rs.getString(1).equalsIgnoreCase("binary"), "unexpected baseType, expected: binary, retrieved:" + rs.getString(1)); + } + + /** + * Testing that inserting value more than 8000 on varchar(8000) should throw failure operand type clash + * + * @throws SQLException + */ + @Test + public void insertVarChar8001() throws SQLException { + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < 8001; i++) { + buffer.append("a"); + } + Utils.dropTableIfExists(tableName, stmt); + stmt.executeUpdate("create table " + tableName + " (col1 sql_variant)"); + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into " + tableName + " values (?)"); + pstmt.setObject(1, buffer.toString()); + try { + pstmt.execute(); + } + catch (SQLServerException e) { + assertTrue(e.toString().contains("com.microsoft.sqlserver.jdbc.SQLServerException: Operand type clash")); + } + } + + /** + * Testing nvarchar4000 + * + * @throws SQLException + */ + @Test + public void readNvarChar4000() throws SQLException { + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < 4000; i++) { + buffer.append("a"); + } + String value = "'" + buffer.toString() + "'"; + createAndPopulateTable("NVARCHAR(4000)", value); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getObject(1), buffer.toString()); + } + + /** + * Update int value + * + * @throws SQLException + * @throws SecurityException + * @throws IOException + */ + @Test + public void UpdateInt() throws SQLException, SecurityException, IOException { + int value = 2; + int updatedValue = 3; + createAndPopulateTable("int", value); + stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getString(1), "" + value); + rs.updateInt(1, updatedValue); + rs.updateRow(); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getString(1), "" + updatedValue); + if (null != rs) { + rs.close(); + } + } + + /** + * Update nChar value + * + * @throws SQLException + * @throws SecurityException + * @throws IOException + */ + @Test + public void UpdateNChar() throws SQLException, SecurityException, IOException { + String value = "a"; + String updatedValue = "b"; + + createAndPopulateTable("nchar", "'" + value + "'"); + stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getString(1).trim(), "" + value); + rs.updateNString(1, updatedValue); + rs.updateRow(); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getString(1), "" + updatedValue); + if (null != rs) { + rs.close(); + } + } + + /** + * update Binary + * + * @throws SQLException + * @throws SecurityException + * @throws IOException + */ + @Test + public void updateBinary20() throws SQLException, SecurityException, IOException { + String value = "hi"; + String updatedValue = "bye"; + createAndPopulateTable("binary(20)", "'" + value + "'"); + stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertTrue(parseByte((byte[]) rs.getObject(1), (byte[]) value.getBytes())); + rs.updateBytes(1, updatedValue.getBytes()); + rs.updateRow(); + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertTrue(parseByte((byte[]) rs.getBytes(1), updatedValue.getBytes())); + if (null != rs) { + rs.close(); + } + } + + /** + * Testing inserting and reading from SqlVariant and int column + * + * @throws SQLException + */ + @Test + public void insertTest() throws SQLException { + Utils.dropTableIfExists(tableName, stmt); + stmt.executeUpdate("create table " + tableName + " (col1 sql_variant, col2 int)"); + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into " + tableName + " values (?, ?)"); + + String[] col1Value = {"Hello", null}; + int[] col2Value = {1, 2}; + pstmt.setObject(1, "Hello"); + pstmt.setInt(2, 1); + pstmt.execute(); + pstmt.setObject(1, null); + pstmt.setInt(2, 2); + pstmt.execute(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + int i = 0; + rs.next(); + do { + assertEquals(rs.getObject(1), col1Value[i]); + assertEquals(rs.getObject(2), col2Value[i]); + i++; + } + while (rs.next()); + } + + /** + * test inserting null value + * + * @throws SQLException + */ + @Test + public void insertTestNull() throws SQLException { + Utils.dropTableIfExists(tableName, stmt); + stmt.executeUpdate("create table " + tableName + " (col1 sql_variant)"); + pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into " + tableName + " values ( ?)"); + + pstmt.setObject(1, null); + pstmt.execute(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getBoolean(1), false); + } + + /** + * Test inserting using setObject + * + * @throws SQLException + * @throws ParseException + */ + @Test + public void insertSetObject() throws SQLException { + Utils.dropTableIfExists(tableName, stmt); + stmt.executeUpdate("create table " + tableName + " (col1 sql_variant)"); + pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into " + tableName + " values (?)"); + + pstmt.setObject(1, 2); + pstmt.execute(); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getObject(1), 2); + } + + /** + * Test callableStatement with SqlVariant + * + * @throws SQLException + */ + @Test + public void callableStatementOutputIntTest() throws SQLException { + int value = 5; + Utils.dropTableIfExists(tableName, stmt); + stmt.executeUpdate("create table " + tableName + " (col1 sql_variant)"); + stmt.executeUpdate("INSERT into " + tableName + " values (CAST (" + value + " AS " + "int" + "))"); + + Utils.dropProcedureIfExists(inputProc, stmt); + String sql = "CREATE PROCEDURE " + inputProc + " @p0 sql_variant OUTPUT AS SELECT TOP 1 @p0=col1 FROM " + tableName; + stmt.execute(sql); + + CallableStatement cs = con.prepareCall(" {call " + inputProc + " (?) }"); + cs.registerOutParameter(1, microsoft.sql.Types.SQL_VARIANT); + cs.execute(); + assertEquals(cs.getString(1), String.valueOf(value)); + if (null != cs) { + cs.close(); + } + } + + /** + * Test callableStatement with SqlVariant + * + * @throws SQLException + */ + @Test + public void callableStatementOutputDateTest() throws SQLException { + String value = "2015-05-08"; + + Utils.dropTableIfExists(tableName, stmt); + stmt.executeUpdate("create table " + tableName + " (col1 sql_variant)"); + stmt.executeUpdate("INSERT into " + tableName + " values (CAST ('" + value + "' AS " + "date" + "))"); + + Utils.dropProcedureIfExists(inputProc, stmt); + String sql = "CREATE PROCEDURE " + inputProc + " @p0 sql_variant OUTPUT AS SELECT TOP 1 @p0=col1 FROM " + tableName; + stmt.execute(sql); + + CallableStatement cs = con.prepareCall(" {call " + inputProc + " (?) }"); + cs.registerOutParameter(1, microsoft.sql.Types.SQL_VARIANT); + cs.execute(); + assertEquals(cs.getString(1), String.valueOf(value)); + if (null != cs) { + cs.close(); + } + } + + /** + * Test callableStatement with SqlVariant + * + * @throws SQLException + */ + @Test + public void callableStatementOutputTimeTest() throws SQLException { + String value = "12:26:27.123345"; + String returnValue = "12:26:27"; + Utils.dropTableIfExists(tableName, stmt); + stmt.executeUpdate("create table " + tableName + " (col1 sql_variant)"); + stmt.executeUpdate("INSERT into " + tableName + " values (CAST ('" + value + "' AS " + "time(3)" + "))"); + + Utils.dropProcedureIfExists(inputProc, stmt); + String sql = "CREATE PROCEDURE " + inputProc + " @p0 sql_variant OUTPUT AS SELECT TOP 1 @p0=col1 FROM " + tableName; + stmt.execute(sql); + + CallableStatement cs = con.prepareCall(" {call " + inputProc + " (?) }"); + cs.registerOutParameter(1, microsoft.sql.Types.SQL_VARIANT, 3); + cs.execute(); + assertEquals(String.valueOf(returnValue), "" + cs.getObject(1)); + if (null != cs) { + cs.close(); + } + } + + /** + * Test callableStatement with SqlVariant Binary value + * + * @throws SQLException + */ + @Test + public void callableStatementOutputBinaryTest() throws SQLException { + byte[] binary20 = RandomData.generateBinaryTypes("20", false, false); + byte[] secondBinary20 = RandomData.generateBinaryTypes("20", false, false); + Utils.dropTableIfExists(tableName, stmt); + stmt.executeUpdate("create table " + tableName + " (col1 sql_variant, col2 sql_variant)"); + pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into " + tableName + " values (?,?)"); + pstmt.setObject(1, binary20); + pstmt.setObject(2, secondBinary20); + pstmt.execute(); + Utils.dropProcedureIfExists(inputProc, stmt); + String sql = "CREATE PROCEDURE " + inputProc + " @p0 sql_variant OUTPUT, @p1 sql_variant" + " AS" + " SELECT top 1 @p0=col1 FROM " + tableName + + " where col2=@p1 "; + stmt.execute(sql); + + CallableStatement cs = con.prepareCall(" {call " + inputProc + " (?,?) }"); + cs.registerOutParameter(1, microsoft.sql.Types.SQL_VARIANT); + cs.setObject(2, secondBinary20, microsoft.sql.Types.SQL_VARIANT); + + cs.execute(); + assertTrue(parseByte((byte[]) cs.getBytes(1), binary20)); + if (null != cs) { + cs.close(); + } + } + + /** + * Test stored procedure with input and output params + * + * @throws SQLException + */ + @Test + public void callableStatementInputOutputIntTest() throws SQLException { + int col1Value = 5; + int col2Value = 2; + Utils.dropTableIfExists(tableName, stmt); + stmt.executeUpdate("create table " + tableName + " (col1 sql_variant, col2 int)"); + stmt.executeUpdate("INSERT into " + tableName + "(col1, col2) values (CAST (" + col1Value + " AS " + "int" + "), " + col2Value + ")"); + Utils.dropProcedureIfExists(inputProc, stmt); + String sql = "CREATE PROCEDURE " + inputProc + " @p0 sql_variant OUTPUT, @p1 sql_variant" + " AS" + " SELECT top 1 @p0=col1 FROM " + tableName + + " where col2=@p1"; + stmt.execute(sql); + CallableStatement cs = con.prepareCall(" {call " + inputProc + " (?,?) }"); + + cs.registerOutParameter(1, microsoft.sql.Types.SQL_VARIANT); + cs.setObject(2, col2Value, microsoft.sql.Types.SQL_VARIANT); + cs.execute(); + assertEquals(cs.getObject(1), col1Value); + if (null != cs) { + cs.close(); + } + } + + /** + * Test stored procedure with input and output and return value + * + * @throws SQLException + */ + @Test + public void callableStatementInputOutputReturnIntTest() throws SQLException { + int col1Value = 5; + int col2Value = 2; + int returnValue = 12; + Utils.dropTableIfExists(tableName, stmt); + stmt.executeUpdate("create table " + tableName + " (col1 sql_variant, col2 int)"); + stmt.executeUpdate("INSERT into " + tableName + "(col1, col2) values (CAST (" + col1Value + " AS " + "int" + "), " + col2Value + ")"); + Utils.dropProcedureIfExists(inputProc, stmt); + String sql = "CREATE PROCEDURE " + inputProc + " @p0 sql_variant OUTPUT, @p1 sql_variant" + " AS" + " SELECT top 1 @p0=col1 FROM " + tableName + + " where col2=@p1" + " return " + returnValue; + stmt.execute(sql); + CallableStatement cs = con.prepareCall(" {? = call " + inputProc + " (?,?) }"); + + cs.registerOutParameter(1, microsoft.sql.Types.SQL_VARIANT); + cs.registerOutParameter(2, microsoft.sql.Types.SQL_VARIANT); + cs.setObject(3, col2Value, microsoft.sql.Types.SQL_VARIANT); + cs.execute(); + assertEquals(cs.getString(1), String.valueOf(returnValue)); + assertEquals(cs.getString(2), String.valueOf(col1Value)); + if (null != cs) { + cs.close(); + } + } + + /** + * test input output procedure + * + * @throws SQLException + */ + @Test + public void callableStatementInputOutputReturnStringTest() throws SQLException { + String col1Value = "aa"; + String col2Value = "bb"; + int returnValue = 12; + + Utils.dropTableIfExists(tableName, stmt); + stmt.executeUpdate("create table " + tableName + " (col1 sql_variant, col2 sql_variant)"); + stmt.executeUpdate("INSERT into " + tableName + "(col1,col2) values" + " (CAST ('" + col1Value + "' AS " + "varchar(5)" + ")" + " ,CAST ('" + + col2Value + "' AS " + "varchar(5)" + ")" + ")"); + Utils.dropProcedureIfExists(inputProc, stmt); + String sql = "CREATE PROCEDURE " + inputProc + " @p0 sql_variant OUTPUT, @p1 sql_variant" + " AS" + " SELECT top 1 @p0=col1 FROM " + tableName + + " where col2=@p1 " + " return " + returnValue; + stmt.execute(sql); + CallableStatement cs = con.prepareCall(" {? = call " + inputProc + " (?,?) }"); + cs.registerOutParameter(1, java.sql.Types.INTEGER); + cs.registerOutParameter(2, microsoft.sql.Types.SQL_VARIANT); + cs.setObject(3, col2Value, microsoft.sql.Types.SQL_VARIANT); + + cs.execute(); + assertEquals(returnValue, cs.getObject(1)); + assertEquals(cs.getObject(2), col1Value); + if (null != cs) { + cs.close(); + } + } + + /** + * Read several rows from SqlVariant + * + * @throws SQLException + */ + @Test + public void readSeveralRows() throws SQLException { + short value1 = 5; + int value2 = 10; + String value3 = "hi"; + Utils.dropTableIfExists(tableName, stmt); + stmt.executeUpdate("create table " + tableName + " (col1 sql_variant, col2 sql_variant, col3 sql_variant)"); + stmt.executeUpdate("INSERT into " + tableName + " values (CAST (" + value1 + " AS " + "tinyint" + ")" + ",CAST (" + value2 + " AS " + "int" + + ")" + ",CAST ('" + value3 + "' AS " + "char(2)" + ")" + ")"); + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + tableName); + rs.next(); + assertEquals(rs.getObject(1), value1); + assertEquals(rs.getObject(2), value2); + assertEquals(rs.getObject(3), value3); + if (null != rs) { + rs.close(); + } + + } + + /** + * Test retrieving values with varchar and integer as basetype + * @throws SQLException + */ + @Test + public void readVarcharInteger() throws SQLException { + Object expected[] = {"abc", 42}; + int index = 0; + rs = (SQLServerResultSet) stmt.executeQuery("SELECT cast('abc' as sql_variant) UNION ALL SELECT cast(42 as sql_variant)"); + while (rs.next()) { + assertEquals(rs.getObject(1), expected[index++]); + } + } + + /** + * Tests unsupported type + * + * @throws SQLException + */ + @Test + public void testUnsupportedDatatype() throws SQLException { + rs = (SQLServerResultSet) stmt.executeQuery("select cast(cast('2017-08-16 17:31:09.995 +07:00' as datetimeoffset) as sql_variant)"); + rs.next(); + try { + rs.getObject(1); + fail("Should have thrown unssuported tds type exception"); + } + catch (Exception e) { + assertTrue(e.getMessage().equalsIgnoreCase("Unexpected TDS type DATETIMEOFFSETN in SQL_VARIANT.")); + } + if (null != rs) { + rs.close(); + } + } + + /** + * Tests that the returning class of base type time in sql_variant is correct. + * + * @throws SQLException + * + */ + @Test + public void testTimeClassAsSqlVariant() throws SQLException { + rs = (SQLServerResultSet) stmt.executeQuery("select cast(cast('17:31:09.995' as time(3)) as sql_variant)"); + rs.next(); + Object object = rs.getObject(1); + assertEquals(object.getClass(), java.sql.Time.class); + ; + } + + private boolean parseByte(byte[] expectedData, + byte[] retrieved) { + assertTrue(Arrays.equals(expectedData, Arrays.copyOf(retrieved, expectedData.length)), " unexpected BINARY value, expected"); + for (int i = expectedData.length; i < retrieved.length; i++) { + assertTrue(0 == retrieved[i], "unexpected data BINARY"); + } + return true; + } + + /** + * Create and populate table + * + * @param columnType + * @param value + * @throws SQLException + */ + private void createAndPopulateTable(String columnType, + Object value) throws SQLException { + Utils.dropTableIfExists(tableName, stmt); + stmt.executeUpdate("create table " + tableName + " (col1 sql_variant)"); + stmt.executeUpdate("INSERT into " + tableName + " values (CAST (" + value + " AS " + columnType + "))"); + } + + /** + * Prepare test + * + * @throws SQLException + * @throws SecurityException + * @throws IOException + */ + @BeforeAll + public static void setupHere() throws SQLException, SecurityException, IOException { + con = (SQLServerConnection) DriverManager.getConnection(connectionString); + stmt = con.createStatement(); + } + + /** + * drop the tables + * + * @throws SQLException + */ + @AfterAll + public static void afterAll() throws SQLException { + Utils.dropProcedureIfExists(inputProc, stmt); + Utils.dropTableIfExists(tableName, stmt); + + if (null != stmt) { + stmt.close(); + } + + if (null != pstmt) { + pstmt.close(); + } + + if (null != rs) { + rs.close(); + } + + if (null != con) { + con.close(); + } + } + +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/TVPWithSqlVariantTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/TVPWithSqlVariantTest.java new file mode 100644 index 000000000..2e1fe11f6 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/TVPWithSqlVariantTest.java @@ -0,0 +1,513 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.datatypes; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.math.BigDecimal; +import java.sql.Date; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Random; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerCallableStatement; +import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.SQLServerDataTable; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.jdbc.SQLServerResultSet; +import com.microsoft.sqlserver.jdbc.SQLServerStatement; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; +import com.microsoft.sqlserver.testframework.sqlType.SqlDate; +import com.microsoft.sqlserver.testframework.util.RandomData; + +@RunWith(JUnitPlatform.class) +public class TVPWithSqlVariantTest extends AbstractTest { + + private static SQLServerConnection conn = null; + static SQLServerStatement stmt = null; + static SQLServerResultSet rs = null; + static SQLServerDataTable tvp = null; + private static String tvpName = "numericTVP"; + private static String destTable = "destTvpSqlVariantTable"; + private static String procedureName = "procedureThatCallsTVP"; + static SQLServerPreparedStatement pstmt = null; + + /** + * Test a previous failure regarding to numeric precision. Issue #211 + * + * @throws SQLServerException + */ + @Test + public void testInt() throws SQLServerException { + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.SQL_VARIANT); + tvp.addRow(12); + pstmt = (SQLServerPreparedStatement) connection.prepareStatement("INSERT INTO " + destTable + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + pstmt.execute(); + if (null != pstmt) { + pstmt.close(); + } + + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTable); + while (rs.next()) { + assertEquals(rs.getInt(1), 12); + assertEquals(rs.getString(1), "" + 12); + assertEquals(rs.getObject(1), 12); + } + } + + /** + * Test with date value + * + * @throws SQLServerException + */ + @Test + public void testDate() throws SQLServerException { + SqlDate sqlDate = new SqlDate(); + Date date = (Date) sqlDate.createdata(); + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.SQL_VARIANT); + tvp.addRow(date); + pstmt = (SQLServerPreparedStatement) connection.prepareStatement("INSERT INTO " + destTable + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + pstmt.execute(); + if (null != pstmt) { + pstmt.close(); + } + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTable); + while (rs.next()) { + assertEquals(rs.getString(1), "" + date); // TODO: GetDate has issues + } + } + + /** + * Test with money value + * + * @throws SQLServerException + */ + @Test + public void testMoney() throws SQLServerException { + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.SQL_VARIANT); + String[] numeric = createNumericValues(); + tvp.addRow(new BigDecimal(numeric[14])); + pstmt = (SQLServerPreparedStatement) connection.prepareStatement("INSERT INTO " + destTable + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + pstmt.execute(); + if (null != pstmt) { + pstmt.close(); + } + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTable); + while (rs.next()) { + assertEquals(rs.getMoney(1), new BigDecimal(numeric[14])); + } + } + + /** + * Test with small int value + * + * @throws SQLServerException + */ + @Test + public void testSmallInt() throws SQLServerException { + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.SQL_VARIANT); + String[] numeric = createNumericValues(); + tvp.addRow(Short.valueOf(numeric[2])); + pstmt = (SQLServerPreparedStatement) connection.prepareStatement("INSERT INTO " + destTable + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + pstmt.execute(); + + if (null != pstmt) { + pstmt.close(); + } + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTable); + while (rs.next()) { + assertEquals("" + rs.getInt(1), numeric[2]); + // System.out.println(rs.getShort(1)); //does not work says cannot cast integer to short cause it is written as int + } + } + + /** + * Test with bigint value + * + * @throws SQLServerException + */ + @Test + public void testBigInt() throws SQLServerException { + Random r = new Random(); + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.SQL_VARIANT); + String[] numeric = createNumericValues(); + tvp.addRow(Long.parseLong(numeric[4])); + + pstmt = (SQLServerPreparedStatement) connection.prepareStatement("INSERT INTO " + destTable + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + pstmt.execute(); + if (null != pstmt) { + pstmt.close(); + } + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTable); + while (rs.next()) { + assertEquals(rs.getLong(1), Long.parseLong(numeric[4])); + } + } + + /** + * Test with boolean value + * + * @throws SQLServerException + */ + @Test + public void testBoolean() throws SQLServerException { + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.SQL_VARIANT); + String[] numeric = createNumericValues(); + tvp.addRow(Boolean.parseBoolean(numeric[0])); + pstmt = (SQLServerPreparedStatement) connection.prepareStatement("INSERT INTO " + destTable + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + pstmt.execute(); + if (null != pstmt) { + pstmt.close(); + } + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTable); + while (rs.next()) { + assertEquals(rs.getBoolean(1), Boolean.parseBoolean(numeric[0])); + } + } + + /** + * Test with float value + * + * @throws SQLServerException + */ + @Test + public void testFloat() throws SQLServerException { + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.SQL_VARIANT); + String[] numeric = createNumericValues(); + tvp.addRow(Float.parseFloat(numeric[1])); + pstmt = (SQLServerPreparedStatement) connection.prepareStatement("INSERT INTO " + destTable + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + pstmt.execute(); + if (null != pstmt) { + pstmt.close(); + } + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTable); + while (rs.next()) { + assertEquals(rs.getFloat(1), Float.parseFloat(numeric[1])); + } + } + + /** + * Test with nvarchar + * + * @throws SQLServerException + */ + @Test + public void testNvarChar() throws SQLServerException { + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.SQL_VARIANT); + String colValue = "س"; + tvp.addRow(colValue); + pstmt = (SQLServerPreparedStatement) connection.prepareStatement("INSERT INTO " + destTable + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + pstmt.execute(); + if (null != pstmt) { + pstmt.close(); + } + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTable); + while (rs.next()) { + assertEquals(rs.getString(1), colValue); + } + } + + /** + * Test with varchar8000 + * + * @throws SQLServerException + */ + @Test + public void testVarChar8000() throws SQLServerException { + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.SQL_VARIANT); + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < 8000; i++) { + buffer.append("a"); + } + String value = buffer.toString(); + tvp.addRow(value); + + pstmt = (SQLServerPreparedStatement) connection.prepareStatement("INSERT INTO " + destTable + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + pstmt.execute(); + if (null != pstmt) { + pstmt.close(); + } + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTable); + while (rs.next()) { + assertEquals(rs.getString(1), value); + } + } + + /** + * Check that we throw proper error message when inserting more than 8000 + * + * @throws SQLServerException + */ + @Test + public void testLongVarChar() throws SQLServerException { + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.SQL_VARIANT); + + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < 8001; i++) { + buffer.append("a"); + } + String value = buffer.toString(); + tvp.addRow(value); + + pstmt = (SQLServerPreparedStatement) connection.prepareStatement("INSERT INTO " + destTable + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + try { + pstmt.execute(); + } + catch (SQLServerException e) { + assertTrue(e.getMessage().contains("SQL_VARIANT does not support string values of length greater than 8000.")); + } + catch (Exception e) { + fail("Test should have failed! mistakenly inserted string value of more than 8000 in sql-variant"); + } + finally { + if (null != pstmt) { + pstmt.close(); + } + } + } + + /** + * Test ith datetime + * + * @throws SQLServerException + */ + @Test + public void testDateTime() throws SQLServerException { + java.sql.Timestamp timestamp = java.sql.Timestamp.valueOf("2007-09-23 10:10:10.0"); + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.SQL_VARIANT); + tvp.addRow(timestamp); + + pstmt = (SQLServerPreparedStatement) connection.prepareStatement("INSERT INTO " + destTable + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + pstmt.execute(); + if (null != pstmt) { + pstmt.close(); + } + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTable); + while (rs.next()) { + assertEquals(rs.getString(1), "" + timestamp); + // System.out.println(rs.getDateTime(1));// TODO does not work + } + } + + /** + * Test with null value + * + * @throws SQLServerException + */ + @Test // TODO We need to check this later. Right now sending null with TVP is not supported + public void testNull() throws SQLServerException { + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.SQL_VARIANT); + try { + tvp.addRow((Date) null); + } + catch (Exception e) { + assertTrue(e.getMessage().startsWith("Use of TVPs containing null sql_variant columns is not supported.")); + } + + pstmt = (SQLServerPreparedStatement) connection.prepareStatement("INSERT INTO " + destTable + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + pstmt.execute(); + if (null != pstmt) { + pstmt.close(); + } + rs = (SQLServerResultSet) stmt.executeQuery("SELECT * FROM " + destTable); + while (rs.next()) { + System.out.println(rs.getString(1)); + } + } + + /** + * Test with stored procedure + * + * @throws SQLServerException + */ + @Test + public void testIntStoredProcedure() throws SQLServerException { + java.sql.Timestamp timestamp = java.sql.Timestamp.valueOf("2007-09-23 10:10:10.0"); + final String sql = "{call " + procedureName + "(?)}"; + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.SQL_VARIANT); + tvp.addRow(timestamp); + SQLServerCallableStatement Cstatement = (SQLServerCallableStatement) connection.prepareCall(sql); + Cstatement.setStructured(1, tvpName, tvp); + Cstatement.execute(); + rs = (SQLServerResultSet) stmt.executeQuery("select * from " + destTable); + while (rs.next()) { + System.out.println(rs.getString(1)); + } + if (null != Cstatement) { + Cstatement.close(); + } + } + + /** + * Test for allowing duplicate columns + * + * @throws SQLServerException + */ + @Test + public void testDuplicateColumn() throws SQLServerException { + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.SQL_VARIANT); + tvp.addColumnMetadata("c2", microsoft.sql.Types.SQL_VARIANT); + try { + tvp.addColumnMetadata("c2", microsoft.sql.Types.SQL_VARIANT); + } catch (SQLServerException e) { + assertEquals(e.getMessage(), "A column name c2 already belongs to this SQLServerDataTable."); + } + } + + private static String[] createNumericValues() { + Boolean C1_BIT; + Short C2_TINYINT; + Short C3_SMALLINT; + Integer C4_INT; + Long C5_BIGINT; + Double C6_FLOAT; + Double C7_FLOAT; + Float C8_REAL; + BigDecimal C9_DECIMAL; + BigDecimal C10_DECIMAL; + BigDecimal C11_NUMERIC; + + boolean nullable = false; + RandomData.returnNull = nullable; + C1_BIT = RandomData.generateBoolean(nullable); + C2_TINYINT = RandomData.generateTinyint(nullable); + C3_SMALLINT = RandomData.generateSmallint(nullable); + C4_INT = RandomData.generateInt(nullable); + C5_BIGINT = RandomData.generateLong(nullable); + C6_FLOAT = RandomData.generateFloat(24, nullable); + C7_FLOAT = RandomData.generateFloat(53, nullable); + C8_REAL = RandomData.generateReal(nullable); + C9_DECIMAL = RandomData.generateDecimalNumeric(18, 0, nullable); + C10_DECIMAL = RandomData.generateDecimalNumeric(10, 5, nullable); + C11_NUMERIC = RandomData.generateDecimalNumeric(18, 0, nullable); + BigDecimal C12_NUMERIC = RandomData.generateDecimalNumeric(8, 2, nullable); + BigDecimal C13_smallMoney = RandomData.generateSmallMoney(nullable); + BigDecimal C14_money = RandomData.generateMoney(nullable); + BigDecimal C15_decimal = RandomData.generateDecimalNumeric(28, 4, nullable); + BigDecimal C16_numeric = RandomData.generateDecimalNumeric(28, 4, nullable); + + String[] numericValues = {"" + C1_BIT, "" + C2_TINYINT, "" + C3_SMALLINT, "" + C4_INT, "" + C5_BIGINT, "" + C6_FLOAT, "" + C7_FLOAT, + "" + C8_REAL, "" + C9_DECIMAL, "" + C10_DECIMAL, "" + C11_NUMERIC, "" + C12_NUMERIC, "" + C13_smallMoney, "" + C14_money, + "" + C15_decimal, "" + C16_numeric}; + + if (RandomData.returnZero && !RandomData.returnNull) { + C10_DECIMAL = new BigDecimal(0); + C12_NUMERIC = new BigDecimal(0); + C13_smallMoney = new BigDecimal(0); + C14_money = new BigDecimal(0); + C15_decimal = new BigDecimal(0); + C16_numeric = new BigDecimal(0); + } + return numericValues; + } + + @BeforeEach + private void testSetup() throws SQLException { + conn = (SQLServerConnection) DriverManager.getConnection(connectionString + ";sendStringParametersAsUnicode=true;"); + stmt = (SQLServerStatement) conn.createStatement(); + + Utils.dropProcedureIfExists(procedureName, stmt); + Utils.dropTableIfExists(destTable, stmt); + dropTVPS(); + + createTVPS(); + createTables(); + createPreocedure(); + } + + private static void dropTVPS() throws SQLException { + stmt.executeUpdate("IF EXISTS (SELECT * FROM sys.types WHERE is_table_type = 1 AND name = '" + tvpName + "') " + " drop type " + tvpName); + } + + private static void createPreocedure() throws SQLException { + String sql = "CREATE PROCEDURE " + procedureName + " @InputData " + tvpName + " READONLY " + " AS " + " BEGIN " + " INSERT INTO " + destTable + + " SELECT * FROM @InputData" + " END"; + + stmt.execute(sql); + } + + private void createTables() throws SQLException { + String sql = "create table " + destTable + " (c1 sql_variant null);"; + stmt.execute(sql); + } + + private void createTVPS() throws SQLException { + String TVPCreateCmd = "CREATE TYPE " + tvpName + " as table (c1 sql_variant null)"; + stmt.executeUpdate(TVPCreateCmd); + } + + @AfterEach + private void terminateVariation() throws SQLException { + Utils.dropProcedureIfExists(procedureName, stmt); + Utils.dropTableIfExists(destTable, stmt); + dropTVPS(); + } + + /** + * drop the tables + * + * @throws SQLException + */ + @AfterAll + public static void afterAll() throws SQLException { + if (null != stmt) { + stmt.close(); + } + + if (null != pstmt) { + pstmt.close(); + } + + if (null != rs) { + rs.close(); + } + + if (null != conn) { + conn.close(); + } + + } + +} \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/dns/DNSRealmsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/dns/DNSRealmsTest.java new file mode 100644 index 000000000..8a16cffc9 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/dns/DNSRealmsTest.java @@ -0,0 +1,28 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.dns; + +import javax.naming.NamingException; + +public class DNSRealmsTest { + + public static void main(String... args) { + if (args.length < 1) { + System.err.println("USAGE: list of domains to test for kerberos realms"); + } + for (String realmName : args) { + try { + System.out.print(DNSKerberosLocator.isRealmValid(realmName) ? "[ VALID ] " : "[INVALID] "); + } catch (NamingException err) { + System.err.print("[ FAILED] : " + err.getClass().getName() + ":" + err.getMessage()); + } + System.out.println(realmName); + } + } + +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/exception/ExceptionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/exception/ExceptionTest.java new file mode 100644 index 000000000..84108ab09 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/exception/ExceptionTest.java @@ -0,0 +1,99 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.exception; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.UnsupportedEncodingException; +import java.net.SocketTimeoutException; +import java.sql.DriverManager; +import java.sql.SQLException; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerBulkCSVFileRecord; +import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; + +@RunWith(JUnitPlatform.class) +public class ExceptionTest extends AbstractTest { + static String inputFile = "BulkCopyCSVTestInput.csv"; + + /** + * Test the SQLServerException has the proper cause when encoding is not supported. + * + * @throws Exception + */ + @Test + public void testBulkCSVFileRecordExceptionCause() throws Exception { + String filePath = Utils.getCurrentClassPath(); + + try { + SQLServerBulkCSVFileRecord scvFileRecord = new SQLServerBulkCSVFileRecord(filePath + inputFile, "invalid_encoding", true); + } + catch (Exception e) { + if (!(e instanceof SQLServerException)) { + throw e; + } + + assertTrue(null != e.getCause(), "Cause should not be null."); + assertTrue(e.getCause() instanceof UnsupportedEncodingException, "Cause should be instance of UnsupportedEncodingException."); + } + } + + String waitForDelaySPName = "waitForDelaySP"; + final int waitForDelaySeconds = 10; + + /** + * Test the SQLServerException has the proper cause when socket timeout occurs. + * + * @throws Exception + * + */ + @Test + @Disabled //TODO Fix the issue : Connection Resiliency + public void testSocketTimeoutExceptionCause() throws Exception { + SQLServerConnection conn = null; + try { + conn = (SQLServerConnection) DriverManager.getConnection(connectionString); + + Utils.dropProcedureIfExists(waitForDelaySPName, conn.createStatement()); + createWaitForDelayPreocedure(conn); + + conn = (SQLServerConnection) DriverManager.getConnection(connectionString + ";socketTimeout=" + (waitForDelaySeconds * 1000 / 2) + ";"); + + try { + conn.createStatement().execute("exec " + waitForDelaySPName); + throw new Exception("Exception for socketTimeout is not thrown."); + } + catch (Exception e) { + if (!(e instanceof SQLServerException)) { + throw e; + } + + assertTrue(null != e.getCause(), "Cause should not be null."); + assertTrue(e.getCause() instanceof SocketTimeoutException, "Cause should be instance of SocketTimeoutException."); + } + } + finally { + if (null != conn) { + conn.close(); + } + } + } + + private void createWaitForDelayPreocedure(SQLServerConnection conn) throws SQLException { + String sql = "CREATE PROCEDURE " + waitForDelaySPName + " AS" + " BEGIN" + " WAITFOR DELAY '00:00:" + waitForDelaySeconds + "';" + " END"; + conn.createStatement().execute(sql); + } +} \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fips/FipsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fips/FipsTest.java index 83f8a9f71..3a257328a 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/fips/FipsTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/fips/FipsTest.java @@ -52,7 +52,7 @@ public void fipsTrustServerCertificateTest() throws Exception { } catch (SQLServerException e) { Assertions.assertTrue( - e.getMessage().contains("Could not enable FIPS due to either encrypt is not true or using trusted certificate settings."), + e.getMessage().contains("Unable to verify FIPS mode settings."), "Should create exception for invalid TrustServerCertificate value"); } } @@ -72,31 +72,11 @@ public void fipsEncryptTest() throws Exception { } catch (SQLServerException e) { Assertions.assertTrue( - e.getMessage().contains("Could not enable FIPS due to either encrypt is not true or using trusted certificate settings."), + e.getMessage().contains("Unable to verify FIPS mode settings."), "Should create exception for invalid encrypt value"); } } - /** - * Test after removing FIPS PROVIDER - * - * @throws Exception - */ - @Test - public void fipsProviderTest() throws Exception { - try { - Properties props = buildConnectionProperties(); - props.remove("fipsProvider"); - props.setProperty("trustStore", "/SOME_PATH"); - Connection con = PrepUtil.getConnection(connectionString, props); - Assertions.fail("It should fail as we are not passing appropriate params"); - } - catch (SQLServerException e) { - Assertions.assertTrue(e.getMessage().contains("Could not enable FIPS due to invalid FIPSProvider or TrustStoreType"), - "Should create exception for invalid FIPSProvider"); - } - } - /** * Test after removing fips, encrypt & trustStore it should work appropriately. * @@ -124,7 +104,6 @@ public void fipsDataSourcePropertyTest() throws Exception { SQLServerDataSource ds = new SQLServerDataSource(); setDataSourceProperties(ds); ds.setFIPS(false); - ds.setFIPSProvider(""); ds.setEncrypt(false); ds.setTrustStoreType("JKS"); Connection con = ds.getConnection(); @@ -148,32 +127,11 @@ public void fipsDatSourceEncrypt() { } catch (SQLServerException e) { Assertions.assertTrue( - e.getMessage().contains("Could not enable FIPS due to either encrypt is not true or using trusted certificate settings."), + e.getMessage().contains("Unable to verify FIPS mode settings."), "Should create exception for invalid encrypt value"); } } - /** - * Test after removing FIPS PROVIDER - * - * @throws Exception - */ - @Test - public void fipsDataSourceProviderTest() throws Exception { - try { - SQLServerDataSource ds = new SQLServerDataSource(); - setDataSourceProperties(ds); - ds.setFIPSProvider(""); - ds.setTrustStore("/SOME_PATH"); - Connection con = ds.getConnection(); - Assertions.fail("It should fail as we are not passing appropriate params"); - } - catch (SQLServerException e) { - Assertions.assertTrue(e.getMessage().contains("Could not enable FIPS due to invalid FIPSProvider or TrustStoreType"), - "Should create exception for invalid FIPSProvider"); - } - } - /** * Test after setting TrustServerCertificate as true. * @@ -190,7 +148,7 @@ public void fipsDataSourceTrustServerCertificateTest() throws Exception { } catch (SQLServerException e) { Assertions.assertTrue( - e.getMessage().contains("Could not enable FIPS due to either encrypt is not true or using trusted certificate settings."), + e.getMessage().contains("Unable to verify FIPS mode settings."), "Should create exception for invalid TrustServerCertificate value"); } } @@ -216,7 +174,6 @@ private void setDataSourceProperties(SQLServerDataSource ds) { ds.setTrustServerCertificate(false); ds.setIntegratedSecurity(false); ds.setTrustStoreType("PKCS12"); - ds.setFIPSProvider("BCFIPS"); } /** @@ -235,7 +192,6 @@ private Properties buildConnectionProperties() { // For New Code connectionProps.setProperty("trustStoreType", "PKCS12"); - connectionProps.setProperty("fipsProvider", "BCFIPS"); connectionProps.setProperty("fips", "true"); return connectionProps; diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/parametermetadata/ParameterMetaDataTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/parametermetadata/ParameterMetaDataTest.java index 1c6d397fd..6419d2199 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/parametermetadata/ParameterMetaDataTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/parametermetadata/ParameterMetaDataTest.java @@ -21,13 +21,15 @@ import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; +import com.microsoft.sqlserver.jdbc.SQLServerException; import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; import com.microsoft.sqlserver.testframework.util.RandomUtil; @RunWith(JUnitPlatform.class) public class ParameterMetaDataTest extends AbstractTest { private static final String tableName = "[" + RandomUtil.getIdentifier("StatementParam") + "]"; - + /** * Test ParameterMetaData#isWrapperFor and ParameterMetaData#unwrap. * @@ -35,23 +37,62 @@ public class ParameterMetaDataTest extends AbstractTest { */ @Test public void testParameterMetaDataWrapper() throws SQLException { - try (Connection con = DriverManager.getConnection(connectionString); - Statement stmt = con.createStatement()) { + try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement()) { stmt.executeUpdate("create table " + tableName + " (col1 int identity(1,1) primary key)"); try { String query = "SELECT * from " + tableName + " where col1 = ?"; - + try (PreparedStatement pstmt = con.prepareStatement(query)) { ParameterMetaData parameterMetaData = pstmt.getParameterMetaData(); assertTrue(parameterMetaData.isWrapperFor(ParameterMetaData.class)); assertSame(parameterMetaData, parameterMetaData.unwrap(ParameterMetaData.class)); } - } finally { - stmt.executeUpdate("drop table if exists " + tableName); } + finally { + Utils.dropTableIfExists(tableName, stmt); + } + } + } + /** + * Test SQLServerException is not wrapped with another SQLServerException. + * + * @throws SQLException + */ + @Test + public void testSQLServerExceptionNotWrapped() throws SQLException { + try (Connection con = DriverManager.getConnection(connectionString); + PreparedStatement pstmt = connection.prepareStatement("invalid query :)");) { + + pstmt.getParameterMetaData(); + } + catch (SQLServerException e) { + assertTrue(!e.getMessage().contains("com.microsoft.sqlserver.jdbc.SQLServerException"), + "SQLServerException should not be wrapped by another SQLServerException."); } } + + /** + * Test ParameterMetaData when parameter name contains braces + * + * @throws SQLException + */ + @Test + public void testNameWithBraces() throws SQLException { + try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement()) { + + stmt.executeUpdate("create table " + tableName + " ([c1_varchar(max)] varchar(max))"); + try { + String query = "insert into " + tableName + " ([c1_varchar(max)]) values (?)"; + try (PreparedStatement pstmt = con.prepareStatement(query)) { + pstmt.getParameterMetaData(); + } + } + finally { + Utils.dropTableIfExists(tableName, stmt); + } + } + } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/parametermetadata/ParameterMetaDataWhiteSpaceTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/parametermetadata/ParameterMetaDataWhiteSpaceTest.java new file mode 100644 index 000000000..6911066b0 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/parametermetadata/ParameterMetaDataWhiteSpaceTest.java @@ -0,0 +1,145 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.parametermetadata; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; +import com.microsoft.sqlserver.testframework.util.RandomUtil; + +@RunWith(JUnitPlatform.class) +public class ParameterMetaDataWhiteSpaceTest extends AbstractTest { + private static final String tableName = "[" + RandomUtil.getIdentifier("ParameterMetaDataWhiteSpaceTest") + "]"; + + private static Statement stmt = null; + + @BeforeAll + public static void BeforeTests() throws SQLException { + connection = (SQLServerConnection) DriverManager.getConnection(connectionString); + stmt = connection.createStatement(); + createCharTable(); + } + + @AfterAll + public static void dropTables() throws SQLException { + Utils.dropTableIfExists(tableName, stmt); + + if (null != stmt) { + stmt.close(); + } + + if (null != connection) { + connection.close(); + } + } + + private static void createCharTable() throws SQLException { + stmt.execute("Create table " + tableName + " (c1 int)"); + } + + /** + * Test regular simple query + * + * @throws SQLException + */ + @Test + public void NormalTest() throws SQLException { + testUpdateWithTwoParameters("update " + tableName + " set c1 = ? where c1 = ?"); + testInsertWithOneParameter("insert into " + tableName + " (c1) values (?)"); + } + + /** + * Test query with new line character + * + * @throws SQLException + */ + @Test + public void NewLineTest() throws SQLException { + testQueriesWithWhiteSpaces("\n"); + } + + /** + * Test query with tab character + * + * @throws SQLException + */ + @Test + public void TabTest() throws SQLException { + testQueriesWithWhiteSpaces("\t"); + } + + /** + * Test query with form feed character + * + * @throws SQLException + */ + @Test + public void FormFeedTest() throws SQLException { + testQueriesWithWhiteSpaces("\f"); + } + + private void testQueriesWithWhiteSpaces(String whiteSpace) throws SQLException { + testUpdateWithTwoParameters("update" + whiteSpace + tableName + " set c1 = ? where c1 = ?"); + testUpdateWithTwoParameters("update " + tableName + " set" + whiteSpace + "c1 = ? where c1 = ?"); + testUpdateWithTwoParameters("update " + tableName + " set c1 = ? where" + whiteSpace + "c1 = ?"); + + testInsertWithOneParameter("insert into " + tableName + "(c1) values (?)"); // no space between table name and column name + testInsertWithOneParameter("insert into" + whiteSpace + tableName + " (c1) values (?)"); + } + + private void testUpdateWithTwoParameters(String sql) throws SQLException { + insertTestRow(1); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setInt(1, 2); + ps.setInt(2, 1); + ps.executeUpdate(); + assertTrue(isIdPresentInTable(2), "Expected ID is not present"); + assertEquals(2, ps.getParameterMetaData().getParameterCount(), "Parameter count mismatch"); + } + } + + private void testInsertWithOneParameter(String sql) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setInt(1, 1); + ps.executeUpdate(); + assertTrue(isIdPresentInTable(1), "Insert statement did not work"); + assertEquals(1, ps.getParameterMetaData().getParameterCount(), "Parameter count mismatch"); + } + } + + private void insertTestRow(int id) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement("insert into " + tableName + " (c1) values (?)")) { + ps.setInt(1, id); + ps.executeUpdate(); + } + } + + private boolean isIdPresentInTable(int id) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement("select c1 from " + tableName + " where c1 = ?")) { + ps.setInt(1, id); + try (ResultSet rs = ps.executeQuery()) { + return rs.next(); + } + } + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithNullTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithNullTest.java new file mode 100644 index 000000000..ceb0bde9b --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithNullTest.java @@ -0,0 +1,140 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.preparedStatement; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; +import org.opentest4j.TestAbortedException; + +import com.microsoft.sqlserver.jdbc.SQLServerStatement; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.DBConnection; +import com.microsoft.sqlserver.testframework.Utils; + +@RunWith(JUnitPlatform.class) +public class BatchExecutionWithNullTest extends AbstractTest { + + static Statement stmt = null; + static Connection connection = null; + static PreparedStatement pstmt = null; + static PreparedStatement pstmt1 = null; + static ResultSet rs = null; + + /** + * Test with combination of setString and setNull which cause the "Violation of PRIMARY KEY constraint and internally + * "Could not find prepared statement with handle X" error. + * @throws SQLException + */ + @Test + public void testAddBatch2() throws SQLException { + // try { + String sPrepStmt = "insert into esimple (id, name) values (?, ?)"; + int updateCountlen = 0; + int key = 42; + + // this is the minimum sequence, I've found to trigger the error + pstmt = connection.prepareStatement(sPrepStmt); + pstmt.setInt(1, key++); + pstmt.setNull(2, Types.VARCHAR); + pstmt.addBatch(); + + pstmt.setInt(1, key++); + pstmt.setString(2, "FOO"); + pstmt.addBatch(); + + pstmt.setInt(1, key++); + pstmt.setNull(2, Types.VARCHAR); + pstmt.addBatch(); + + int[] updateCount = pstmt.executeBatch(); + updateCountlen += updateCount.length; + + pstmt.setInt(1, key++); + pstmt.setString(2, "BAR"); + pstmt.addBatch(); + + pstmt.setInt(1, key++); + pstmt.setNull(2, Types.VARCHAR); + pstmt.addBatch(); + + updateCount = pstmt.executeBatch(); + updateCountlen += updateCount.length; + + assertTrue(updateCountlen == 5, "addBatch does not add the SQL Statements to Batch ,call to addBatch failed"); + + String sPrepStmt1 = "select count(*) from esimple"; + + pstmt1 = connection.prepareStatement(sPrepStmt1); + rs = pstmt1.executeQuery(); + rs.next(); + assertTrue(rs.getInt(1) == 5, "affected rows does not match with batch size. Insert failed"); + pstmt1.close(); + + } + + /** + * Tests the same as addBatch2, only with AE on the connection string + * + * @throws SQLException + */ + @Test + public void testAddbatch2AEOnConnection() throws SQLException { + connection = DriverManager.getConnection(connectionString + ";columnEncryptionSetting=Enabled;"); + testAddBatch2(); + } + + @BeforeEach + public void testSetup() throws TestAbortedException, Exception { + assumeTrue(13 <= new DBConnection(connectionString).getServerVersion(), + "Aborting test case as SQL Server version is not compatible with Always encrypted "); + + connection = DriverManager.getConnection(connectionString); + SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(); + Utils.dropTableIfExists("esimple", stmt); + String sql1 = "create table esimple (id integer not null, name varchar(255), constraint pk_esimple primary key (id))"; + stmt.execute(sql1); + stmt.close(); + } + + @AfterAll + public static void terminateVariation() throws SQLException { + + SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(); + Utils.dropTableIfExists("esimple", stmt); + + if (null != connection) { + connection.close(); + } + if (null != pstmt) { + pstmt.close(); + } + if (null != pstmt1) { + pstmt1.close(); + } + if (null != stmt) { + stmt.close(); + } + if (null != rs) { + rs.close(); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java new file mode 100644 index 000000000..05f36bdee --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java @@ -0,0 +1,436 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.preparedStatement; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; + +/** + * Tests with sql queries using preparedStatement without parameters + * + * + */ +@RunWith(JUnitPlatform.class) +public class RegressionTest extends AbstractTest { + static Connection con = null; + static PreparedStatement pstmt1 = null; + static PreparedStatement pstmt2 = null; + static PreparedStatement pstmt3 = null; + static PreparedStatement pstmt4 = null; + + /** + * Setup before test + * + * @throws SQLException + */ + @BeforeAll + public static void setupTest() throws SQLException { + con = DriverManager.getConnection(connectionString); + Statement stmt = con.createStatement(); + Utils.dropTableIfExists("x", stmt); + if (null != stmt) { + stmt.close(); + } + } + + /** + * Tests creating view using preparedStatement + * + * @throws SQLException + */ + @Test + public void createViewTest() throws SQLException { + try { + pstmt1 = con.prepareStatement("create view x as select 1 a"); + pstmt2 = con.prepareStatement("drop view x"); + pstmt1.execute(); + pstmt2.execute(); + } + catch (SQLException e) { + fail("Create/drop view with preparedStatement failed! Error message: " + e.getMessage()); + } + + finally { + if (null != pstmt1) { + pstmt1.close(); + } + if (null != pstmt2) { + pstmt2.close(); + } + } + } + + /** + * Tests creating schema using preparedStatement + * + * @throws SQLException + */ + @Test + public void createSchemaTest() throws SQLException { + try { + pstmt1 = con.prepareStatement("create schema x"); + pstmt2 = con.prepareStatement("drop schema x"); + pstmt1.execute(); + pstmt2.execute(); + } + catch (SQLException e) { + fail("Create/drop schema with preparedStatement failed! Error message:" + e.getMessage()); + } + + finally { + if (null != pstmt1) { + pstmt1.close(); + } + if (null != pstmt2) { + pstmt2.close(); + } + } + } + + /** + * Test creating and dropping tabel with preparedStatement + * + * @throws SQLException + */ + @Test + public void createTableTest() throws SQLException { + try { + pstmt1 = con.prepareStatement("create table x (col1 int)"); + pstmt2 = con.prepareStatement("drop table x"); + pstmt1.execute(); + pstmt2.execute(); + } + catch (SQLException e) { + fail("Create/drop table with preparedStatement failed! Error message:" + e.getMessage()); + } + + finally { + if (null != pstmt1) { + pstmt1.close(); + } + if (null != pstmt2) { + pstmt2.close(); + } + } + } + + /** + * Tests creating/altering/dropping table + * + * @throws SQLException + */ + @Test + public void alterTableTest() throws SQLException { + try { + pstmt1 = con.prepareStatement("create table x (col1 int)"); + pstmt2 = con.prepareStatement("ALTER TABLE x ADD column_name char;"); + pstmt3 = con.prepareStatement("drop table x"); + pstmt1.execute(); + pstmt2.execute(); + pstmt3.execute(); + } + catch (SQLException e) { + fail("Create/drop/alter table with preparedStatement failed! Error message:" + e.getMessage()); + } + + finally { + if (null != pstmt1) { + pstmt1.close(); + } + if (null != pstmt2) { + pstmt2.close(); + } + if (null != pstmt3) { + pstmt3.close(); + } + } + } + + /** + * Tests with grant queries + * + * @throws SQLException + */ + @Test + public void grantTest() throws SQLException { + try { + pstmt1 = con.prepareStatement("create table x (col1 int)"); + pstmt2 = con.prepareStatement("grant select on x to public"); + pstmt3 = con.prepareStatement("revoke select on x from public"); + pstmt4 = con.prepareStatement("drop table x"); + pstmt1.execute(); + pstmt2.execute(); + pstmt3.execute(); + pstmt4.execute(); + } + catch (SQLException e) { + fail("grant with preparedStatement failed! Error message:" + e.getMessage()); + } + + finally { + if (null != pstmt1) { + pstmt1.close(); + } + if (null != pstmt2) { + pstmt2.close(); + } + if (null != pstmt3) { + pstmt3.close(); + } + if (null != pstmt4) { + pstmt4.close(); + } + } + } + + /** + * Test with large string and batch + * + * @throws SQLException + */ + @Test + public void batchWithLargeStringTest() throws SQLException { + Statement stmt = con.createStatement(); + PreparedStatement pstmt = null; + ResultSet rs = null; + Utils.dropTableIfExists("TEST_TABLE", stmt); + + con.setAutoCommit(false); + + // create a table with two columns + boolean createPrimaryKey = false; + try { + stmt.execute("if object_id('TEST_TABLE', 'U') is not null\ndrop table TEST_TABLE;"); + if (createPrimaryKey) { + stmt.execute("create table TEST_TABLE ( ID int, DATA nvarchar(max), primary key (ID) );"); + } + else { + stmt.execute("create table TEST_TABLE ( ID int, DATA nvarchar(max) );"); + } + } + catch (Exception e) { + fail(e.toString()); + } + + con.commit(); + + // build a String with 4001 characters + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < 4001; i++) { + stringBuilder.append('c'); + } + String largeString = stringBuilder.toString(); + + String[] values = {"a", "b", largeString, "d", "e"}; + // insert five rows into the table; use a batch for each row + try { + pstmt = con.prepareStatement("insert into TEST_TABLE values (?,?)"); + // 0,a + pstmt.setInt(1, 0); + pstmt.setNString(2, values[0]); + pstmt.addBatch(); + + // 1,b + pstmt.setInt(1, 1); + pstmt.setNString(2, values[1]); + pstmt.addBatch(); + + // 2,ccc... + pstmt.setInt(1, 2); + pstmt.setNString(2, values[2]); + pstmt.addBatch(); + + // 3,d + pstmt.setInt(1, 3); + pstmt.setNString(2, values[3]); + pstmt.addBatch(); + + // 4,e + pstmt.setInt(1, 4); + pstmt.setNString(2, values[4]); + pstmt.addBatch(); + + pstmt.executeBatch(); + } + catch (Exception e) { + fail(e.toString()); + } + connection.commit(); + + // check the data in the table + Map selectedValues = new LinkedHashMap<>(); + int id = 0; + try { + pstmt = con.prepareStatement("select * from TEST_TABLE;"); + try { + rs = pstmt.executeQuery(); + int i = 0; + while (rs.next()) { + id = rs.getInt(1); + String data = rs.getNString(2); + if (selectedValues.containsKey(id)) { + fail("Found duplicate id: " + id + " ,actual values is : " + values[i++] + " data is: " + data); + } + selectedValues.put(id, data); + } + } + finally { + if (null != rs) { + rs.close(); + } + } + } + finally { + Utils.dropTableIfExists("TEST_TABLE", stmt); + if (null != pstmt) { + pstmt.close(); + } + if (null != stmt) { + stmt.close(); + } + } + + } + + /** + * Test with large string and tests with more batch queries + * + * @throws SQLException + */ + @Test + public void addBatchWithLargeStringTest() throws SQLException { + Statement stmt = con.createStatement(); + PreparedStatement pstmt = null; + Utils.dropTableIfExists("TEST_TABLE", stmt); + + con.setAutoCommit(false); + + // create a table with two columns + boolean createPrimaryKey = false; + try { + stmt.execute("if object_id('testTable', 'U') is not null\ndrop table testTable;"); + if (createPrimaryKey) { + stmt.execute("create table testTable ( ID int, DATA nvarchar(max), primary key (ID) );"); + } + else { + stmt.execute("create table testTable ( ID int, DATA nvarchar(max) );"); + } + } + catch (Exception e) { + fail(e.toString()); + } + con.commit(); + + // build a String with 4001 characters + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < 4001; i++) { + stringBuilder.append('x'); + } + String largeString = stringBuilder.toString(); + + // insert five rows into the table; use a batch for each row + try { + pstmt = con.prepareStatement("insert into testTable values (?,?), (?,?);"); + // 0,a + // 1,b + pstmt.setInt(1, 0); + pstmt.setNString(2, "a"); + pstmt.setInt(3, 1); + pstmt.setNString(4, "b"); + pstmt.addBatch(); + + // 2,c + // 3,d + pstmt.setInt(1, 2); + pstmt.setNString(2, "c"); + pstmt.setInt(3, 3); + pstmt.setNString(4, "d"); + pstmt.addBatch(); + + // 4,xxx... + // 5,f + pstmt.setInt(1, 4); + pstmt.setNString(2, largeString); + pstmt.setInt(3, 5); + pstmt.setNString(4, "f"); + pstmt.addBatch(); + + // 6,g + // 7,h + pstmt.setInt(1, 6); + pstmt.setNString(2, "g"); + pstmt.setInt(3, 7); + pstmt.setNString(4, "h"); + pstmt.addBatch(); + + // 8,i + // 9,xxx... + pstmt.setInt(1, 8); + pstmt.setNString(2, "i"); + pstmt.setInt(3, 9); + pstmt.setNString(4, largeString); + pstmt.addBatch(); + + pstmt.executeBatch(); + + con.commit(); + } + + catch (Exception e) { + fail(e.toString()); + } + finally { + Utils.dropTableIfExists("testTable", stmt); + if (null != stmt) { + stmt.close(); + } + } + } + + /** + * Cleanup after test + * + * @throws SQLException + */ + @AfterAll + public static void cleanup() throws SQLException { + Statement stmt = con.createStatement(); + Utils.dropTableIfExists("x", stmt); + Utils.dropTableIfExists("TEST_TABLE", stmt); + if (null != stmt) { + stmt.close(); + } + if (null != con) { + con.close(); + } + if (null != pstmt1) { + pstmt1.close(); + } + if (null != pstmt2) { + pstmt2.close(); + } + + } + +} \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/resultset/ResultSetTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/resultset/ResultSetTest.java index 19c9cf8b6..4801b7fd1 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/resultset/ResultSetTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/resultset/ResultSetTest.java @@ -7,17 +7,24 @@ */ package com.microsoft.sqlserver.jdbc.resultset; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import java.math.BigDecimal; +import java.sql.Blob; +import java.sql.Clob; import java.sql.Connection; import java.sql.DriverManager; +import java.sql.NClob; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLXML; import java.sql.Statement; +import java.util.UUID; import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; @@ -25,6 +32,7 @@ import com.microsoft.sqlserver.jdbc.ISQLServerResultSet; import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; import com.microsoft.sqlserver.testframework.util.RandomUtil; @RunWith(JUnitPlatform.class) @@ -34,47 +42,201 @@ public class ResultSetTest extends AbstractTest { /** * Tests proper exception for unsupported operation * - * @throws Exception + * @throws SQLException */ @Test - public void testJdbc41ResultSetMethods() throws Exception { - Connection con = DriverManager.getConnection(connectionString); - Statement stmt = con.createStatement(); - try { - stmt.executeUpdate("create table " + tableName + " (col1 int, col2 text, col3 int identity(1,1) primary key)"); + public void testJdbc41ResultSetMethods() throws SQLException { + try (Connection con = DriverManager.getConnection(connectionString); + Statement stmt = con.createStatement()) { + stmt.executeUpdate("create table " + tableName + " ( " + + "col1 int, " + + "col2 varchar(512), " + + "col3 float, " + + "col4 decimal(10,5), " + + "col5 uniqueidentifier, " + + "col6 xml, " + + "col7 varbinary(max), " + + "col8 text, " + + "col9 ntext, " + + "col10 varbinary(max), " + + "col11 date, " + + "col12 time, " + + "col13 datetime2, " + + "col14 datetimeoffset, " + + "order_column int identity(1,1) primary key)"); + try { + + stmt.executeUpdate("Insert into " + tableName + " values(" + + "1, " // col1 + + "'hello', " // col2 + + "2.0, " // col3 + + "123.45, " // col4 + + "'6F9619FF-8B86-D011-B42D-00C04FC964FF', " // col5 + + "'', " // col6 + + "0x63C34D6BCAD555EB64BF7E848D02C376, " // col7 + + "'text', " // col8 + + "'ntext', " // col9 + + "0x63C34D6BCAD555EB64BF7E848D02C376," // col10 + + "'2017-05-19'," // col11 + + "'10:47:15.1234567'," // col12 + + "'2017-05-19T10:47:15.1234567'," // col13 + + "'2017-05-19T10:47:15.1234567+02:00'" // col14 + + ")"); + + stmt.executeUpdate("Insert into " + tableName + " values(" + + "null, " + + "null, " + + "null, " + + "null, " + + "null, " + + "null, " + + "null, " + + "null, " + + "null, " + + "null, " + + "null, " + + "null, " + + "null, " + + "null)"); + + try (ResultSet rs = stmt.executeQuery("select * from " + tableName + " order by order_column")) { + // test non-null values + assertTrue(rs.next()); + assertEquals(Byte.valueOf((byte) 1), rs.getObject(1, Byte.class)); + assertEquals(Byte.valueOf((byte) 1), rs.getObject("col1", Byte.class)); + assertEquals(Short.valueOf((short) 1), rs.getObject(1, Short.class)); + assertEquals(Short.valueOf((short) 1), rs.getObject("col1", Short.class)); + assertEquals(Integer.valueOf(1), rs.getObject(1, Integer.class)); + assertEquals(Integer.valueOf(1), rs.getObject("col1", Integer.class)); + assertEquals(Long.valueOf(1), rs.getObject(1, Long.class)); + assertEquals(Long.valueOf(1), rs.getObject("col1", Long.class)); + assertEquals(Boolean.TRUE, rs.getObject(1, Boolean.class)); + assertEquals(Boolean.TRUE, rs.getObject("col1", Boolean.class)); - stmt.executeUpdate("Insert into " + tableName + " values(0, 'hello')"); + assertEquals("hello", rs.getObject(2, String.class)); + assertEquals("hello", rs.getObject("col2", String.class)); - stmt.executeUpdate("Insert into " + tableName + " values(0, 'yo')"); + assertEquals(2.0f, rs.getObject(3, Float.class), 0.0001f); + assertEquals(2.0f, rs.getObject("col3", Float.class), 0.0001f); + assertEquals(2.0d, rs.getObject(3, Double.class), 0.0001d); + assertEquals(2.0d, rs.getObject("col3", Double.class), 0.0001d); - ResultSet rs = stmt.executeQuery("select * from " + tableName); - rs.next(); - // Both methods throw exceptions - try { + // BigDecimal#equals considers the number of decimal places + assertEquals(0, rs.getObject(4, BigDecimal.class).compareTo(new BigDecimal("123.45"))); + assertEquals(0, rs.getObject("col4", BigDecimal.class).compareTo(new BigDecimal("123.45"))); - int col1 = rs.getObject(1, Integer.class); - } - catch (Exception e) { - // unsupported feature - assertEquals(e.getClass(), SQLFeatureNotSupportedException.class, "Verify exception type: " + e.getMessage()); - } - try { - String col2 = rs.getObject("col2", String.class); - } - catch (Exception e) { - // unsupported feature - assertEquals(e.getClass(), SQLFeatureNotSupportedException.class, "Verify exception type: " + e.getMessage()); - } - try { + assertEquals(UUID.fromString("6F9619FF-8B86-D011-B42D-00C04FC964FF"), rs.getObject(5, UUID.class)); + assertEquals(UUID.fromString("6F9619FF-8B86-D011-B42D-00C04FC964FF"), rs.getObject("col5", UUID.class)); + + SQLXML sqlXml; + sqlXml = rs.getObject(6, SQLXML.class); + try { + assertEquals("", sqlXml.getString()); + } finally { + sqlXml.free(); + } + + Blob blob; + blob = rs.getObject(7, Blob.class); + try { + assertArrayEquals(new byte[] {0x63, (byte) 0xC3, 0x4D, 0x6B, (byte) 0xCA, (byte) 0xD5, 0x55, (byte) 0xEB, 0x64, (byte) 0xBF, 0x7E, (byte) 0x84, (byte) 0x8D, 0x02, (byte) 0xC3, 0x76}, + blob.getBytes(1, 16)); + } finally { + blob.free(); + } + + Clob clob; + clob = rs.getObject(8, Clob.class); + try { + assertEquals("text", clob.getSubString(1, 4)); + } finally { + clob.free(); + } + + NClob nclob; + nclob = rs.getObject(9, NClob.class); + try { + assertEquals("ntext", nclob.getSubString(1, 5)); + } finally { + nclob.free(); + } + + assertArrayEquals(new byte[] {0x63, (byte) 0xC3, 0x4D, 0x6B, (byte) 0xCA, (byte) 0xD5, 0x55, (byte) 0xEB, 0x64, (byte) 0xBF, 0x7E, (byte) 0x84, (byte) 0x8D, 0x02, (byte) 0xC3, 0x76}, + rs.getObject(10, byte[].class)); + + assertEquals(java.sql.Date.valueOf("2017-05-19"), rs.getObject(11, java.sql.Date.class)); + assertEquals(java.sql.Date.valueOf("2017-05-19"), rs.getObject("col11", java.sql.Date.class)); + + java.sql.Time expectedTime = new java.sql.Time(java.sql.Time.valueOf("10:47:15").getTime() + 123L); + assertEquals(expectedTime, rs.getObject(12, java.sql.Time.class)); + assertEquals(expectedTime, rs.getObject("col12", java.sql.Time.class)); + + assertEquals(java.sql.Timestamp.valueOf("2017-05-19 10:47:15.1234567"), rs.getObject(13, java.sql.Timestamp.class)); + assertEquals(java.sql.Timestamp.valueOf("2017-05-19 10:47:15.1234567"), rs.getObject("col13", java.sql.Timestamp.class)); + + assertEquals("2017-05-19 10:47:15.1234567 +02:00", rs.getObject(14, microsoft.sql.DateTimeOffset.class).toString()); + assertEquals("2017-05-19 10:47:15.1234567 +02:00", rs.getObject("col14", microsoft.sql.DateTimeOffset.class).toString()); + + + // test null values, mostly to verify primitive wrappers do not return default values + assertTrue(rs.next()); + assertNull(rs.getObject("col1", Boolean.class)); + assertNull(rs.getObject(1, Boolean.class)); + assertNull(rs.getObject("col1", Byte.class)); + assertNull(rs.getObject(1, Byte.class)); + assertNull(rs.getObject("col1", Short.class)); + assertNull(rs.getObject(1, Short.class)); + assertNull(rs.getObject(1, Integer.class)); + assertNull(rs.getObject("col1", Integer.class)); + assertNull(rs.getObject(1, Long.class)); + assertNull(rs.getObject("col1", Long.class)); + + assertNull(rs.getObject(2, String.class)); + assertNull(rs.getObject("col2", String.class)); + + assertNull(rs.getObject(3, Float.class)); + assertNull(rs.getObject("col3", Float.class)); + assertNull(rs.getObject(3, Double.class)); + assertNull(rs.getObject("col3", Double.class)); + + assertNull(rs.getObject(4, BigDecimal.class)); + assertNull(rs.getObject("col4", BigDecimal.class)); + + assertNull(rs.getObject(5, UUID.class)); + assertNull(rs.getObject("col5", UUID.class)); + + assertNull(rs.getObject(6, SQLXML.class)); + assertNull(rs.getObject("col6", SQLXML.class)); + + assertNull(rs.getObject(7, Blob.class)); + assertNull(rs.getObject("col7", Blob.class)); + + assertNull(rs.getObject(8, Clob.class)); + assertNull(rs.getObject("col8", Clob.class)); + + assertNull(rs.getObject(9, NClob.class)); + assertNull(rs.getObject("col9", NClob.class)); + + assertNull(rs.getObject(10, byte[].class)); + assertNull(rs.getObject("col10", byte[].class)); + + assertNull(rs.getObject(11, java.sql.Date.class)); + assertNull(rs.getObject("col11", java.sql.Date.class)); + + assertNull(rs.getObject(12, java.sql.Time.class)); + assertNull(rs.getObject("col12", java.sql.Time.class)); + + assertNull(rs.getObject(13, java.sql.Timestamp.class)); + assertNull(rs.getObject("col14", java.sql.Timestamp.class)); + + assertNull(rs.getObject(14, microsoft.sql.DateTimeOffset.class)); + assertNull(rs.getObject("col14", microsoft.sql.DateTimeOffset.class)); + + assertFalse(rs.next()); + } + } finally { stmt.executeUpdate("drop table " + tableName); } - catch (Exception ex) { - fail(ex.toString()); - } - } - finally { - stmt.close(); - con.close(); } } @@ -84,7 +246,7 @@ public void testJdbc41ResultSetMethods() throws Exception { * @throws SQLException */ @Test - public void testTesultSetWrapper() throws SQLException { + public void testResultSetWrapper() throws SQLException { try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement()) { @@ -97,7 +259,37 @@ public void testTesultSetWrapper() throws SQLException { assertSame(rs, rs.unwrap(ResultSet.class)); assertSame(rs, rs.unwrap(ISQLServerResultSet.class)); } finally { - stmt.executeUpdate("drop table if exists " + tableName); + Utils.dropTableIfExists(tableName, stmt); + } + } + } + + /** + * Tests calling any getter on a null column should work regardless of their type. + * + * @throws SQLException + */ + @Test + public void testGetterOnNull() throws SQLException { + Connection con = null; + Statement stmt = null; + ResultSet rs = null; + try { + con = DriverManager.getConnection(connectionString); + stmt = con.createStatement(); + rs = stmt.executeQuery("select null"); + rs.next(); + assertEquals(null, rs.getTime(1)); + } + finally { + if (con != null) { + con.close(); + } + if (stmt != null) { + stmt.close(); + } + if (rs != null) { + rs.close(); } } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/resultset/ResultSetWrapper42Test.java b/src/test/java/com/microsoft/sqlserver/jdbc/resultset/ResultSetWrapper42Test.java new file mode 100644 index 000000000..78612674e --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/resultset/ResultSetWrapper42Test.java @@ -0,0 +1,82 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.resultset; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerResultSet; +import com.microsoft.sqlserver.jdbc.SQLServerResultSet42; +import com.microsoft.sqlserver.testframework.AbstractTest; + +/** + * Test SQLServerResultSet42 class + * + */ +@RunWith(JUnitPlatform.class) +public class ResultSetWrapper42Test extends AbstractTest { + static Connection connection = null; + double javaVersion = Double.parseDouble(System.getProperty("java.specification.version")); + static int major; + static int minor; + + /** + * Tests creation of SQLServerResultSet42 object + * + * @throws SQLException + */ + @Test + public void SQLServerResultSet42Test() throws SQLException { + String sql = "SELECT SUSER_SNAME()"; + + ResultSet rs = null; + try { + rs = connection.createStatement().executeQuery(sql); + + if (1.8d <= javaVersion && 4 == major && 2 == minor) { + assertTrue(rs instanceof SQLServerResultSet42); + } + else { + assertTrue(rs instanceof SQLServerResultSet); + } + } + finally { + if (null != rs) { + rs.close(); + } + } + } + + @BeforeAll + private static void setupConnection() throws SQLException { + connection = DriverManager.getConnection(connectionString); + + DatabaseMetaData metadata = connection.getMetaData(); + major = metadata.getJDBCMajorVersion(); + minor = metadata.getJDBCMinorVersion(); + } + + @AfterAll + private static void terminateVariation() throws SQLException { + if (null != connection) { + connection.close(); + } + } + +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ssl/trustmanager/CustomTrustManagerTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ssl/trustmanager/CustomTrustManagerTest.java new file mode 100644 index 000000000..6874ca363 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ssl/trustmanager/CustomTrustManagerTest.java @@ -0,0 +1,59 @@ +package com.microsoft.sqlserver.jdbc.ssl.trustmanager; + +import java.sql.DriverManager; + +import static org.junit.Assert.*; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.testframework.AbstractTest; + +@RunWith(JUnitPlatform.class) +public class CustomTrustManagerTest extends AbstractTest { + + /** + * Connect with a permissive Trust Manager that always accepts the X509Certificate chain offered to it. + * + * @throws Exception + */ + @Test + public void testWithPermissiveX509TrustManager() throws Exception { + String url = connectionString + ";trustManagerClass=" + PermissiveTrustManager.class.getName() + ";encrypt=true;"; + try (SQLServerConnection con = (SQLServerConnection) DriverManager.getConnection(url)) { + assertTrue(con != null); + } + } + + /** + * Connect with a Trust Manager that requires trustManagerConstructorArg. + * + * @throws Exception + */ + @Test + public void testWithTrustManagerConstructorArg() throws Exception { + String url = connectionString + ";trustManagerClass=" + TrustManagerWithConstructorArg.class.getName() + + ";trustManagerConstructorArg=dummyString;" + ";encrypt=true;"; + try (SQLServerConnection con = (SQLServerConnection) DriverManager.getConnection(url)) { + assertTrue(con != null); + } + } + + /** + * Test with a custom Trust Manager that does not implement X509TrustManager. + * + * @throws Exception + */ + @Test + public void testWithInvalidTrustManager() throws Exception { + String url = connectionString + ";trustManagerClass=" + InvalidTrustManager.class.getName() + ";encrypt=true;"; + try (SQLServerConnection con = (SQLServerConnection) DriverManager.getConnection(url)) { + fail(); + } + catch (SQLServerException e) { + assertTrue(e.getMessage().contains("The class specified by the trustManagerClass property must implement javax.net.ssl.TrustManager")); + } + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ssl/trustmanager/InvalidTrustManager.java b/src/test/java/com/microsoft/sqlserver/jdbc/ssl/trustmanager/InvalidTrustManager.java new file mode 100644 index 000000000..f54fcf5a1 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ssl/trustmanager/InvalidTrustManager.java @@ -0,0 +1,26 @@ +package com.microsoft.sqlserver.jdbc.ssl.trustmanager; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * This class does not implement X509TrustManager and the connection must fail when it is specified by the trustManagerClass property + * + */ +public final class InvalidTrustManager { + public InvalidTrustManager() { + } + + public void checkClientTrusted(X509Certificate[] chain, + String authType) throws CertificateException { + + } + + public void checkServerTrusted(X509Certificate[] chain, + String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ssl/trustmanager/PermissiveTrustManager.java b/src/test/java/com/microsoft/sqlserver/jdbc/ssl/trustmanager/PermissiveTrustManager.java new file mode 100644 index 000000000..11c824385 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ssl/trustmanager/PermissiveTrustManager.java @@ -0,0 +1,24 @@ +package com.microsoft.sqlserver.jdbc.ssl.trustmanager; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.X509TrustManager; + +/** + * This class implements an X509TrustManager that always accepts the X509Certificate chain offered to it. + */ + +public final class PermissiveTrustManager implements X509TrustManager { + public void checkClientTrusted(X509Certificate[] chain, + String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, + String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ssl/trustmanager/TrustManagerWithConstructorArg.java b/src/test/java/com/microsoft/sqlserver/jdbc/ssl/trustmanager/TrustManagerWithConstructorArg.java new file mode 100644 index 000000000..4b572fccc --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ssl/trustmanager/TrustManagerWithConstructorArg.java @@ -0,0 +1,54 @@ +package com.microsoft.sqlserver.jdbc.ssl.trustmanager; + +import java.io.IOException; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.GeneralSecurityException; +import javax.net.ssl.X509TrustManager; + +/** + * This class implements an X509TrustManager that always accepts the X509Certificate chain offered to it. + * + * The constructor argument certToTrust is a dummy string used to test trustManagerConstructorArg. + * + */ + +public class TrustManagerWithConstructorArg implements X509TrustManager { + X509Certificate cert; + X509TrustManager trustManager; + + public TrustManagerWithConstructorArg(String certToTrust) throws IOException, GeneralSecurityException { + trustManager = new X509TrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, + String authType) throws CertificateException { + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, + String authType) throws CertificateException { + } + }; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, + String authType) throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, + String authType) throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPAllTypes.java b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPAllTypes.java new file mode 100644 index 000000000..4b33f124e --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPAllTypes.java @@ -0,0 +1,217 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.tvp; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerCallableStatement; +import com.microsoft.sqlserver.jdbc.SQLServerDataTable; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.DBConnection; +import com.microsoft.sqlserver.testframework.DBStatement; +import com.microsoft.sqlserver.testframework.DBTable; +import com.microsoft.sqlserver.testframework.Utils; +import com.microsoft.sqlserver.testframework.sqlType.SqlType; +import com.microsoft.sqlserver.testframework.util.ComparisonUtil; + +@RunWith(JUnitPlatform.class) +public class TVPAllTypes extends AbstractTest { + private static Connection conn = null; + static Statement stmt = null; + + private static String tvpName = "TVPAllTypesTable_char_TVP"; + private static String procedureName = "TVPAllTypesTable_char_SP"; + + private static DBTable tableSrc = null; + private static DBTable tableDest = null; + + /** + * Test TVP with result set + * + * @throws SQLException + */ + @Test + public void testTVPResultSet() throws SQLException { + testTVPResultSet(false, null, null); + testTVPResultSet(true, null, null); + testTVPResultSet(false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + testTVPResultSet(false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE); + testTVPResultSet(false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); + testTVPResultSet(false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); + } + + private void testTVPResultSet(boolean setSelectMethod, + Integer resultSetType, + Integer resultSetConcurrency) throws SQLException { + setupVariation(); + + Connection connnection = null; + if (setSelectMethod) { + connnection = DriverManager.getConnection(connectionString + ";selectMethod=cursor;"); + } + else { + connnection = DriverManager.getConnection(connectionString); + } + + Statement stmtement = null; + if (null != resultSetType || null != resultSetConcurrency) { + stmtement = connnection.createStatement(resultSetType, resultSetConcurrency); + } + else { + stmtement = connnection.createStatement(); + } + + ResultSet rs = stmtement.executeQuery("select * from " + tableSrc.getEscapedTableName()); + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connnection + .prepareStatement("INSERT INTO " + tableDest.getEscapedTableName() + " select * from ? ;"); + pstmt.setStructured(1, tvpName, rs); + pstmt.execute(); + + ComparisonUtil.compareSrcTableAndDestTableIgnoreRowOrder(new DBConnection(connectionString), tableSrc, tableDest); + + terminateVariation(); + } + + /** + * Test TVP with stored procedure and result set + * + * @throws SQLException + */ + @Test + public void testTVPStoredProcedureResultSet() throws SQLException { + testTVPStoredProcedureResultSet(false, null, null); + testTVPStoredProcedureResultSet(true, null, null); + testTVPStoredProcedureResultSet(false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + testTVPStoredProcedureResultSet(false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE); + testTVPStoredProcedureResultSet(false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); + testTVPStoredProcedureResultSet(false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); + } + + private void testTVPStoredProcedureResultSet(boolean setSelectMethod, + Integer resultSetType, + Integer resultSetConcurrency) throws SQLException { + setupVariation(); + + Connection connnection = null; + if (setSelectMethod) { + connnection = DriverManager.getConnection(connectionString + ";selectMethod=cursor;"); + } + else { + connnection = DriverManager.getConnection(connectionString); + } + + Statement stmtement = null; + if (null != resultSetType || null != resultSetConcurrency) { + stmtement = connnection.createStatement(resultSetType, resultSetConcurrency); + } + else { + stmtement = connnection.createStatement(); + } + + ResultSet rs = stmtement.executeQuery("select * from " + tableSrc.getEscapedTableName()); + + String sql = "{call " + procedureName + "(?)}"; + SQLServerCallableStatement Cstmt = (SQLServerCallableStatement) connnection.prepareCall(sql); + Cstmt.setStructured(1, tvpName, rs); + Cstmt.execute(); + + ComparisonUtil.compareSrcTableAndDestTableIgnoreRowOrder(new DBConnection(connectionString), tableSrc, tableDest); + + terminateVariation(); + } + + /** + * Test TVP with DataTable + * + * @throws SQLException + */ + @Test + public void testTVPDataTable() throws SQLException { + setupVariation(); + + SQLServerDataTable dt = new SQLServerDataTable(); + + int numberOfColumns = tableDest.getColumns().size(); + Object[] values = new Object[numberOfColumns]; + for (int i = 0; i < numberOfColumns; i++) { + SqlType sqlType = tableDest.getColumns().get(i).getSqlType(); + dt.addColumnMetadata(tableDest.getColumnName(i), sqlType.getJdbctype().getVendorTypeNumber()); + values[i] = sqlType.createdata(); + } + + int numberOfRows = 10; + for (int i = 0; i < numberOfRows; i++) { + dt.addRow(values); + } + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection + .prepareStatement("INSERT INTO " + tableDest.getEscapedTableName() + " select * from ? ;"); + pstmt.setStructured(1, tvpName, dt); + pstmt.execute(); + } + + private static void createPreocedure(String procedureName, + String destTable) throws SQLException { + String sql = "CREATE PROCEDURE " + procedureName + " @InputData " + tvpName + " READONLY " + " AS " + " BEGIN " + " INSERT INTO " + destTable + + " SELECT * FROM @InputData" + " END"; + + stmt.execute(sql); + } + + private static void dropTVPS(String tvpName) throws SQLException { + stmt.executeUpdate("IF EXISTS (SELECT * FROM sys.types WHERE is_table_type = 1 AND name = '" + tvpName + "') " + " drop type " + tvpName); + } + + private static void createTVPS(String TVPName, + String TVPDefinition) throws SQLException { + String TVPCreateCmd = "CREATE TYPE " + TVPName + " as table (" + TVPDefinition + ");"; + stmt.executeUpdate(TVPCreateCmd); + } + + private void setupVariation() throws SQLException { + conn = DriverManager.getConnection(connectionString); + stmt = conn.createStatement(); + + Utils.dropProcedureIfExists(procedureName, stmt); + dropTVPS(tvpName); + + DBConnection dbConnection = new DBConnection(connectionString); + DBStatement dbStmt = dbConnection.createStatement(); + + tableSrc = new DBTable(true); + tableDest = tableSrc.cloneSchema(); + + dbStmt.createTable(tableSrc); + dbStmt.createTable(tableDest); + + createTVPS(tvpName, tableSrc.getDefinitionOfColumns()); + createPreocedure(procedureName, tableDest.getEscapedTableName()); + + dbStmt.populateTable(tableSrc); + } + + private void terminateVariation() throws SQLException { + conn = DriverManager.getConnection(connectionString); + stmt = conn.createStatement(); + + Utils.dropProcedureIfExists(procedureName, stmt); + Utils.dropTableIfExists(tableSrc.getEscapedTableName(), stmt); + Utils.dropTableIfExists(tableDest.getEscapedTableName(), stmt); + dropTVPS(tvpName); + } +} \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPIssuesTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPIssuesTest.java new file mode 100644 index 000000000..26087ff36 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPIssuesTest.java @@ -0,0 +1,226 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.tvp; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerCallableStatement; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.jdbc.SQLServerStatement; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; + +@RunWith(JUnitPlatform.class) +public class TVPIssuesTest extends AbstractTest { + + static Connection connection = null; + static Statement stmt = null; + private static String tvp_varcharMax = "TVPIssuesTest_varcharMax_TVP"; + private static String spName_varcharMax = "TVPIssuesTest_varcharMax_SP"; + private static String srcTable_varcharMax = "TVPIssuesTest_varcharMax_srcTable"; + private static String desTable_varcharMax = "TVPIssuesTest_varcharMax_destTable"; + + private static String tvp_time_6 = "TVPIssuesTest_time_6_TVP"; + private static String srcTable_time_6 = "TVPIssuesTest_time_6_srcTable"; + private static String desTable_time_6 = "TVPIssuesTest_time_6_destTable"; + + private static String expectedTime6value = "15:39:27.616667"; + + @Test + public void tryTVPRSvarcharMax4000Issue() throws Exception { + + setup(); + + SQLServerStatement st = (SQLServerStatement) connection.createStatement(); + ResultSet rs = st.executeQuery("select * from " + srcTable_varcharMax); + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection + .prepareStatement("INSERT INTO " + desTable_varcharMax + " select * from ? ;"); + + pstmt.setStructured(1, tvp_varcharMax, rs); + pstmt.execute(); + + testCharDestTable(); + } + + /** + * Test exception when invalid stored procedure name is used. + * + * @throws Exception + */ + @Test + public void testExceptionWithInvalidStoredProcedureName() throws Exception { + SQLServerStatement st = (SQLServerStatement) connection.createStatement(); + ResultSet rs = st.executeQuery("select * from " + srcTable_varcharMax); + + dropProcedure(); + + final String sql = "{call " + spName_varcharMax + "(?)}"; + SQLServerCallableStatement Cstmt = (SQLServerCallableStatement) connection.prepareCall(sql); + try { + Cstmt.setObject(1, rs); + throw new Exception("Expected Exception for invalied stored procedure name is not thrown."); + } + catch (Exception e) { + if (e instanceof SQLServerException) { + assertTrue(e.getMessage().contains("Could not find stored procedure"), "Invalid Error Message."); + } + else { + throw e; + } + } + } + + /** + * Fix an issue: If column is time(x) and TVP is used (with either ResultSet, Stored Procedure or SQLServerDataTable). The milliseconds or + * nanoseconds are not copied into the destination table. + * + * @throws Exception + */ + @Test + public void tryTVPPrecisionmissedissue315() throws Exception { + + setup(); + + ResultSet rs = stmt.executeQuery("select * from " + srcTable_time_6); + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection + .prepareStatement("INSERT INTO " + desTable_time_6 + " select * from ? ;"); + pstmt.setStructured(1, tvp_time_6, rs); + pstmt.execute(); + + testTime6DestTable(); + } + + private void testCharDestTable() throws SQLException, IOException { + ResultSet rs = connection.createStatement().executeQuery("select * from " + desTable_varcharMax); + while (rs.next()) { + assertEquals(rs.getString(1).length(), 4001, " The inserted length is truncated or not correct!"); + } + if (null != rs) { + rs.close(); + } + } + + private void testTime6DestTable() throws SQLException, IOException { + ResultSet rs = connection.createStatement().executeQuery("select * from " + desTable_time_6); + while (rs.next()) { + assertEquals(rs.getString(1), expectedTime6value, " The time value is truncated or not correct!"); + } + if (null != rs) { + rs.close(); + } + } + + @BeforeAll + public static void beforeAll() throws SQLException { + + connection = DriverManager.getConnection(connectionString); + stmt = connection.createStatement(); + + dropProcedure(); + + stmt.executeUpdate( + "IF EXISTS (SELECT * FROM sys.types WHERE is_table_type = 1 AND name = '" + tvp_varcharMax + "') " + " drop type " + tvp_varcharMax); + Utils.dropTableIfExists(srcTable_varcharMax, stmt); + Utils.dropTableIfExists(desTable_varcharMax, stmt); + + stmt.executeUpdate( + "IF EXISTS (SELECT * FROM sys.types WHERE is_table_type = 1 AND name = '" + tvp_time_6 + "') " + " drop type " + tvp_time_6); + Utils.dropTableIfExists(srcTable_time_6, stmt); + Utils.dropTableIfExists(desTable_time_6, stmt); + + String sql = "create table " + srcTable_varcharMax + " (c1 varchar(max) null);"; + stmt.execute(sql); + sql = "create table " + desTable_varcharMax + " (c1 varchar(max) null);"; + stmt.execute(sql); + + sql = "create table " + srcTable_time_6 + " (c1 time(6) null);"; + stmt.execute(sql); + sql = "create table " + desTable_time_6 + " (c1 time(6) null);"; + stmt.execute(sql); + + String TVPCreateCmd = "CREATE TYPE " + tvp_varcharMax + " as table (c1 varchar(max) null)"; + stmt.executeUpdate(TVPCreateCmd); + + TVPCreateCmd = "CREATE TYPE " + tvp_time_6 + " as table (c1 time(6) null)"; + stmt.executeUpdate(TVPCreateCmd); + + createPreocedure(); + + populateCharSrcTable(); + populateTime6SrcTable(); + } + + private static void populateCharSrcTable() throws SQLException { + String sql = "insert into " + srcTable_varcharMax + " values (?)"; + + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < 4001; i++) { + sb.append("a"); + } + String value = sb.toString(); + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(sql); + + pstmt.setString(1, value); + pstmt.execute(); + } + + private static void populateTime6SrcTable() throws SQLException { + String sql = "insert into " + srcTable_time_6 + " values ('2017-05-12 " + expectedTime6value + "')"; + connection.createStatement().execute(sql); + } + + private static void dropProcedure() throws SQLException { + Utils.dropProcedureIfExists(spName_varcharMax, stmt); + } + + private static void createPreocedure() throws SQLException { + String sql = "CREATE PROCEDURE " + spName_varcharMax + " @InputData " + tvp_varcharMax + " READONLY " + " AS " + " BEGIN " + " INSERT INTO " + + desTable_varcharMax + " SELECT * FROM @InputData" + " END"; + + stmt.execute(sql); + } + + @AfterAll + public static void terminateVariation() throws SQLException { + dropProcedure(); + stmt.executeUpdate( + "IF EXISTS (SELECT * FROM sys.types WHERE is_table_type = 1 AND name = '" + tvp_varcharMax + "') " + " drop type " + tvp_varcharMax); + Utils.dropTableIfExists(srcTable_varcharMax, stmt); + Utils.dropTableIfExists(desTable_varcharMax, stmt); + + stmt.executeUpdate( + "IF EXISTS (SELECT * FROM sys.types WHERE is_table_type = 1 AND name = '" + tvp_time_6 + "') " + " drop type " + tvp_time_6); + Utils.dropTableIfExists(srcTable_time_6, stmt); + Utils.dropTableIfExists(desTable_time_6, stmt); + + if (null != connection) { + connection.close(); + } + if (null != stmt) { + stmt.close(); + } + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPNumericTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPNumericTest.java new file mode 100644 index 000000000..11225805a --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPNumericTest.java @@ -0,0 +1,125 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.tvp; + +import java.sql.SQLException; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerDataTable; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.DBConnection; +import com.microsoft.sqlserver.testframework.DBResultSet; +import com.microsoft.sqlserver.testframework.DBStatement; + +@RunWith(JUnitPlatform.class) +public class TVPNumericTest extends AbstractTest { + + private static DBConnection conn = null; + static DBStatement stmt = null; + static DBResultSet rs = null; + static SQLServerDataTable tvp = null; + static String expectecValue1 = "hello"; + static String expectecValue2 = "world"; + static String expectecValue3 = "again"; + private static String tvpName = "numericTVP"; + private static String charTable = "tvpNumericTable"; + private static String procedureName = "procedureThatCallsTVP"; + + /** + * Test a previous failure regarding to numeric precision. Issue #211 + * + * @throws SQLServerException + */ + @Test + public void testNumericPresicionIssue211() throws SQLServerException { + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", java.sql.Types.NUMERIC); + + tvp.addRow(12.12); + tvp.addRow(1.123); + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection + .prepareStatement("INSERT INTO " + charTable + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + + pstmt.execute(); + + if (null != pstmt) { + pstmt.close(); + } + } + + @BeforeEach + private void testSetup() throws SQLException { + conn = new DBConnection(connectionString); + stmt = conn.createStatement(); + + dropProcedure(); + dropTables(); + dropTVPS(); + + createTVPS(); + createTables(); + createPreocedure(); + } + + private void dropProcedure() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + procedureName + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + + " DROP PROCEDURE " + procedureName; + stmt.execute(sql); + } + + private static void dropTables() throws SQLException { + stmt.executeUpdate("if object_id('" + charTable + "','U') is not null" + " drop table " + charTable); + } + + private static void dropTVPS() throws SQLException { + stmt.executeUpdate("IF EXISTS (SELECT * FROM sys.types WHERE is_table_type = 1 AND name = '" + tvpName + "') " + " drop type " + tvpName); + } + + private static void createPreocedure() throws SQLException { + String sql = "CREATE PROCEDURE " + procedureName + " @InputData " + tvpName + " READONLY " + " AS " + " BEGIN " + " INSERT INTO " + charTable + + " SELECT * FROM @InputData" + " END"; + + stmt.execute(sql); + } + + private void createTables() throws SQLException { + String sql = "create table " + charTable + " (c1 numeric(6,3) null);"; + stmt.execute(sql); + } + + private void createTVPS() throws SQLException { + String TVPCreateCmd = "CREATE TYPE " + tvpName + " as table (c1 numeric(6,3) null)"; + stmt.executeUpdate(TVPCreateCmd); + } + + @AfterEach + private void terminateVariation() throws SQLException { + if (null != conn) { + conn.close(); + } + if (null != stmt) { + stmt.close(); + } + if (null != rs) { + rs.close(); + } + if (null != tvp) { + tvp.clear(); + } + } + +} \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPResultSetCursorTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPResultSetCursorTest.java new file mode 100644 index 000000000..1374dc5c9 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPResultSetCursorTest.java @@ -0,0 +1,424 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.tvp; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Properties; +import java.util.TimeZone; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerCallableStatement; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; + +@RunWith(JUnitPlatform.class) +public class TVPResultSetCursorTest extends AbstractTest { + + private static Connection conn = null; + static Statement stmt = null; + + static BigDecimal[] expectedBigDecimals = {new BigDecimal("12345.12345"), new BigDecimal("125.123"), new BigDecimal("45.12345")}; + static String[] expectedBigDecimalStrings = {"12345.12345", "125.12300", "45.12345"}; + + static String[] expectedStrings = {"hello", "world", "!!!"}; + + static Timestamp[] expectedTimestamps = {new Timestamp(1433338533461L), new Timestamp(14917485583999L), new Timestamp(1491123533000L)}; + static String[] expectedTimestampStrings = {"2015-06-03 13:35:33.4610000", "2442-09-19 01:59:43.9990000", "2017-04-02 08:58:53.0000000"}; + + private static String tvpName = "TVPResultSetCursorTest_TVP"; + private static String procedureName = "TVPResultSetCursorTest_SP"; + private static String srcTable = "TVPResultSetCursorTest_SourceTable"; + private static String desTable = "TVPResultSetCursorTest_DestinationTable"; + + /** + * Test a previous failure when using server cursor and using the same connection to create TVP and result set. + * + * @throws SQLException + */ + @Test + public void testServerCursors() throws SQLException { + serverCursorsTest(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + serverCursorsTest(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE); + serverCursorsTest(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); + serverCursorsTest(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); + } + + private void serverCursorsTest(int resultSetType, + int resultSetConcurrency) throws SQLException { + conn = DriverManager.getConnection(connectionString); + stmt = conn.createStatement(); + + dropTVPS(); + dropTables(); + + createTVPS(); + createTables(); + + populateSourceTable(); + + ResultSet rs = conn.createStatement(resultSetType, resultSetConcurrency).executeQuery("select * from " + srcTable); + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) conn.prepareStatement("INSERT INTO " + desTable + " select * from ? ;"); + pstmt.setStructured(1, tvpName, rs); + pstmt.execute(); + + verifyDestinationTableData(expectedBigDecimals.length); + + if (null != pstmt) { + pstmt.close(); + } + if (null != rs) { + rs.close(); + } + } + + /** + * Test a previous failure when setting SelectMethod to cursor and using the same connection to create TVP and result set. + * + * @throws SQLException + */ + @Test + public void testSelectMethodSetToCursor() throws SQLException { + Properties info = new Properties(); + info.setProperty("SelectMethod", "cursor"); + conn = DriverManager.getConnection(connectionString, info); + + stmt = conn.createStatement(); + + dropTVPS(); + dropTables(); + + createTVPS(); + createTables(); + + populateSourceTable(); + + ResultSet rs = conn.createStatement().executeQuery("select * from " + srcTable); + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) conn.prepareStatement("INSERT INTO " + desTable + " select * from ? ;"); + pstmt.setStructured(1, tvpName, rs); + pstmt.execute(); + + verifyDestinationTableData(expectedBigDecimals.length); + + if (null != pstmt) { + pstmt.close(); + } + if (null != rs) { + rs.close(); + } + } + + /** + * Test a previous failure when setting SelectMethod to cursor and using the same connection to create TVP, SP and result set. + * + * @throws SQLException + */ + @Test + public void testSelectMethodSetToCursorWithSP() throws SQLException { + Properties info = new Properties(); + info.setProperty("SelectMethod", "cursor"); + conn = DriverManager.getConnection(connectionString, info); + + stmt = conn.createStatement(); + + dropProcedure(); + dropTVPS(); + dropTables(); + + createTVPS(); + createTables(); + createPreocedure(); + + populateSourceTable(); + + ResultSet rs = conn.createStatement().executeQuery("select * from " + srcTable); + + final String sql = "{call " + procedureName + "(?)}"; + SQLServerCallableStatement pstmt = (SQLServerCallableStatement) conn.prepareCall(sql); + pstmt.setStructured(1, tvpName, rs); + + try { + pstmt.execute(); + + verifyDestinationTableData(expectedBigDecimals.length); + } + finally { + if (null != pstmt) { + pstmt.close(); + } + if (null != rs) { + rs.close(); + } + + dropProcedure(); + } + } + + /** + * Test exception when giving invalid TVP name + * + * @throws SQLException + */ + @Test + public void testInvalidTVPName() throws SQLException { + Properties info = new Properties(); + info.setProperty("SelectMethod", "cursor"); + conn = DriverManager.getConnection(connectionString, info); + + stmt = conn.createStatement(); + + dropTVPS(); + dropTables(); + + createTVPS(); + createTables(); + + populateSourceTable(); + + ResultSet rs = conn.createStatement().executeQuery("select * from " + srcTable); + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) conn.prepareStatement("INSERT INTO " + desTable + " select * from ? ;"); + pstmt.setStructured(1, "invalid" + tvpName, rs); + + try { + pstmt.execute(); + } + catch (SQLServerException e) { + if (!e.getMessage().contains("Cannot find data type")) { + throw e; + } + } + finally { + if (null != pstmt) { + pstmt.close(); + } + if (null != rs) { + rs.close(); + } + } + } + + /** + * Test exception when giving invalid stored procedure name + * + * @throws SQLException + */ + @Test + public void testInvalidStoredProcedureName() throws SQLException { + Properties info = new Properties(); + info.setProperty("SelectMethod", "cursor"); + conn = DriverManager.getConnection(connectionString, info); + + stmt = conn.createStatement(); + + dropProcedure(); + dropTVPS(); + dropTables(); + + createTVPS(); + createTables(); + createPreocedure(); + + populateSourceTable(); + + ResultSet rs = conn.createStatement().executeQuery("select * from " + srcTable); + + final String sql = "{call invalid" + procedureName + "(?)}"; + SQLServerCallableStatement pstmt = (SQLServerCallableStatement) conn.prepareCall(sql); + pstmt.setStructured(1, tvpName, rs); + + try { + pstmt.execute(); + } + catch (SQLServerException e) { + if (!e.getMessage().contains("Could not find stored procedure")) { + throw e; + } + } + finally { + + if (null != pstmt) { + pstmt.close(); + } + if (null != rs) { + rs.close(); + } + + dropProcedure(); + } + } + + /** + * test with multiple prepared statements and result sets + * + * @throws SQLException + */ + @Test + public void testMultiplePreparedStatementAndResultSet() throws SQLException { + conn = DriverManager.getConnection(connectionString); + + stmt = conn.createStatement(); + + dropTVPS(); + dropTables(); + + createTVPS(); + createTables(); + + populateSourceTable(); + + ResultSet rs = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE).executeQuery("select * from " + srcTable); + + SQLServerPreparedStatement pstmt1 = (SQLServerPreparedStatement) conn.prepareStatement("INSERT INTO " + desTable + " select * from ? ;"); + pstmt1.setStructured(1, tvpName, rs); + pstmt1.execute(); + verifyDestinationTableData(expectedBigDecimals.length); + + rs.beforeFirst(); + pstmt1 = (SQLServerPreparedStatement) conn.prepareStatement("INSERT INTO " + desTable + " select * from ? ;"); + pstmt1.setStructured(1, tvpName, rs); + pstmt1.execute(); + verifyDestinationTableData(expectedBigDecimals.length * 2); + + rs.beforeFirst(); + SQLServerPreparedStatement pstmt2 = (SQLServerPreparedStatement) conn.prepareStatement("INSERT INTO " + desTable + " select * from ? ;"); + pstmt2.setStructured(1, tvpName, rs); + pstmt2.execute(); + verifyDestinationTableData(expectedBigDecimals.length * 3); + + String sql = "insert into " + desTable + " values (?,?,?,?)"; + Calendar calGMT = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + pstmt1 = (SQLServerPreparedStatement) conn.prepareStatement(sql); + for (int i = 0; i < expectedBigDecimals.length; i++) { + pstmt1.setBigDecimal(1, expectedBigDecimals[i]); + pstmt1.setString(2, expectedStrings[i]); + pstmt1.setTimestamp(3, expectedTimestamps[i], calGMT); + pstmt1.setString(4, expectedStrings[i]); + pstmt1.execute(); + } + verifyDestinationTableData(expectedBigDecimals.length * 4); + + ResultSet rs2 = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE).executeQuery("select * from " + srcTable); + + pstmt1 = (SQLServerPreparedStatement) conn.prepareStatement("INSERT INTO " + desTable + " select * from ? ;"); + pstmt1.setStructured(1, tvpName, rs2); + pstmt1.execute(); + verifyDestinationTableData(expectedBigDecimals.length * 5); + + if (null != pstmt1) { + pstmt1.close(); + } + if (null != pstmt2) { + pstmt2.close(); + } + if (null != rs) { + rs.close(); + } + if (null != rs2) { + rs2.close(); + } + } + + private static void verifyDestinationTableData(int expectedNumberOfRows) throws SQLException { + ResultSet rs = conn.createStatement().executeQuery("select * from " + desTable); + + int expectedArrayLength = expectedBigDecimals.length; + + int i = 0; + while (rs.next()) { + assertTrue(rs.getString(1).equals(expectedBigDecimalStrings[i % expectedArrayLength]), + "Expected Value:" + expectedBigDecimalStrings[i % expectedArrayLength] + ", Actual Value: " + rs.getString(1)); + assertTrue(rs.getString(2).trim().equals(expectedStrings[i % expectedArrayLength]), + "Expected Value:" + expectedStrings[i % expectedArrayLength] + ", Actual Value: " + rs.getString(2)); + assertTrue(rs.getString(3).equals(expectedTimestampStrings[i % expectedArrayLength]), + "Expected Value:" + expectedTimestampStrings[i % expectedArrayLength] + ", Actual Value: " + rs.getString(3)); + assertTrue(rs.getString(4).trim().equals(expectedStrings[i % expectedArrayLength]), + "Expected Value:" + expectedStrings[i % expectedArrayLength] + ", Actual Value: " + rs.getString(4)); + i++; + } + + assertTrue(i == expectedNumberOfRows); + } + + private static void populateSourceTable() throws SQLException { + String sql = "insert into " + srcTable + " values (?,?,?,?)"; + + Calendar calGMT = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) conn.prepareStatement(sql); + + for (int i = 0; i < expectedBigDecimals.length; i++) { + pstmt.setBigDecimal(1, expectedBigDecimals[i]); + pstmt.setString(2, expectedStrings[i]); + pstmt.setTimestamp(3, expectedTimestamps[i], calGMT); + pstmt.setString(4, expectedStrings[i]); + pstmt.execute(); + } + } + + private static void dropTables() throws SQLException { + Utils.dropTableIfExists(srcTable, stmt); + Utils.dropTableIfExists(desTable, stmt); + } + + private static void createTables() throws SQLException { + String sql = "create table " + srcTable + " (c1 decimal(10,5) null, c2 nchar(50) null, c3 datetime2(7) null, c4 char(7000));"; + stmt.execute(sql); + + sql = "create table " + desTable + " (c1 decimal(10,5) null, c2 nchar(50) null, c3 datetime2(7) null, c4 char(7000));"; + stmt.execute(sql); + } + + private static void createTVPS() throws SQLException { + String TVPCreateCmd = "CREATE TYPE " + tvpName + + " as table (c1 decimal(10,5) null, c2 nchar(50) null, c3 datetime2(7) null, c4 char(7000) null)"; + stmt.execute(TVPCreateCmd); + } + + private static void dropTVPS() throws SQLException { + stmt.execute("IF EXISTS (SELECT * FROM sys.types WHERE is_table_type = 1 AND name = '" + tvpName + "') " + " drop type " + tvpName); + } + + private static void dropProcedure() throws SQLException { + Utils.dropProcedureIfExists(procedureName, stmt); + } + + private static void createPreocedure() throws SQLException { + String sql = "CREATE PROCEDURE " + procedureName + " @InputData " + tvpName + " READONLY " + " AS " + " BEGIN " + " INSERT INTO " + desTable + + " SELECT * FROM @InputData" + " END"; + + stmt.execute(sql); + } + + @AfterEach + private void terminateVariation() throws SQLException { + if (null != conn) { + conn.close(); + } + if (null != stmt) { + stmt.close(); + } + } + +} \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPSchemaTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPSchemaTest.java index 517e9985c..7182646b3 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPSchemaTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPSchemaTest.java @@ -49,8 +49,8 @@ public class TVPSchemaTest extends AbstractTest { * @throws SQLException */ @Test - @DisplayName("TVPSchema_PreparedStatement_StoredProcedure()") - public void testTVPSchema_PreparedStatement_StoredProcedure() throws SQLException { + @DisplayName("TVPSchemaPreparedStatementStoredProcedure()") + public void testTVPSchemaPreparedStatementStoredProcedure() throws SQLException { final String sql = "{call " + procedureName + "(?)}"; @@ -72,8 +72,8 @@ public void testTVPSchema_PreparedStatement_StoredProcedure() throws SQLExceptio * @throws SQLException */ @Test - @DisplayName("TVPSchema_CallableStatement_StoredProcedure()") - public void testTVPSchema_CallableStatement_StoredProcedure() throws SQLException { + @DisplayName("TVPSchemaCallableStatementStoredProcedure()") + public void testTVPSchemaCallableStatementStoredProcedure() throws SQLException { final String sql = "{call " + procedureName + "(?)}"; @@ -96,8 +96,8 @@ public void testTVPSchema_CallableStatement_StoredProcedure() throws SQLExceptio * @throws IOException */ @Test - @DisplayName("TVPSchema_Prepared_InsertCommand") - public void testTVPSchema_Prepared_InsertCommand() throws SQLException, IOException { + @DisplayName("TVPSchemaPreparedInsertCommand") + public void testTVPSchemaPreparedInsertCommand() throws SQLException, IOException { SQLServerPreparedStatement P_C_stmt = (SQLServerPreparedStatement) connection .prepareStatement("INSERT INTO " + charTable + " select * from ? ;"); @@ -119,8 +119,8 @@ public void testTVPSchema_Prepared_InsertCommand() throws SQLException, IOExcept * @throws IOException */ @Test - @DisplayName("TVPSchema_Callable_InsertCommand()") - public void testTVPSchema_Callable_InsertCommand() throws SQLException, IOException { + @DisplayName("TVPSchemaCallableInsertCommand()") + public void testTVPSchemaCallableInsertCommand() throws SQLException, IOException { SQLServerCallableStatement P_C_stmt = (SQLServerCallableStatement) connection.prepareCall("INSERT INTO " + charTable + " select * from ? ;"); P_C_stmt.setStructured(1, tvpNameWithSchema, tvp); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java new file mode 100644 index 000000000..5d4ec875c --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java @@ -0,0 +1,589 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.tvp; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerCallableStatement; +import com.microsoft.sqlserver.jdbc.SQLServerDataTable; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.jdbc.SQLServerResultSet; +import com.microsoft.sqlserver.testframework.AbstractTest; + +@RunWith(JUnitPlatform.class) +public class TVPTypesTest extends AbstractTest { + + private static Connection conn = null; + static Statement stmt = null; + static ResultSet rs = null; + static SQLServerDataTable tvp = null; + private static String tvpName = "TVP"; + private static String table = "TVPTable"; + private static String procedureName = "procedureThatCallsTVP"; + private String value = null; + + /** + * Test a longvarchar support + * + * @throws SQLException + */ + @Test + public void testLongVarchar() throws SQLException { + createTables("varchar(max)"); + createTVPS("varchar(max)"); + + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < 9000; i++) + buffer.append("a"); + + value = buffer.toString(); + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", java.sql.Types.LONGVARCHAR); + tvp.addRow(value); + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection + .prepareStatement("INSERT INTO " + table + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + + pstmt.execute(); + + rs = conn.createStatement().executeQuery("select * from " + table); + while (rs.next()) { + assertEquals(rs.getString(1), value); + } + if (null != pstmt) { + pstmt.close(); + } + } + + /** + * Test longnvarchar + * + * @throws SQLException + */ + @Test + public void testLongNVarchar() throws SQLException { + createTables("nvarchar(max)"); + createTVPS("nvarchar(max)"); + + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < 8001; i++) + buffer.append("سس"); + + value = buffer.toString(); + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", java.sql.Types.LONGNVARCHAR); + tvp.addRow(value); + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection + .prepareStatement("INSERT INTO " + table + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + + pstmt.execute(); + + rs = conn.createStatement().executeQuery("select * from " + table); + while (rs.next()) { + assertEquals(rs.getString(1), value); + } + + if (null != pstmt) { + pstmt.close(); + } + } + + /** + * Test xml support + * + * @throws SQLException + */ + @Test + public void testXML() throws SQLException { + createTables("xml"); + createTVPS("xml"); + value = "Variable E" + "Variable F" + "API" + + "The following are Japanese chars." + + " Some UTF-8 encoded characters: �������"; + + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", java.sql.Types.SQLXML); + tvp.addRow(value); + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection + .prepareStatement("INSERT INTO " + table + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + + pstmt.execute(); + + Connection con = DriverManager.getConnection(connectionString); + ResultSet rs = con.createStatement().executeQuery("select * from " + table); + while (rs.next()) + assertEquals(rs.getString(1), value); + + if (null != pstmt) { + pstmt.close(); + } + } + + /** + * Test ntext support + * + * @throws SQLException + */ + @Test + public void testnText() throws SQLException { + createTables("ntext"); + createTVPS("ntext"); + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < 9000; i++) + buffer.append("س"); + value = buffer.toString(); + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", java.sql.Types.LONGNVARCHAR); + tvp.addRow(value); + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection + .prepareStatement("INSERT INTO " + table + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + + pstmt.execute(); + + Connection con = DriverManager.getConnection(connectionString); + ResultSet rs = con.createStatement().executeQuery("select * from " + table); + while (rs.next()) + assertEquals(rs.getString(1), value); + + if (null != pstmt) { + pstmt.close(); + } + } + + /** + * Test text support + * + * @throws SQLException + */ + @Test + public void testText() throws SQLException { + createTables("text"); + createTVPS("text"); + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < 9000; i++) + buffer.append("a"); + value = buffer.toString(); + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", java.sql.Types.LONGVARCHAR); + tvp.addRow(value); + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection + .prepareStatement("INSERT INTO " + table + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + + pstmt.execute(); + + Connection con = DriverManager.getConnection(connectionString); + ResultSet rs = con.createStatement().executeQuery("select * from " + table); + while (rs.next()) + assertEquals(rs.getString(1), value); + + if (null != pstmt) { + pstmt.close(); + } + } + + /** + * Test text support + * + * @throws SQLException + */ + @Test + public void testImage() throws SQLException { + createTables("varbinary(max)"); + createTVPS("varbinary(max)"); + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < 10000; i++) + buffer.append("a"); + value = buffer.toString(); + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", java.sql.Types.LONGVARBINARY); + tvp.addRow(value.getBytes()); + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection + .prepareStatement("INSERT INTO " + table + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + + pstmt.execute(); + + Connection con = DriverManager.getConnection(connectionString); + ResultSet rs = con.createStatement().executeQuery("select * from " + table); + + while (rs.next()) + assertTrue(parseByte(rs.getBytes(1), value.getBytes())); + + if (null != pstmt) { + pstmt.close(); + } + } + + /** + * LongVarchar with StoredProcedure + * + * @throws SQLException + */ + @Test + public void testTVPLongVarcharStoredProcedure() throws SQLException { + createTables("varchar(max)"); + createTVPS("varchar(max)"); + createPreocedure(); + + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < 8001; i++) + buffer.append("a"); + + value = buffer.toString(); + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", java.sql.Types.LONGVARCHAR); + tvp.addRow(value); + + final String sql = "{call " + procedureName + "(?)}"; + + SQLServerCallableStatement P_C_statement = (SQLServerCallableStatement) connection.prepareCall(sql); + P_C_statement.setStructured(1, tvpName, tvp); + P_C_statement.execute(); + + rs = stmt.executeQuery("select * from " + table); + while (rs.next()) + assertEquals(rs.getString(1), value); + + if (null != P_C_statement) { + P_C_statement.close(); + } + } + + /** + * LongNVarchar with StoredProcedure + * + * @throws SQLException + */ + @Test + public void testTVPLongNVarcharStoredProcedure() throws SQLException { + createTables("nvarchar(max)"); + createTVPS("nvarchar(max)"); + createPreocedure(); + + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < 8001; i++) + buffer.append("سس"); + value = buffer.toString(); + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", java.sql.Types.LONGNVARCHAR); + tvp.addRow(buffer.toString()); + + final String sql = "{call " + procedureName + "(?)}"; + + SQLServerCallableStatement P_C_statement = (SQLServerCallableStatement) connection.prepareCall(sql); + P_C_statement.setStructured(1, tvpName, tvp); + P_C_statement.execute(); + + rs = stmt.executeQuery("select * from " + table); + while (rs.next()) + assertEquals(rs.getString(1), value); + + if (null != P_C_statement) { + P_C_statement.close(); + } + } + + /** + * XML with StoredProcedure + * + * @throws SQLException + */ + @Test + public void testTVPXMLStoredProcedure() throws SQLException { + createTables("xml"); + createTVPS("xml"); + createPreocedure(); + + value = "Variable E" + "Variable F" + "API" + + "The following are Japanese chars." + + " Some UTF-8 encoded characters: �������"; + + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", java.sql.Types.SQLXML); + tvp.addRow(value); + + final String sql = "{call " + procedureName + "(?)}"; + + SQLServerCallableStatement P_C_statement = (SQLServerCallableStatement) connection.prepareCall(sql); + P_C_statement.setStructured(1, tvpName, tvp); + P_C_statement.execute(); + + rs = stmt.executeQuery("select * from " + table); + while (rs.next()) + assertEquals(rs.getString(1), value); + if (null != P_C_statement) { + P_C_statement.close(); + } + } + + /** + * Text with StoredProcedure + * + * @throws SQLException + */ + @Test + public void testTVPTextStoredProcedure() throws SQLException { + createTables("text"); + createTVPS("text"); + createPreocedure(); + + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < 9000; i++) + buffer.append("a"); + value = buffer.toString(); + + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", java.sql.Types.LONGVARCHAR); + tvp.addRow(value); + + final String sql = "{call " + procedureName + "(?)}"; + + SQLServerCallableStatement P_C_statement = (SQLServerCallableStatement) connection.prepareCall(sql); + P_C_statement.setStructured(1, tvpName, tvp); + P_C_statement.execute(); + + rs = stmt.executeQuery("select * from " + table); + while (rs.next()) + assertEquals(rs.getString(1), value); + if (null != P_C_statement) { + P_C_statement.close(); + } + } + + /** + * Text with StoredProcedure + * + * @throws SQLException + */ + @Test + public void testTVPNTextStoredProcedure() throws SQLException { + createTables("ntext"); + createTVPS("ntext"); + createPreocedure(); + + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < 9000; i++) + buffer.append("س"); + value = buffer.toString(); + + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", java.sql.Types.LONGNVARCHAR); + tvp.addRow(value); + + final String sql = "{call " + procedureName + "(?)}"; + + SQLServerCallableStatement P_C_statement = (SQLServerCallableStatement) connection.prepareCall(sql); + P_C_statement.setStructured(1, tvpName, tvp); + P_C_statement.execute(); + + rs = stmt.executeQuery("select * from " + table); + while (rs.next()) + assertEquals(rs.getString(1), value); + if (null != P_C_statement) { + P_C_statement.close(); + } + } + + /** + * Image with StoredProcedure acts the same as varbinary(max) + * + * @throws SQLException + */ + @Test + public void testTVPImageStoredProcedure() throws SQLException { + createTables("image"); + createTVPS("image"); + createPreocedure(); + + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < 9000; i++) + buffer.append("a"); + value = buffer.toString(); + + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", java.sql.Types.LONGVARBINARY); + tvp.addRow(value.getBytes()); + + final String sql = "{call " + procedureName + "(?)}"; + + SQLServerCallableStatement P_C_statement = (SQLServerCallableStatement) connection.prepareCall(sql); + P_C_statement.setStructured(1, tvpName, tvp); + P_C_statement.execute(); + + rs = stmt.executeQuery("select * from " + table); + while (rs.next()) + assertTrue(parseByte(rs.getBytes(1), value.getBytes())); + if (null != P_C_statement) { + P_C_statement.close(); + } + } + + /** + * Test a datetime support + * + * @throws SQLException + */ + @Test + public void testDateTime() throws SQLException { + createTables("datetime"); + createTVPS("datetime"); + + java.sql.Timestamp value = java.sql.Timestamp.valueOf("2007-09-23 10:10:10.123"); + + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.DATETIME); + tvp.addRow(value); + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection + .prepareStatement("INSERT INTO " + table + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + + pstmt.execute(); + + rs = conn.createStatement().executeQuery("select * from " + table); + while (rs.next()) { + assertEquals(((SQLServerResultSet) rs).getDateTime(1), value); + } + if (null != pstmt) { + pstmt.close(); + } + } + + /** + * Test a smalldatetime support + * + * @throws SQLException + */ + @Test + public void testSmallDateTime() throws SQLException { + createTables("smalldatetime"); + createTVPS("smalldatetime"); + + java.sql.Timestamp value = java.sql.Timestamp.valueOf("2007-09-23 10:10:10.123"); + java.sql.Timestamp returnValue = java.sql.Timestamp.valueOf("2007-09-23 10:10:00.0"); + + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.SMALLDATETIME); + tvp.addRow(value); + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection + .prepareStatement("INSERT INTO " + table + " select * from ? ;"); + pstmt.setStructured(1, tvpName, tvp); + + pstmt.execute(); + + rs = conn.createStatement().executeQuery("select * from " + table); + while (rs.next()) { + assertEquals(((SQLServerResultSet) rs).getSmallDateTime(1), returnValue); + } + if (null != pstmt) { + pstmt.close(); + } + } + + @BeforeEach + private void testSetup() throws SQLException { + conn = DriverManager.getConnection(connectionString); + stmt = conn.createStatement(); + + dropProcedure(); + dropTables(); + dropTVPS(); + } + + @AfterAll + public static void terminate() throws SQLException { + conn = DriverManager.getConnection(connectionString); + stmt = conn.createStatement(); + dropProcedure(); + dropTables(); + dropTVPS(); + } + + private static void dropProcedure() throws SQLException { + String sql = " IF EXISTS (select * from sysobjects where id = object_id(N'" + procedureName + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + + " DROP PROCEDURE " + procedureName; + stmt.execute(sql); + } + + private static void dropTables() throws SQLException { + stmt.executeUpdate("if object_id('" + table + "','U') is not null" + " drop table " + table); + } + + private static void dropTVPS() throws SQLException { + stmt.executeUpdate("IF EXISTS (SELECT * FROM sys.types WHERE is_table_type = 1 AND name = '" + tvpName + "') " + " drop type " + tvpName); + } + + private static void createPreocedure() throws SQLException { + String sql = "CREATE PROCEDURE " + procedureName + " @InputData " + tvpName + " READONLY " + " AS " + " BEGIN " + " INSERT INTO " + table + + " SELECT * FROM @InputData" + " END"; + + stmt.execute(sql); + } + + private void createTables(String colType) throws SQLException { + String sql = "create table " + table + " (c1 " + colType + " null);"; + stmt.execute(sql); + } + + private void createTVPS(String colType) throws SQLException { + String TVPCreateCmd = "CREATE TYPE " + tvpName + " as table (c1 " + colType + " null)"; + stmt.executeUpdate(TVPCreateCmd); + } + + private boolean parseByte(byte[] expectedData, + byte[] retrieved) { + assertTrue(Arrays.equals(expectedData, Arrays.copyOf(retrieved, expectedData.length)), " unexpected BINARY value, expected"); + for (int i = expectedData.length; i < retrieved.length; i++) { + assertTrue(0 == retrieved[i], "unexpected data BINARY"); + } + return true; + } + + @AfterEach + private void terminateVariation() throws SQLException { + if (null != conn) { + conn.close(); + } + if (null != stmt) { + stmt.close(); + } + if (null != rs) { + rs.close(); + } + if (null != tvp) { + tvp.clear(); + } + } + +} \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/TestSavepoint.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/TestSavepoint.java new file mode 100644 index 000000000..8670ca769 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/TestSavepoint.java @@ -0,0 +1,126 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.unit; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.SQLServerSavepoint; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.util.RandomUtil; + +/** + * Unit test case for Creating SavePoint. + */ +@RunWith(JUnitPlatform.class) +public class TestSavepoint extends AbstractTest { + + Connection connection = null; + Statement statement = null; + String savePointName = RandomUtil.getIdentifier("SavePoint", 31, true, false); + + /** + * Testing SavePoint with name. + */ + @Test + public void testSavePointName() throws SQLException { + connection = DriverManager.getConnection(connectionString); + + connection.setAutoCommit(false); + + SQLServerSavepoint savePoint = (SQLServerSavepoint) connection.setSavepoint(savePointName); + assertTrue(savePointName.equals(savePoint.getSavepointName()), "Savepoint Name should be same."); + + assertTrue(savePointName.equals(savePoint.getLabel()), "Savepoint Label should be same as Savepoint Name."); + + assertTrue(savePoint.isNamed(), "SQLServerSavepoint.isNamed should be true"); + try { + savePoint.getSavepointId(); + assertTrue(false, "Expecting Exception as trying to get SavePointId when we created savepoint with name"); + } + catch (SQLServerException e) { + } + + connection.rollback(); + } + + /** + * Testing SavePoint without name. + * + * @throws SQLException + */ + @Test + public void testSavePointId() throws SQLException { + connection = DriverManager.getConnection(connectionString); + + connection.setAutoCommit(false); + + SQLServerSavepoint savePoint = (SQLServerSavepoint) connection.setSavepoint(null); + assertNotNull(savePoint.getLabel(), "Savepoint Label should not be null."); + + try { + savePoint.getSavepointName(); + assertTrue(false, "Expecting Exception as trying to get SavePointname when we created savepoint without name"); + } + catch (SQLServerException e) { + } + + assertTrue(savePoint.getSavepointId() != 0, "SavePoint should not be 0"); + connection.rollback(); + } + + /** + * Testing SavePoint without name. + * + * @throws SQLException + */ + @Test + public void testSavePointIsNamed() throws SQLException { + connection = DriverManager.getConnection(connectionString); + + connection.setAutoCommit(false); + + SQLServerSavepoint savePoint = (SQLServerSavepoint) connection.setSavepoint(null); + + assertFalse(savePoint.isNamed(), "SQLServerSavepoint.isNamed should be false as savePoint is created without name"); + + connection.rollback(); + } + + /** + * Test SavePoint when auto commit is true. + * + * @throws SQLException + */ + @Test + public void testSavePointWithAutoCommit() throws SQLException { + connection = DriverManager.getConnection(connectionString); + + connection.setAutoCommit(true); + + try { + connection.setSavepoint(null); + assertTrue(false, "Expecting Exception as can not set SetPoint when AutoCommit mode is set to true."); + } + catch (SQLServerException e) { + } + + } + +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/lobs/lobsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/lobs/lobsTest.java index f0b4c6576..af9499fa9 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/lobs/lobsTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/lobs/lobsTest.java @@ -101,10 +101,10 @@ public Collection executeDynamicTests() { List isResultSetTypes = new ArrayList<>(Arrays.asList(true, false)); Collection dynamicTests = new ArrayList<>(); - for (int i = 0; i < classes.size(); i++) { - for (int j = 0; j < isResultSetTypes.size(); j++) { - final Class lobClass = classes.get(i); - final boolean isResultSet = isResultSetTypes.get(j); + for (Class aClass : classes) { + for (Boolean isResultSetType : isResultSetTypes) { + final Class lobClass = aClass; + final boolean isResultSet = isResultSetType; Executable exec = new Executable() { @Override public void execute() throws Throwable { @@ -285,10 +285,10 @@ private void testMultipleClose(Class streamClass) throws Exception { * @throws Exception */ @Test - @DisplayName("testlLobs_InsertRetrive") + @DisplayName("testlLobsInsertRetrive") public void testNClob() throws Exception { String types[] = {"nvarchar(max)"}; - testLobs_InsertRetrive(types, NClob.class); + testLobsInsertRetrive(types, NClob.class); } /** @@ -297,10 +297,10 @@ public void testNClob() throws Exception { * @throws Exception */ @Test - @DisplayName("testlLobs_InsertRetrive") + @DisplayName("testlLobsInsertRetrive") public void testBlob() throws Exception { String types[] = {"varbinary(max)"}; - testLobs_InsertRetrive(types, Blob.class); + testLobsInsertRetrive(types, Blob.class); } /** @@ -309,13 +309,13 @@ public void testBlob() throws Exception { * @throws Exception */ @Test - @DisplayName("testlLobs_InsertRetrive") + @DisplayName("testlLobsInsertRetrive") public void testClob() throws Exception { String types[] = {"varchar(max)"}; - testLobs_InsertRetrive(types, Clob.class); + testLobsInsertRetrive(types, Clob.class); } - private void testLobs_InsertRetrive(String types[], + private void testLobsInsertRetrive(String types[], Class lobClass) throws Exception { table = createTable(table, types, false); // create empty table int size = 10000; @@ -365,8 +365,8 @@ else if (nClobType == classType(lobClass)) { } else if (nClobType == classType(lobClass)) { nclob = rs.getNClob(1); - stream = nclob.getAsciiStream(); assertEquals(nclob.length(), size); + stream = nclob.getAsciiStream(); BufferedInputStream is = new BufferedInputStream(stream); is.read(chunk); assertEquals(chunk.length, size); @@ -572,8 +572,8 @@ private static DBTable createTable(DBTable table, DBStatement stmt = new DBConnection(connectionString).createStatement(); table = new DBTable(false); - for (int i = 0; i < types.length; i++) { - SqlType type = Utils.find(types[i]); + for (String type1 : types) { + SqlType type = Utils.find(type1); table.addColumn(type); } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java index 697b96b52..dade10c4b 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java @@ -272,7 +272,7 @@ public void Repro47239() throws SQLException { */ @Test @DisplayName("Regression test for using 'large' methods") - public void Repro47239_large() throws Exception { + public void Repro47239large() throws Exception { assumeTrue("JDBC42".equals(Utils.getConfiguredProperty("JDBC_Version")), "Aborting test case as JDBC version is not compatible. "); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java new file mode 100644 index 000000000..85eb4a91b --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java @@ -0,0 +1,228 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.unit.statement; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.sql.BatchUpdateException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; +import org.opentest4j.TestAbortedException; + +import com.microsoft.sqlserver.jdbc.SQLServerStatement; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.DBConnection; +import com.microsoft.sqlserver.testframework.Utils; + +/** + * Tests batch execution with AE On connection + * + */ +@RunWith(JUnitPlatform.class) +public class BatchExecutionTest extends AbstractTest { + + static Statement stmt = null; + static Connection connection = null; + static PreparedStatement pstmt = null; + static PreparedStatement pstmt1 = null; + static ResultSet rs = null; + + /** + * testAddBatch1 and testExecutionBatch one looks similar except for the parameters being passed for select query. + * TODO: we should look and simply the test later by parameterized values + * @throws Exception + */ + @Test + public void testBatchExceptionAEOn() throws Exception { + testAddBatch1(); + testExecuteBatch1(); + } + + /** + * Get a PreparedStatement object and call the addBatch() method with 3 SQL statements and call the executeBatch() method and it should return + * array of Integer values of length 3 + */ + public void testAddBatch1() { + int i = 0; + int retValue[] = {0, 0, 0}; + try { + String sPrepStmt = "update ctstable2 set PRICE=PRICE*20 where TYPE_ID=?"; + pstmt = connection.prepareStatement(sPrepStmt); + pstmt.setInt(1, 2); + pstmt.addBatch(); + + pstmt.setInt(1, 3); + pstmt.addBatch(); + + pstmt.setInt(1, 4); + pstmt.addBatch(); + + int[] updateCount = pstmt.executeBatch(); + int updateCountlen = updateCount.length; + + assertTrue(updateCountlen == 3, "addBatch does not add the SQL Statements to Batch ,call to addBatch failed"); + + String sPrepStmt1 = "select count(*) from ctstable2 where TYPE_ID=?"; + + pstmt1 = connection.prepareStatement(sPrepStmt1); + + // 2 is the number that is set First for Type Id in Prepared Statement + for (int n = 2; n <= 4; n++) { + pstmt1.setInt(1, n); + rs = pstmt1.executeQuery(); + rs.next(); + retValue[i++] = rs.getInt(1); + } + + pstmt1.close(); + + for (int j = 0; j < updateCount.length; j++) { + + if (updateCount[j] != retValue[j] && updateCount[j] != Statement.SUCCESS_NO_INFO) { + fail("affected row count does not match with the updateCount value, Call to addBatch is Failed!"); + } + } + } + catch (BatchUpdateException b) { + fail("BatchUpdateException : Call to addBatch is Failed!"); + } + catch (SQLException sqle) { + fail("Call to addBatch is Failed!"); + } + catch (Exception e) { + fail("Call to addBatch is Failed!"); + } + } + + /** + * Get a PreparedStatement object and call the addBatch() method with a 3 valid SQL statements and call the executeBatch() method It should return + * an array of Integer values of length 3. + */ + public void testExecuteBatch1() { + int i = 0; + int retValue[] = {0, 0, 0}; + int updCountLength = 0; + try { + String sPrepStmt = "update ctstable2 set PRICE=PRICE*20 where TYPE_ID=?"; + + pstmt = connection.prepareStatement(sPrepStmt); + pstmt.setInt(1, 1); + pstmt.addBatch(); + + pstmt.setInt(1, 2); + pstmt.addBatch(); + + pstmt.setInt(1, 3); + pstmt.addBatch(); + + int[] updateCount = pstmt.executeBatch(); + updCountLength = updateCount.length; + + assertTrue(updCountLength == 3, "executeBatch does not execute the Batch of SQL statements, Call to executeBatch is Failed!"); + + String sPrepStmt1 = "select count(*) from ctstable2 where TYPE_ID=?"; + + pstmt1 = connection.prepareStatement(sPrepStmt1); + + for (int n = 1; n <= 3; n++) { + pstmt1.setInt(1, n); + rs = pstmt1.executeQuery(); + rs.next(); + retValue[i++] = rs.getInt(1); + } + + pstmt1.close(); + + for (int j = 0; j < updateCount.length; j++) { + if (updateCount[j] != retValue[j] && updateCount[j] != Statement.SUCCESS_NO_INFO) { + fail("executeBatch does not execute the Batch of SQL statements, Call to executeBatch is Failed!"); + } + } + } + catch (BatchUpdateException b) { + fail("BatchUpdateException : Call to executeBatch is Failed!"); + } + catch (SQLException sqle) { + fail("Call to executeBatch is Failed!"); + } + catch (Exception e) { + fail("Call to executeBatch is Failed!"); + } + } + + private static void createTable() throws SQLException { + String sql1 = "create table ctstable1 (TYPE_ID int, TYPE_DESC varchar(32), primary key(TYPE_ID)) "; + String sql2 = "create table ctstable2 (KEY_ID int, COF_NAME varchar(32), PRICE float, TYPE_ID int, primary key(KEY_ID), foreign key(TYPE_ID) references ctstable1) "; + stmt.execute(sql1); + stmt.execute(sql2); + + String sqlin2 = "insert into ctstable1 values (1,'COFFEE-Desc')"; + stmt.execute(sqlin2); + sqlin2 = "insert into ctstable1 values (2,'COFFEE-Desc2')"; + stmt.execute(sqlin2); + sqlin2 = "insert into ctstable1 values (3,'COFFEE-Desc3')"; + stmt.execute(sqlin2); + + String sqlin1 = "insert into ctstable2 values (9,'COFFEE-9',9.0, 1)"; + stmt.execute(sqlin1); + sqlin1 = "insert into ctstable2 values (10,'COFFEE-10',10.0, 2)"; + stmt.execute(sqlin1); + sqlin1 = "insert into ctstable2 values (11,'COFFEE-11',11.0, 3)"; + stmt.execute(sqlin1); + + } + + @BeforeAll + public static void testSetup() throws TestAbortedException, Exception { + assumeTrue(13 <= new DBConnection(connectionString).getServerVersion(), + "Aborting test case as SQL Server version is not compatible with Always encrypted "); + connection = DriverManager.getConnection(connectionString + ";columnEncryptionSetting=Enabled;"); + stmt = (SQLServerStatement) connection.createStatement(); + dropTable(); + createTable(); + } + + private static void dropTable() throws SQLException { + Utils.dropTableIfExists("ctstable2", stmt); + Utils.dropTableIfExists("ctstable1", stmt); + } + + @AfterAll + public static void terminateVariation() throws SQLException { + + dropTable(); + + if (null != connection) { + connection.close(); + } + if (null != pstmt) { + pstmt.close(); + } + if (null != pstmt1) { + pstmt1.close(); + } + if (null != stmt) { + stmt.close(); + } + if (null != rs) { + rs.close(); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchTriggerTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchTriggerTest.java new file mode 100644 index 000000000..1abcf471c --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchTriggerTest.java @@ -0,0 +1,161 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.unit.statement; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; +import org.opentest4j.TestAbortedException; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import com.microsoft.sqlserver.jdbc.SQLServerStatement; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; + +/** + * Tests batch execution with trigger exception + * + */ +@RunWith(JUnitPlatform.class) +public class BatchTriggerTest extends AbstractTest { + + static Statement stmt = null; + static Connection connection = null; + static String tableName = "triggerTable"; + static String triggerName = "triggerTest"; + static String customErrorMessage = "Custom error message, you should see me. col1 should be higher than 10"; + static String insertQuery = "insert into " + tableName + " (col1, col2, col3, col4) values (1, '22-08-2017 17:30:00.000', 'R4760', 31)"; + + /** + * Tests that the proper trigger exception is thrown using statement + * + * @throws SQLException + */ + @Test + public void statementTest() throws SQLException { + Statement stmt = null; + try { + stmt = connection.createStatement(); + stmt.addBatch(insertQuery); + stmt.executeBatch(); + fail("Trigger Exception not thrown"); + } + catch (Exception e) { + assertTrue(e.getMessage().equalsIgnoreCase(customErrorMessage)); + } + + finally { + if (stmt != null) { + stmt.close(); + } + } + } + + /** + * Tests that the proper trigger exception is thrown using preparedSatement + * + * @throws SQLException + */ + @Test + public void preparedStatementTest() throws SQLException { + PreparedStatement pstmt = null; + try { + pstmt = connection.prepareStatement(insertQuery); + pstmt.addBatch(); + pstmt.executeBatch(); + fail("Trigger Exception not thrown"); + } + catch (Exception e) { + + assertTrue(e.getMessage().equalsIgnoreCase(customErrorMessage)); + } + finally { + if (pstmt != null) { + pstmt.close(); + } + } + } + + /** + * Create the trigger + * + * @param triggerName + * @throws SQLException + */ + private static void createTrigger(String triggerName) throws SQLException { + String sql = "create trigger " + triggerName + " on " + tableName + " for insert " + "as " + "begin " + "if (select col1 from " + tableName + + ") > 10 " + "begin " + "return " + "end " + + "RAISERROR ('Custom error message, you should see me. col1 should be higher than 10', 16, 0) " + "rollback transaction " + "end"; + stmt.execute(sql); + } + + /** + * Creating tables + * + * @throws SQLException + */ + private static void createTable() throws SQLException { + String sql = "create table " + tableName + " ( col1 int, col2 varchar(50), col3 varchar(10), col4 int)"; + stmt.execute(sql); + } + + /** + * Setup test + * + * @throws TestAbortedException + * @throws Exception + */ + @BeforeAll + public static void testSetup() throws TestAbortedException, Exception { + connection = DriverManager.getConnection(connectionString); + stmt = (SQLServerStatement) connection.createStatement(); + stmt.execute("IF EXISTS (\r\n" + " SELECT *\r\n" + " FROM sys.objects\r\n" + " WHERE [type] = 'TR' AND [name] = '" + triggerName + + "'\r\n" + " )\r\n" + " DROP TRIGGER " + triggerName + ";"); + dropTable(); + createTable(); + createTrigger(triggerName); + } + + /** + * Drop the table + * + * @throws SQLException + */ + private static void dropTable() throws SQLException { + Utils.dropTableIfExists(tableName, stmt); + } + + /** + * Cleaning up + * + * @throws SQLException + */ + @AfterAll + public static void terminateVariation() throws SQLException { + dropTable(); + stmt.execute("IF EXISTS (\r\n" + " SELECT *\r\n" + " FROM sys.objects\r\n" + " WHERE [type] = 'TR' AND [name] = '" + triggerName + + "'\r\n" + " )\r\n" + " DROP TRIGGER " + triggerName + ";"); + + if (null != connection) { + connection.close(); + } + if (null != stmt) { + stmt.close(); + } + + } +} \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/CallableMixedTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/CallableMixedTest.java index 95f2962c2..e93f1c150 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/CallableMixedTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/CallableMixedTest.java @@ -22,6 +22,7 @@ import com.microsoft.sqlserver.testframework.AbstractSQLGenerator; import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; import com.microsoft.sqlserver.testframework.util.RandomUtil; /** @@ -31,7 +32,6 @@ @RunWith(JUnitPlatform.class) public class CallableMixedTest extends AbstractTest { Connection connection = null; - Statement statement = null; String tableN = RandomUtil.getIdentifier("TFOO3"); String procN = RandomUtil.getIdentifier("SPFOO3"); String tableName = AbstractSQLGenerator.escapeIdentifier(tableN); @@ -39,71 +39,62 @@ public class CallableMixedTest extends AbstractTest { /** * Tests Callable mix + * * @throws SQLException */ @Test @DisplayName("Test CallableMix") public void datatypesTest() throws SQLException { - connection = DriverManager.getConnection(connectionString); - statement = connection.createStatement(); + try (Connection connection = DriverManager.getConnection(connectionString); Statement statement = connection.createStatement();) { - try { - statement.executeUpdate("DROP TABLE " + tableName); - statement.executeUpdate(" DROP PROCEDURE " + procName); - } - catch (Exception e) { - } + statement.executeUpdate("create table " + tableName + " (c1_int int primary key, col2 int)"); + statement.executeUpdate("Insert into " + tableName + " values(0, 1)"); - statement.executeUpdate("create table " + tableName + " (c1_int int primary key, col2 int)"); - statement.executeUpdate("Insert into " + tableName + " values(0, 1)"); - statement.close(); - Statement stmt = connection.createStatement(); - stmt.executeUpdate("CREATE PROCEDURE " + procName - + " (@p2_int int, @p2_int_out int OUTPUT, @p4_smallint smallint, @p4_smallint_out smallint OUTPUT) AS begin transaction SELECT * FROM " - + tableName + " ; SELECT @p2_int_out=@p2_int, @p4_smallint_out=@p4_smallint commit transaction RETURN -2147483648"); - stmt.close(); + statement.executeUpdate("CREATE PROCEDURE " + procName + + " (@p2_int int, @p2_int_out int OUTPUT, @p4_smallint smallint, @p4_smallint_out smallint OUTPUT) AS begin transaction SELECT * FROM " + + tableName + " ; SELECT @p2_int_out=@p2_int, @p4_smallint_out=@p4_smallint commit transaction RETURN -2147483648"); - CallableStatement callableStatement = connection.prepareCall("{ ? = CALL " + procName + " (?, ?, ?, ?) }"); - callableStatement.registerOutParameter((int) 1, (int) 4); - callableStatement.setObject((int) 2, Integer.valueOf("31"), (int) 4); - callableStatement.registerOutParameter((int) 3, (int) 4); - callableStatement.registerOutParameter((int) 5, java.sql.Types.BINARY); - callableStatement.registerOutParameter((int) 5, (int) 5); - callableStatement.setObject((int) 4, Short.valueOf("-5372"), (int) 5); + try (CallableStatement callableStatement = connection.prepareCall("{ ? = CALL " + procName + " (?, ?, ?, ?) }")) { + callableStatement.registerOutParameter((int) 1, (int) 4); + callableStatement.setObject((int) 2, Integer.valueOf("31"), (int) 4); + callableStatement.registerOutParameter((int) 3, (int) 4); + callableStatement.registerOutParameter((int) 5, java.sql.Types.BINARY); + callableStatement.registerOutParameter((int) 5, (int) 5); + callableStatement.setObject((int) 4, Short.valueOf("-5372"), (int) 5); - // get results and a value - ResultSet rs = callableStatement.executeQuery(); - rs.next(); + // get results and a value + ResultSet rs = callableStatement.executeQuery(); + rs.next(); - assertEquals(rs.getInt(1), 0, "Received data not equal to setdata"); - assertEquals(callableStatement.getInt((int) 5), -5372, "Received data not equal to setdata"); + assertEquals(rs.getInt(1), 0, "Received data not equal to setdata"); + assertEquals(callableStatement.getInt((int) 5), -5372, "Received data not equal to setdata"); - // do nothing and reexecute - rs = callableStatement.executeQuery(); - // get the param without getting the resultset - rs = callableStatement.executeQuery(); - assertEquals(callableStatement.getInt((int) 1), -2147483648, "Received data not equal to setdata"); + // do nothing and reexecute + rs = callableStatement.executeQuery(); + // get the param without getting the resultset + rs = callableStatement.executeQuery(); + assertEquals(callableStatement.getInt((int) 1), -2147483648, "Received data not equal to setdata"); - rs = callableStatement.executeQuery(); - rs.next(); + rs = callableStatement.executeQuery(); + rs.next(); - assertEquals(rs.getInt(1), 0, "Received data not equal to setdata"); - assertEquals(callableStatement.getInt((int) 1), -2147483648, "Received data not equal to setdata"); - assertEquals(callableStatement.getInt((int) 5), -5372, "Received data not equal to setdata"); - rs = callableStatement.executeQuery(); - callableStatement.close(); - rs.close(); - stmt.close(); - terminateVariation(); + assertEquals(rs.getInt(1), 0, "Received data not equal to setdata"); + assertEquals(callableStatement.getInt((int) 1), -2147483648, "Received data not equal to setdata"); + assertEquals(callableStatement.getInt((int) 5), -5372, "Received data not equal to setdata"); + rs = callableStatement.executeQuery(); + rs.close(); + } + terminateVariation(statement); + } } - - private void terminateVariation() throws SQLException { - statement = connection.createStatement(); - statement.executeUpdate("DROP TABLE " + tableName); - statement.executeUpdate(" DROP PROCEDURE " + procName); - statement.close(); - connection.close(); + /** + * Cleanups + * + * @throws SQLException + */ + private void terminateVariation(Statement statement) throws SQLException { + Utils.dropTableIfExists(tableName, statement); + Utils.dropProcedureIfExists(procName, statement); } - } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java index bd6bccd2c..6e3563101 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java @@ -32,6 +32,7 @@ import org.junit.runner.RunWith; import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; /** * Testing with LimitEscape queries @@ -40,7 +41,7 @@ @RunWith(JUnitPlatform.class) public class LimitEscapeTest extends AbstractTest { public static final Logger log = Logger.getLogger("LimitEscape"); - private static Vector offsetQuery = new Vector(); + private static Vector offsetQuery = new Vector<>(); private static Connection conn = null; static class Query { @@ -782,13 +783,10 @@ public static void afterAll() throws Exception { Statement stmt = conn.createStatement(); try { - stmt.executeUpdate("IF OBJECT_ID (N'UnitStatement_LimitEscape_t1', N'U') IS NOT NULL DROP TABLE UnitStatement_LimitEscape_t1"); - - stmt.executeUpdate("IF OBJECT_ID (N'UnitStatement_LimitEscape_t2', N'U') IS NOT NULL DROP TABLE UnitStatement_LimitEscape_t2"); - - stmt.executeUpdate("IF OBJECT_ID (N'UnitStatement_LimitEscape_t3', N'U') IS NOT NULL DROP TABLE UnitStatement_LimitEscape_t3"); - - stmt.executeUpdate("IF OBJECT_ID (N'UnitStatement_LimitEscape_t4', N'U') IS NOT NULL DROP TABLE UnitStatement_LimitEscape_t4"); + Utils.dropTableIfExists("UnitStatement_LimitEscape_t1", stmt); + Utils.dropTableIfExists("UnitStatement_LimitEscape_t2", stmt); + Utils.dropTableIfExists("UnitStatement_LimitEscape_t3", stmt); + Utils.dropTableIfExists("UnitStatement_LimitEscape_t4", stmt); } catch (Exception ex) { fail(ex.toString()); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/MergeTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/MergeTest.java index 43b0bccc1..54ebcdde2 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/MergeTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/MergeTest.java @@ -10,7 +10,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; +import java.sql.Connection; +import java.sql.DriverManager; import java.sql.ResultSet; +import java.sql.Statement; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.DisplayName; @@ -21,6 +24,7 @@ import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.DBConnection; import com.microsoft.sqlserver.testframework.DBStatement; +import com.microsoft.sqlserver.testframework.Utils; /** * Testing merge queries @@ -44,52 +48,42 @@ public class MergeTest extends AbstractTest { + "VALUES (SOURCE.CricketTeamID, SOURCE.CricketTeamCountry, SOURCE.CricketTeamContinent) " + "WHEN NOT MATCHED BY SOURCE THEN DELETE;"; - /** * Merge test + * * @throws Exception */ @Test @DisplayName("Merge Test") public void runTest() throws Exception { - DBConnection conn = new DBConnection(connectionString); - if (conn.getServerVersion() >= 10) { - DBStatement stmt = conn.createStatement(); - stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE); - stmt.executeUpdate(setupTables); - stmt.executeUpdate(mergeCmd2); - int updateCount = stmt.getUpdateCount(); - assertEquals(updateCount, 3, "Received the wrong update count!"); - - if (null != stmt) { - stmt.close(); - } - if (null != conn) { - conn.close(); + try (DBConnection conn = new DBConnection(connectionString)) { + if (conn.getServerVersion() >= 10) { + try (DBStatement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);) { + stmt.executeUpdate(setupTables); + stmt.executeUpdate(mergeCmd2); + int updateCount = stmt.getUpdateCount(); + assertEquals(updateCount, 3, "Received the wrong update count!"); + + } } } } - + /** * Clean up + * * @throws Exception */ @AfterAll public static void afterAll() throws Exception { - DBConnection conn = new DBConnection(connectionString); - DBStatement stmt = conn.createStatement(); - try { - stmt.executeUpdate("IF OBJECT_ID (N'dbo.CricketTeams', N'U') IS NOT NULL DROP TABLE dbo.CricketTeams"); - } - catch (Exception ex) { - fail(ex.toString()); - } - finally { - stmt.close(); - conn.close(); + try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement()) { + try { + Utils.dropTableIfExists("dbo.CricketTeams", stmt); + } + catch (Exception ex) { + fail(ex.toString()); + } } - } - } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/NamedParamMultiPartTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/NamedParamMultiPartTest.java index 47c73ed95..426a35183 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/NamedParamMultiPartTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/NamedParamMultiPartTest.java @@ -12,8 +12,6 @@ import java.sql.CallableStatement; import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.Driver; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; @@ -21,12 +19,13 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; /** * Multipart parameters @@ -34,120 +33,137 @@ */ @RunWith(JUnitPlatform.class) public class NamedParamMultiPartTest extends AbstractTest { - private static final String dataPut = "eminem "; + private static final String dataPut = "eminem"; private static Connection connection = null; - private static CallableStatement cs = null; + String procedureName = "mystoredproc"; /** * setup + * * @throws SQLException */ @BeforeAll public static void beforeAll() throws SQLException { connection = DriverManager.getConnection(connectionString); - Statement statement = connection.createStatement(); - statement.executeUpdate( - "if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[mystoredproc]') and OBJECTPROPERTY(id, N'IsProcedure') = 1) DROP PROCEDURE [mystoredproc]"); - statement.executeUpdate("CREATE PROCEDURE [mystoredproc] (@p_out varchar(255) OUTPUT) AS set @p_out = '" + dataPut + "'"); - statement.close(); - } + try (Statement statement = connection.createStatement()) { + Utils.dropProcedureIfExists("mystoredproc", statement); + statement.executeUpdate("CREATE PROCEDURE [mystoredproc] (@p_out varchar(255) OUTPUT) AS set @p_out = '" + dataPut + "'"); + } + } + /** * Stored procedure call + * * @throws Exception */ @Test public void update1() throws Exception { - cs = connection.prepareCall("{ CALL [mystoredproc] (?) }"); - cs.registerOutParameter("p_out", Types.VARCHAR); - cs.executeUpdate(); - String data = cs.getString("p_out"); - assertEquals(data, dataPut, "Received data not equal to setdata"); + try (CallableStatement cs = connection.prepareCall("{ CALL " + procedureName + " (?) }")) { + cs.registerOutParameter("p_out", Types.VARCHAR); + cs.executeUpdate(); + String data = cs.getString("p_out"); + assertEquals(data, dataPut, "Received data not equal to setdata"); + } } /** * Stored procedure call + * * @throws Exception */ @Test public void update2() throws Exception { - cs = connection.prepareCall("{ CALL [dbo].[mystoredproc] (?) }"); - cs.registerOutParameter("p_out", Types.VARCHAR); - cs.executeUpdate(); - Object data = cs.getObject("p_out"); - assertEquals(data, dataPut, "Received data not equal to setdata"); + try (CallableStatement cs = connection.prepareCall("{ CALL " + procedureName + " (?) }")) { + cs.registerOutParameter("p_out", Types.VARCHAR); + cs.executeUpdate(); + Object data = cs.getObject("p_out"); + assertEquals(data, dataPut, "Received data not equal to setdata"); + } } /** * Stored procedure call + * * @throws Exception */ @Test public void update3() throws Exception { String catalog = connection.getCatalog(); String storedproc = "[" + catalog + "]" + ".[dbo].[mystoredproc]"; - cs = connection.prepareCall("{ CALL " + storedproc + " (?) }"); - cs.registerOutParameter("p_out", Types.VARCHAR); - cs.executeUpdate(); - Object data = cs.getObject("p_out"); - assertEquals(data, dataPut, "Received data not equal to setdata"); + try (CallableStatement cs = connection.prepareCall("{ CALL " + storedproc + " (?) }")) { + cs.registerOutParameter("p_out", Types.VARCHAR); + cs.executeUpdate(); + Object data = cs.getObject("p_out"); + assertEquals(data, dataPut, "Received data not equal to setdata"); + } } /** * Stored procedure call + * * @throws Exception */ @Test public void update4() throws Exception { - cs = connection.prepareCall("{ CALL mystoredproc (?) }"); - cs.registerOutParameter("p_out", Types.VARCHAR); - cs.executeUpdate(); - Object data = cs.getObject("p_out"); - assertEquals(data, dataPut, "Received data not equal to setdata"); + try (CallableStatement cs = connection.prepareCall("{ CALL " + procedureName + " (?) }")) { + cs.registerOutParameter("p_out", Types.VARCHAR); + cs.executeUpdate(); + Object data = cs.getObject("p_out"); + assertEquals(data, dataPut, "Received data not equal to setdata"); + } } /** * Stored procedure call + * * @throws Exception */ + @Test + @Disabled public void update5() throws Exception { - cs = connection.prepareCall("{ CALL dbo.mystoredproc (?) }"); - cs.registerOutParameter("p_out", Types.VARCHAR); - cs.executeUpdate(); - Object data = cs.getObject("p_out"); - assertEquals(data, dataPut, "Received data not equal to setdata"); + try (CallableStatement cs = connection.prepareCall("{ CALL " + procedureName + " (?) }")) { + cs.registerOutParameter("p_out", Types.VARCHAR); + cs.executeUpdate(); + Object data = cs.getObject("p_out"); + assertEquals(data, dataPut, "Received data not equal to setdata"); + } } + /** * * @throws Exception */ + @Test + @Disabled public void update6() throws Exception { String catalog = connection.getCatalog(); - String storedproc = catalog + ".dbo.mystoredproc"; - cs = connection.prepareCall("{ CALL " + storedproc + " (?) }"); - cs.registerOutParameter("p_out", Types.VARCHAR); - cs.executeUpdate(); - Object data = cs.getObject("p_out"); - assertEquals(data, dataPut, "Received data not equal to setdata"); + String storedproc = catalog + ".dbo." + procedureName; + try (CallableStatement cs = connection.prepareCall("{ CALL " + storedproc + " (?) }")) { + cs.registerOutParameter("p_out", Types.VARCHAR); + cs.executeUpdate(); + Object data = cs.getObject("p_out"); + assertEquals(data, dataPut, "Received data not equal to setdata"); + } } + /** * Clean up + * + * @throws SQLException */ @AfterAll - public static void afterAll() { - try { - if (null != connection) { + public static void afterAll() throws SQLException { + try (Statement stmt = connection.createStatement()) { + Utils.dropProcedureIfExists("mystoredproc", stmt); + } + finally { + if (connection != null) { connection.close(); } - if (null != cs) { - cs.close(); - } - } - catch (SQLException e) { - fail(e.toString()); } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PQImpsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PQImpsTest.java index a8c3abcdd..7d786dc46 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PQImpsTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PQImpsTest.java @@ -8,8 +8,8 @@ package com.microsoft.sqlserver.jdbc.unit.statement; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.sql.DriverManager; import java.sql.ParameterMetaData; @@ -27,8 +27,10 @@ import com.microsoft.sqlserver.jdbc.SQLServerConnection; import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.SQLServerParameterMetaData; import com.microsoft.sqlserver.testframework.AbstractSQLGenerator; import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; import com.microsoft.sqlserver.testframework.util.RandomUtil; /** @@ -51,12 +53,15 @@ public class PQImpsTest extends AbstractTest { private static String mergeNameDesTable = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("mergeNameDesTable_DB")); private static String numericTable = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("numericTable_DB")); private static String charTable = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("charTable_DB")); + private static String charTable2 = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("charTable2_DB")); private static String binaryTable = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("binaryTable_DB")); private static String dateAndTimeTable = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dateAndTimeTable_DB")); private static String multipleTypesTable = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("multipleTypesTable_DB")); + private static String spaceTable = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("spaceTable_DB")); /** * Setup + * * @throws SQLException */ @BeforeAll @@ -67,14 +72,17 @@ public static void BeforeTests() throws SQLException { createMultipleTypesTable(); createNumericTable(); createCharTable(); + createChar2Table(); createBinaryTable(); createDateAndTimeTable(); createTablesForCompexQueries(); + createSpaceTable(); populateTablesForCompexQueries(); } /** * Numeric types test + * * @throws SQLException */ @Test @@ -102,6 +110,7 @@ public void numericTest() throws SQLException { /** * Char types test + * * @throws SQLException */ @Test @@ -129,6 +138,7 @@ public void charTests() throws SQLException { /** * Binary types test + * * @throws SQLException */ @Test @@ -157,6 +167,7 @@ public void binaryTests() throws SQLException { /** * Temporal types test + * * @throws SQLException */ @Test @@ -185,6 +196,7 @@ public void temporalTests() throws SQLException { /** * Multiple Types table + * * @throws Exception */ @Test @@ -415,6 +427,15 @@ private static void createCharTable() throws SQLException { + "c4 nvarchar(60) not null," + "c5 text not null," + "c6 ntext not null" + ")"); } + private static void createSpaceTable() throws SQLException { + stmt.execute("Create table " + spaceTable + " (" + "[c1*/someString withspace] char(50) not null," + "c2 varchar(20) not null," + + "c3 nchar(30) not null," + "c4 nvarchar(60) not null," + "c5 text not null," + "c6 ntext not null" + ")"); + } + + private static void createChar2Table() throws SQLException { + stmt.execute("Create table " + charTable2 + " (" + "table2c1 char(50) not null)"); + } + private static void populateCharTable() throws SQLException { stmt.execute("insert into " + charTable + " values (" + "'Hello'," + "'Hello'," + "N'Hello'," + "N'Hello'," + "'Hello'," + "N'Hello'" + ")"); } @@ -707,6 +728,7 @@ private static void populateTablesForCompexQueries() throws SQLException { /** * Test subquery + * * @throws SQLException */ @Test @@ -736,6 +758,7 @@ public void testSubquery() throws SQLException { /** * Test join + * * @throws SQLException */ @Test @@ -766,7 +789,8 @@ public void testJoin() throws SQLException { } /** - * Test merge + * Test merge + * * @throws SQLException */ @Test @@ -965,6 +989,7 @@ private static void testMixedWithHardcodedValues() throws SQLException { /** * Test Orderby + * * @throws SQLException */ @Test @@ -994,6 +1019,7 @@ public void testOrderBy() throws SQLException { /** * Test Groupby + * * @throws SQLException */ @Test @@ -1023,6 +1049,7 @@ private void testGroupBy() throws SQLException { /** * Test Lower + * * @throws SQLException */ @Test @@ -1051,6 +1078,7 @@ public void testLower() throws SQLException { /** * Test Power + * * @throws SQLException */ @Test @@ -1078,6 +1106,7 @@ public void testPower() throws SQLException { /** * All in one queries + * * @throws SQLException */ @Test @@ -1109,20 +1138,260 @@ public void testAllInOneQuery() throws SQLException { } } + /** + * test query with simple multiple line comments + * + * @throws SQLException + */ + @Test + public void testQueryWithMultipleLineComments1() throws SQLException { + pstmt = connection.prepareStatement("/*te\nst*//*test*/select top 100 c1 from " + charTable + " where c1 = ?"); + pstmt.setString(1, "abc"); + + try { + pstmt.getParameterMetaData(); + pstmt.executeQuery(); + } + catch (Exception e) { + fail(e.toString()); + } + } + + /** + * test query with complex multiple line comments + * + * @throws SQLException + */ + @Test + public void testQueryWithMultipleLineComments2() throws SQLException { + pstmt = connection + .prepareStatement("/*/*te\nst*/ te/*test*/st /*te\nst*/*//*te/*test*/st*/select top 100 c1 from " + charTable + " where c1 = ?"); + pstmt.setString(1, "abc"); + + try { + pstmt.getParameterMetaData(); + pstmt.executeQuery(); + } + catch (Exception e) { + fail(e.toString()); + } + } + + /** + * test insertion query with multiple line comments + * + * @throws SQLException + */ + @Test + public void testQueryWithMultipleLineCommentsInsert() throws SQLException { + pstmt = connection.prepareStatement("/*te\nst*//*test*/insert /*test*/into " + charTable + " (c1) VALUES(?)"); + + try { + pstmt.getParameterMetaData(); + } + catch (Exception e) { + fail(e.toString()); + } + } + + /** + * test update query with multiple line comments + * + * @throws SQLException + */ + @Test + public void testQueryWithMultipleLineCommentsUpdate() throws SQLException { + pstmt = connection.prepareStatement("/*te\nst*//*test*/update /*test*/" + charTable + " set c1=123 where c1=?"); + + try { + pstmt.getParameterMetaData(); + } + catch (Exception e) { + fail(e.toString()); + } + } + + /** + * test deletion query with multiple line comments + * + * @throws SQLException + */ + @Test + public void testQueryWithMultipleLineCommentsDeletion() throws SQLException { + pstmt = connection.prepareStatement("/*te\nst*//*test*/delete /*test*/from " + charTable + " where c1=?"); + + try { + pstmt.getParameterMetaData(); + } + catch (Exception e) { + fail(e.toString()); + } + } + + /** + * test query with single line comments + * + * @throws SQLException + */ + @Test + public void testQueryWithSingleLineComments1() throws SQLException { + pstmt = connection.prepareStatement("-- #test \n select top 100 c1 from " + charTable + " where c1 = ?"); + pstmt.setString(1, "abc"); + + try { + pstmt.getParameterMetaData(); + pstmt.executeQuery(); + } + catch (Exception e) { + fail(e.toString()); + } + } + + /** + * test query with single line comments + * + * @throws SQLException + */ + @Test + public void testQueryWithSingleLineComments2() throws SQLException { + pstmt = connection.prepareStatement("--#test\nselect top 100 c1 from " + charTable + " where c1 = ?"); + pstmt.setString(1, "abc"); + + try { + pstmt.getParameterMetaData(); + pstmt.executeQuery(); + } + catch (Exception e) { + fail(e.toString()); + } + } + + /** + * test query with single line comment + * + * @throws SQLException + */ + @Test + public void testQueryWithSingleLineComments3() throws SQLException { + pstmt = connection.prepareStatement("select top 100 c1\nfrom " + charTable + " where c1 = ?"); + pstmt.setString(1, "abc"); + + try { + pstmt.getParameterMetaData(); + pstmt.executeQuery(); + } + catch (Exception e) { + fail(e.toString()); + } + } + + /** + * test insertion query with single line comments + * + * @throws SQLException + */ + @Test + public void testQueryWithSingleLineCommentsInsert() throws SQLException { + pstmt = connection.prepareStatement("--#test\ninsert /*test*/into " + charTable + " (c1) VALUES(?)"); + + try { + pstmt.getParameterMetaData(); + } + catch (Exception e) { + fail(e.toString()); + } + } + + /** + * test update query with single line comments + * + * @throws SQLException + */ + @Test + public void testQueryWithSingleLineCommentsUpdate() throws SQLException { + pstmt = connection.prepareStatement("--#test\nupdate /*test*/" + charTable + " set c1=123 where c1=?"); + + try { + pstmt.getParameterMetaData(); + } + catch (Exception e) { + fail(e.toString()); + } + } + + /** + * test deletion query with single line comments + * + * @throws SQLException + */ + @Test + public void testQueryWithSingleLineCommentsDeletion() throws SQLException { + pstmt = connection.prepareStatement("--#test\ndelete /*test*/from " + charTable + " where c1=?"); + + try { + pstmt.getParameterMetaData(); + } + catch (Exception e) { + fail(e.toString()); + } + } + + /** + * test column name with end comment mark and space + * + * @throws SQLServerException + */ + @Test + public void testQueryWithSpaceAndEndCommentMarkInColumnName() throws SQLServerException { + pstmt = connection.prepareStatement("SELECT [c1*/someString withspace] from " + spaceTable); + + try { + pstmt.getParameterMetaData(); + } + catch (Exception e) { + fail(e.toString()); + } + } + + /** + * test getting parameter count with a complex query with multiple table + * + * @throws SQLServerException + */ + @Test + public void testComplexQueryWithMultipleTables() throws SQLServerException { + pstmt = connection.prepareStatement( + "insert into " + charTable + " (c1) select ? where not exists (select * from " + charTable2 + " where table2c1 = ?)"); + + try { + SQLServerParameterMetaData pMD = (SQLServerParameterMetaData) pstmt.getParameterMetaData(); + int parameterCount = pMD.getParameterCount(); + + assertTrue(2 == parameterCount, "Parameter Count should be 2."); + } + catch (Exception e) { + fail(e.toString()); + } + } + /** * Cleanup + * * @throws SQLException */ @AfterAll public static void dropTables() throws SQLException { - stmt.execute("if object_id('" + nameTable + "','U') is not null" + " drop table " + nameTable); - stmt.execute("if object_id('" + phoneNumberTable + "','U') is not null" + " drop table " + phoneNumberTable); - stmt.execute("if object_id('" + mergeNameDesTable + "','U') is not null" + " drop table " + mergeNameDesTable); - stmt.execute("if object_id('" + numericTable + "','U') is not null" + " drop table " + numericTable); - stmt.execute("if object_id('" + charTable + "','U') is not null" + " drop table " + charTable); - stmt.execute("if object_id('" + binaryTable + "','U') is not null" + " drop table " + binaryTable); - stmt.execute("if object_id('" + dateAndTimeTable + "','U') is not null" + " drop table " + dateAndTimeTable); - stmt.execute("if object_id('" + multipleTypesTable + "','U') is not null" + " drop table " + multipleTypesTable); + Utils.dropTableIfExists(nameTable, stmt); + Utils.dropTableIfExists(phoneNumberTable, stmt); + Utils.dropTableIfExists(mergeNameDesTable, stmt); + Utils.dropTableIfExists(numericTable, stmt); + Utils.dropTableIfExists(phoneNumberTable, stmt); + Utils.dropTableIfExists(charTable, stmt); + Utils.dropTableIfExists(charTable2, stmt); + Utils.dropTableIfExists(binaryTable, stmt); + Utils.dropTableIfExists(dateAndTimeTable, stmt); + Utils.dropTableIfExists(multipleTypesTable, stmt); + Utils.dropTableIfExists(spaceTable, stmt); if (null != rs) { rs.close(); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PoolableTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PoolableTest.java index 626efeb99..7ce0773de 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PoolableTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PoolableTest.java @@ -8,6 +8,7 @@ package com.microsoft.sqlserver.jdbc.unit.statement; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import java.sql.CallableStatement; import java.sql.Connection; @@ -16,6 +17,7 @@ import java.sql.SQLException; import java.sql.Statement; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; @@ -25,6 +27,7 @@ import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; import com.microsoft.sqlserver.jdbc.SQLServerStatement; import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; /** * Test Poolable statements @@ -35,44 +38,60 @@ public class PoolableTest extends AbstractTest { /** * Poolable Test + * * @throws SQLException * @throws ClassNotFoundException */ @Test @DisplayName("Poolable Test") - public void poolableTest() throws SQLException, ClassNotFoundException { - Connection connection = DriverManager.getConnection(connectionString); - Statement statement = connection.createStatement(); - try { - // First get the default values - boolean isPoolable = ((SQLServerStatement) statement).isPoolable(); - assertEquals(isPoolable, false, "SQLServerStatement should not be Poolable by default"); + public void poolableTest() throws SQLException, ClassNotFoundException { + try (Connection conn = DriverManager.getConnection(connectionString); Statement statement = conn.createStatement()) { + try { + // First get the default values + boolean isPoolable = ((SQLServerStatement) statement).isPoolable(); + assertEquals(isPoolable, false, "SQLServerStatement should not be Poolable by default"); - PreparedStatement prepStmt = connection.prepareStatement("select 1"); - isPoolable = ((SQLServerPreparedStatement) prepStmt).isPoolable(); - assertEquals(isPoolable, true, "SQLServerPreparedStatement should be Poolable by default"); + try (PreparedStatement prepStmt = connection.prepareStatement("select 1")) { + isPoolable = ((SQLServerPreparedStatement) prepStmt).isPoolable(); + assertEquals(isPoolable, true, "SQLServerPreparedStatement should be Poolable by default"); + } + try (CallableStatement callableStatement = connection.prepareCall("{ ? = CALL " + "ProcName" + " (?, ?, ?, ?) }");) { + isPoolable = ((SQLServerCallableStatement) callableStatement).isPoolable(); - CallableStatement callableStatement = connection.prepareCall("{ ? = CALL " + "ProcName" + " (?, ?, ?, ?) }"); - isPoolable = ((SQLServerCallableStatement) callableStatement).isPoolable(); + assertEquals(isPoolable, true, "SQLServerCallableStatement should be Poolable by default"); - assertEquals(isPoolable, true, "SQLServerCallableStatement should be Poolable by default"); + // Now do couple of sets and gets - // Now do couple of sets and gets - - ((SQLServerCallableStatement) callableStatement).setPoolable(false); - assertEquals(((SQLServerCallableStatement) callableStatement).isPoolable(), false, "set did not work"); - callableStatement.close(); - - ((SQLServerStatement) statement).setPoolable(true); - assertEquals(((SQLServerStatement) statement).isPoolable(), true, "set did not work"); - statement.close(); + ((SQLServerCallableStatement) callableStatement).setPoolable(false); + assertEquals(((SQLServerCallableStatement) callableStatement).isPoolable(), false, "set did not work"); + } + ((SQLServerStatement) statement).setPoolable(true); + assertEquals(((SQLServerStatement) statement).isPoolable(), true, "set did not work"); + } + catch (UnsupportedOperationException e) { + assertEquals(System.getProperty("java.specification.version"), "1.5", "PoolableTest should be supported in anything other than 1.5"); + assertEquals(e.getMessage(), "This operation is not supported.", "Wrong exception message"); + } } - catch (UnsupportedOperationException e) { - assertEquals(System.getProperty("java.specification.version"), "1.5", "PoolableTest should be supported in anything other than 1.5"); - assertEquals(e.getMessage(), "This operation is not supported.", "Wrong exception message"); + } + + /** + * Clean up + * + * @throws Exception + */ + @AfterAll + public static void afterAll() throws Exception { + try (Connection conn = DriverManager.getConnection(connectionString); Statement stmt = conn.createStatement()) { + try { + Utils.dropProcedureIfExists("ProcName", stmt); + } + catch (Exception ex) { + fail(ex.toString()); + } } - } - + } + } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java new file mode 100644 index 000000000..dd7a43d10 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java @@ -0,0 +1,511 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.unit.statement; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; + +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.SQLServerDataSource; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.util.RandomUtil; + +@RunWith(JUnitPlatform.class) +public class PreparedStatementTest extends AbstractTest { + private void executeSQL(SQLServerConnection conn, String sql) throws SQLException { + Statement stmt = conn.createStatement(); + stmt.execute(sql); + } + + private int executeSQLReturnFirstInt(SQLServerConnection conn, String sql) throws SQLException { + Statement stmt = conn.createStatement(); + ResultSet result = stmt.executeQuery(sql); + + int returnValue = -1; + + if(result.next()) + returnValue = result.getInt(1); + + return returnValue; + } + + /** + * Test handling of unpreparing prepared statements. + * + * @throws SQLException + */ + @Test + public void testBatchedUnprepare() throws SQLException { + SQLServerConnection conOuter = null; + + try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { + conOuter = con; + + // Turn off use of prepared statement cache. + con.setStatementPoolingCacheSize(0); + + // Clean-up proc cache + this.executeSQL(con, "DBCC FREEPROCCACHE;"); + + String lookupUniqueifier = UUID.randomUUID().toString(); + + String queryCacheLookup = String.format("%%/*unpreparetest_%s%%*/SELECT * FROM sys.tables;", lookupUniqueifier); + String query = String.format("/*unpreparetest_%s only sp_executesql*/SELECT * FROM sys.tables;", lookupUniqueifier); + + // Verify nothing in cache. + String verifyTotalCacheUsesQuery = String.format("SELECT CAST(ISNULL(SUM(usecounts), 0) AS INT) FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS s WHERE s.text LIKE '%s'", queryCacheLookup); + + assertSame(0, executeSQLReturnFirstInt(con, verifyTotalCacheUsesQuery)); + + int iterations = 25; + + query = String.format("/*unpreparetest_%s, sp_executesql->sp_prepexec->sp_execute- batched sp_unprepare*/SELECT * FROM sys.tables;", lookupUniqueifier); + int prevDiscardActionCount = 0; + + // Now verify unprepares are needed. + for(int i = 0; i < iterations; ++i) { + + // Verify current queue depth is expected. + assertSame(prevDiscardActionCount, con.getDiscardedServerPreparedStatementCount()); + + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(String.format("%s--%s", query, i))) { + pstmt.execute(); // sp_executesql + + pstmt.execute(); // sp_prepexec + ++prevDiscardActionCount; + + pstmt.execute(); // sp_execute + } + + // Verify clean-up is happening as expected. + if(prevDiscardActionCount > con.getServerPreparedStatementDiscardThreshold()) { + prevDiscardActionCount = 0; + } + + assertSame(prevDiscardActionCount, con.getDiscardedServerPreparedStatementCount()); + } + + // Skipped for now due to unexpected failures. Not functional so not critical. + /* + // Verify total cache use. + int expectedCacheHits = iterations * 4; + int allowedDiscrepency = 20; + // Allow some discrepency in number of cache hits to not fail test ( + // TODO: Follow up on why there is sometimes a discrepency in number of cache hits (less than expected). + assertTrue(expectedCacheHits >= executeSQLReturnFirstInt(con, verifyTotalCacheUsesQuery)); + assertTrue(expectedCacheHits - allowedDiscrepency < executeSQLReturnFirstInt(con, verifyTotalCacheUsesQuery)); + */ + } + // Verify clean-up happened on connection close. + assertSame(0, conOuter.getDiscardedServerPreparedStatementCount()); + } + + /** + * Test handling of statement pooling for prepared statements. + * + * @throws SQLException + */ + @Test + @Tag("slow") + public void testStatementPooling() throws SQLException { + // Test % handle re-use + try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { + String query = String.format("/*statementpoolingtest_re-use_%s*/SELECT TOP(1) * FROM sys.tables;", UUID.randomUUID().toString()); + + con.setStatementPoolingCacheSize(10); + + boolean[] prepOnFirstCalls = {false, true}; + + for(boolean prepOnFirstCall : prepOnFirstCalls) { + + con.setEnablePrepareOnFirstPreparedStatementCall(prepOnFirstCall); + + int[] queryCounts = {10, 20, 30, 40}; + for(int queryCount : queryCounts) { + String[] queries = new String[queryCount]; + for(int i = 0; i < queries.length; ++i) { + queries[i] = String.format("%s--%s--%s--%s", query, i, queryCount, prepOnFirstCall); + } + + int testsWithHandleReuse = 0; + final int testCount = 500; + for(int i = 0; i < testCount; ++i) { + Random random = new Random(); + int queryNumber = random.nextInt(queries.length); + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement(queries[queryNumber])) { + pstmt.execute(); + + // Grab handle-reuse before it would be populated if initially created. + if(0 < pstmt.getPreparedStatementHandle()) + testsWithHandleReuse++; + + pstmt.getMoreResults(); // Make sure handle is updated. + } + } + System.out.println(String.format("Prep on first call: %s Query count:%s: %s of %s (%s)", prepOnFirstCall, queryCount, testsWithHandleReuse, testCount, (double)testsWithHandleReuse/(double)testCount)); + } + } + } + + try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { + + // Test behvaior with statement pooling. + con.setStatementPoolingCacheSize(10); + + // Test with missing handle failures (fake). + this.executeSQL(con, "CREATE TABLE #update1 (col INT);INSERT #update1 VALUES (1);"); + this.executeSQL(con, "CREATE PROC #updateProc1 AS UPDATE #update1 SET col += 1; IF EXISTS (SELECT * FROM #update1 WHERE col % 5 = 0) THROW 99586, 'Prepared handle GAH!', 1;"); + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement("#updateProc1")) { + for (int i = 0; i < 100; ++i) { + assertSame(1, pstmt.executeUpdate()); + } + } + + // Test batching with missing handle failures (fake). + this.executeSQL(con, "CREATE TABLE #update2 (col INT);INSERT #update2 VALUES (1);"); + this.executeSQL(con, "CREATE PROC #updateProc2 AS UPDATE #update2 SET col += 1; IF EXISTS (SELECT * FROM #update2 WHERE col % 5 = 0) THROW 99586, 'Prepared handle GAH!', 1;"); + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement("#updateProc2")) { + for (int i = 0; i < 100; ++i) + pstmt.addBatch(); + + int[] updateCounts = pstmt.executeBatch(); + + // Verify update counts are correct + for (int i : updateCounts) { + assertSame(1, i); + } + } + } + + try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { + // Test behvaior with statement pooling. + con.setStatementPoolingCacheSize(10); + + String lookupUniqueifier = UUID.randomUUID().toString(); + String query = String.format("/*statementpoolingtest_%s*/SELECT * FROM sys.tables;", lookupUniqueifier); + + // Execute statement first, should create cache entry WITHOUT handle (since sp_executesql was used). + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { + pstmt.execute(); // sp_executesql + pstmt.getMoreResults(); // Make sure handle is updated. + + assertSame(0, pstmt.getPreparedStatementHandle()); + } + + // Execute statement again, should now create handle. + int handle = 0; + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { + pstmt.execute(); // sp_prepexec + pstmt.getMoreResults(); // Make sure handle is updated. + + handle = pstmt.getPreparedStatementHandle(); + assertNotSame(0, handle); + } + + // Execute statement again and verify same handle was used. + //TODO Fix the issue : Connection Resiliency +// try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { +// pstmt.execute(); // sp_execute +// pstmt.getMoreResults(); // Make sure handle is updated. +// +// assertNotSame(0, pstmt.getPreparedStatementHandle()); +// assertSame(handle, pstmt.getPreparedStatementHandle()); +// } + + // Execute new statement with different SQL text and verify it does NOT get same handle (should now fall back to using sp_executesql). + SQLServerPreparedStatement outer = null; + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query + ";")) { + outer = pstmt; + pstmt.execute(); // sp_executesql + pstmt.getMoreResults(); // Make sure handle is updated. + + assertSame(0, pstmt.getPreparedStatementHandle()); + assertNotSame(handle, pstmt.getPreparedStatementHandle()); + } + try { + System.out.println(outer.getPreparedStatementHandle()); + fail("Error for invalid use of getPreparedStatementHandle() after statement close expected."); + } + catch(Exception e) { + // Good! + } + } + } + + /** + * Test handling of eviction from statement pooling for prepared statements. + * + * @throws SQLException + */ + @Test + public void testStatementPoolingEviction() throws SQLException { + + for (int testNo = 0; testNo < 2; ++testNo) { + try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { + + int cacheSize = 10; + int discardedStatementCount = testNo == 0 ? 5 /*batched unprepares*/ : 0 /*regular unprepares*/; + + con.setStatementPoolingCacheSize(cacheSize); + con.setServerPreparedStatementDiscardThreshold(discardedStatementCount); + + String lookupUniqueifier = UUID.randomUUID().toString(); + String query = String.format("/*statementpoolingevictiontest_%s*/SELECT * FROM sys.tables; -- ", lookupUniqueifier); + + // Add new statements to fill up the statement pool. + for (int i = 0; i < cacheSize; ++i) { + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query + new Integer(i).toString())) { + pstmt.execute(); // sp_executesql + pstmt.execute(); // sp_prepexec, actual handle created and cached. + } + // Make sure no handles in discard queue (still only in statement pool). + assertSame(0, con.getDiscardedServerPreparedStatementCount()); + } + + // No discarded handles yet, all in statement pool. + assertSame(0, con.getDiscardedServerPreparedStatementCount()); + + // Add new statements to fill up the statement discard action queue + // (new statement pushes existing statement from pool into discard + // action queue). + for (int i = cacheSize; i < cacheSize + 5; ++i) { + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query + new Integer(i).toString())) { + pstmt.execute(); // sp_executesql + pstmt.execute(); // sp_prepexec, actual handle created and cached. + } + // If we use discard queue handles should start going into discard queue. + if(0 == testNo) + assertNotSame(0, con.getDiscardedServerPreparedStatementCount()); + else + assertSame(0, con.getDiscardedServerPreparedStatementCount()); + } + + // If we use it, now discard queue should be "full". + if (0 == testNo) + assertSame(discardedStatementCount, con.getDiscardedServerPreparedStatementCount()); + else + assertSame(0, con.getDiscardedServerPreparedStatementCount()); + + // Adding one more statement should cause one more pooled statement to be invalidated and + // discarding actions should be executed (i.e. sp_unprepare batch), clearing out the discard + // action queue. + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { + pstmt.execute(); // sp_executesql + pstmt.execute(); // sp_prepexec, actual handle created and cached. + } + + // Discard queue should now be empty. + assertSame(0, con.getDiscardedServerPreparedStatementCount()); + + // Set statement pool size to 0 and verify statements get discarded. + int statementsInCache = con.getStatementHandleCacheEntryCount(); + con.setStatementPoolingCacheSize(0); + assertSame(0, con.getStatementHandleCacheEntryCount()); + + if(0 == testNo) + // Verify statements moved over to discard action queue. + assertSame(statementsInCache, con.getDiscardedServerPreparedStatementCount()); + + // Run discard actions (otherwise run on pstmt.close) + con.closeUnreferencedPreparedStatementHandles(); + + assertSame(0, con.getDiscardedServerPreparedStatementCount()); + + // Verify new statement does not go into cache (since cache is now off) + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { + pstmt.execute(); // sp_executesql + pstmt.execute(); // sp_prepexec, actual handle created and cached. + + assertSame(0, con.getStatementHandleCacheEntryCount()); + } + } + } + } + + final class TestPrepareRace implements Runnable { + + SQLServerConnection con; + String[] queries; + AtomicReference exception; + + TestPrepareRace(SQLServerConnection con, String[] queries, AtomicReference exception) { + this.con = con; + this.queries = queries; + this.exception = exception; + } + + @Override + public void run() + { + for (int j = 0; j < 500000; j++) { + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement(queries[j % 3])) { + pstmt.execute(); + } + catch (SQLException e) { + exception.set(e); + break; + } + } + } + } + + @Test + public void testPrepareRace() throws Exception { + + String[] queries = new String[3]; + queries[0] = String.format("SELECT * FROM sys.tables -- %s", UUID.randomUUID()); + queries[1] = String.format("SELECT * FROM sys.tables -- %s", UUID.randomUUID()); + queries[2] = String.format("SELECT * FROM sys.tables -- %s", UUID.randomUUID()); + + ExecutorService threadPool = Executors.newFixedThreadPool(4); + AtomicReference exception = new AtomicReference<>(); + try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { + + for (int i = 0; i < 4; i++) { + threadPool.execute(new TestPrepareRace(con, queries, exception)); + } + + threadPool.shutdown(); + threadPool.awaitTermination(10, SECONDS); + + assertNull(exception.get()); + + // Force un-prepares. + con.closeUnreferencedPreparedStatementHandles(); + + // Verify that queue is now empty. + assertSame(0, con.getDiscardedServerPreparedStatementCount()); + } + } + + /** + * Test handling of the two configuration knobs related to prepared statement handling. + * + * @throws SQLException + */ + @Test + public void testStatementPoolingPreparedStatementExecAndUnprepareConfig() throws SQLException { + + // Test Data Source properties + SQLServerDataSource dataSource = new SQLServerDataSource(); + dataSource.setURL(connectionString); + // Verify defaults. + assertTrue(0 < dataSource.getStatementPoolingCacheSize()); + // Verify change + dataSource.setStatementPoolingCacheSize(0); + assertSame(0, dataSource.getStatementPoolingCacheSize()); + dataSource.setEnablePrepareOnFirstPreparedStatementCall(!dataSource.getEnablePrepareOnFirstPreparedStatementCall()); + dataSource.setServerPreparedStatementDiscardThreshold(dataSource.getServerPreparedStatementDiscardThreshold() + 1); + // Verify connection from data source has same parameters. + SQLServerConnection connDataSource = (SQLServerConnection)dataSource.getConnection(); + assertSame(dataSource.getStatementPoolingCacheSize(), connDataSource.getStatementPoolingCacheSize()); + assertSame(dataSource.getEnablePrepareOnFirstPreparedStatementCall(), connDataSource.getEnablePrepareOnFirstPreparedStatementCall()); + assertSame(dataSource.getServerPreparedStatementDiscardThreshold(), connDataSource.getServerPreparedStatementDiscardThreshold()); + + // Test connection string properties. + + // Test disableStatementPooling + String connectionStringDisableStatementPooling = connectionString + ";disableStatementPooling=true;"; + SQLServerConnection connectionDisableStatementPooling = (SQLServerConnection)DriverManager.getConnection(connectionStringDisableStatementPooling); + assertSame(0, connectionDisableStatementPooling.getStatementPoolingCacheSize()); + assertTrue(!connectionDisableStatementPooling.isStatementPoolingEnabled()); + String connectionStringEnableStatementPooling = connectionString + ";disableStatementPooling=false;"; + SQLServerConnection connectionEnableStatementPooling = (SQLServerConnection)DriverManager.getConnection(connectionStringEnableStatementPooling); + assertTrue(0 < connectionEnableStatementPooling.getStatementPoolingCacheSize()); + + // Test EnablePrepareOnFirstPreparedStatementCall + String connectionStringNoExecuteSQL = connectionString + ";enablePrepareOnFirstPreparedStatementCall=true;"; + SQLServerConnection connectionNoExecuteSQL = (SQLServerConnection)DriverManager.getConnection(connectionStringNoExecuteSQL); + assertSame(true, connectionNoExecuteSQL.getEnablePrepareOnFirstPreparedStatementCall()); + + // Test ServerPreparedStatementDiscardThreshold + String connectionStringThreshold3 = connectionString + ";ServerPreparedStatementDiscardThreshold=3;"; + SQLServerConnection connectionThreshold3 = (SQLServerConnection)DriverManager.getConnection(connectionStringThreshold3); + assertSame(3, connectionThreshold3.getServerPreparedStatementDiscardThreshold()); + + // Test combination of EnablePrepareOnFirstPreparedStatementCall and ServerPreparedStatementDiscardThreshold + String connectionStringThresholdAndNoExecuteSQL = connectionString + ";ServerPreparedStatementDiscardThreshold=3;enablePrepareOnFirstPreparedStatementCall=true;"; + SQLServerConnection connectionThresholdAndNoExecuteSQL = (SQLServerConnection)DriverManager.getConnection(connectionStringThresholdAndNoExecuteSQL); + assertSame(true, connectionThresholdAndNoExecuteSQL.getEnablePrepareOnFirstPreparedStatementCall()); + assertSame(3, connectionThresholdAndNoExecuteSQL.getServerPreparedStatementDiscardThreshold()); + + // Test that an error is thrown for invalid connection string property values (non int/bool). + try { + String connectionStringThresholdError = connectionString + ";ServerPreparedStatementDiscardThreshold=hej;"; + DriverManager.getConnection(connectionStringThresholdError); + fail("Error for invalid ServerPreparedStatementDiscardThresholdexpected."); + } + catch(SQLException e) { + // Good! + } + try { + String connectionStringNoExecuteSQLError = connectionString + ";enablePrepareOnFirstPreparedStatementCall=dobidoo;"; + DriverManager.getConnection(connectionStringNoExecuteSQLError); + fail("Error for invalid enablePrepareOnFirstPreparedStatementCall expected."); + } + catch(SQLException e) { + // Good! + } + + // Verify instance setting is followed. + try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { + + // Turn off use of prepared statement cache. + con.setStatementPoolingCacheSize(0); + + String query = "/*unprepSettingsTest*/SELECT * FROM sys.objects;"; + + // Verify initial default is not serial: + assertTrue(1 < con.getServerPreparedStatementDiscardThreshold()); + + // Verify first use is batched. + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { + pstmt.execute(); // sp_executesql + pstmt.execute(); // sp_prepexec + } + + // Verify that the un-prepare action was not handled immediately. + assertSame(1, con.getDiscardedServerPreparedStatementCount()); + + // Force un-prepares. + con.closeUnreferencedPreparedStatementHandles(); + + // Verify that queue is now empty. + assertSame(0, con.getDiscardedServerPreparedStatementCount()); + + // Set instance setting to serial execution of un-prepare actions. + con.setServerPreparedStatementDiscardThreshold(1); + + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { + pstmt.execute(); + } + // Verify that the un-prepare action was handled immediately. + assertSame(0, con.getDiscardedServerPreparedStatementCount()); + } + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/RegressionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/RegressionTest.java index d7549d73a..2eec0cd70 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/RegressionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/RegressionTest.java @@ -8,12 +8,17 @@ package com.microsoft.sqlserver.jdbc.unit.statement; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.sql.DriverManager; +import java.sql.JDBCType; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.Connection; +import java.sql.Statement; import java.sql.SQLException; import java.sql.Statement; +import java.sql.Types; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; @@ -21,8 +26,10 @@ import org.junit.runner.RunWith; import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.DBConnection; +import com.microsoft.sqlserver.testframework.Utils; @RunWith(JUnitPlatform.class) public class RegressionTest extends AbstractTest { @@ -121,16 +128,119 @@ public void testSelectIntoUpdateCount() throws SQLException { if (null != con) con.close(); } + + /** + * Tests update query + * + * @throws SQLException + */ + @Test + public void testUpdateQuery() throws SQLException { + assumeTrue("JDBC41".equals(Utils.getConfiguredProperty("JDBC_Version")), "Aborting test case as JDBC version is not compatible. "); + + SQLServerConnection con = (SQLServerConnection) DriverManager.getConnection(connectionString); + String sql; + SQLServerPreparedStatement pstmt = null; + JDBCType[] targets = {JDBCType.INTEGER, JDBCType.SMALLINT}; + int rows = 3; + final String tableName = "[updateQuery]"; + + Statement stmt = con.createStatement(); + Utils.dropTableIfExists(tableName, stmt); + stmt.executeUpdate("CREATE TABLE " + tableName + " (" + "c1 int null," + "PK int NOT NULL PRIMARY KEY" + ")"); + + /* + * populate table + */ + sql = "insert into " + tableName + " values(" + "?,?" + ")"; + pstmt = (SQLServerPreparedStatement)con.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, connection.getHoldability()); + + for (int i = 1; i <= rows; i++) { + pstmt.setObject(1, i, JDBCType.INTEGER); + pstmt.setObject(2, i, JDBCType.INTEGER); + pstmt.executeUpdate(); + } + + /* + * Update table + */ + sql = "update " + tableName + " SET c1= ? where PK =1"; + for (int i = 1; i <= rows; i++) { + pstmt = (SQLServerPreparedStatement)con.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + for (JDBCType target : targets) { + pstmt.setObject(1, 5 + i, target); + pstmt.executeUpdate(); + } + } + + /* + * Verify + */ + ResultSet rs = stmt.executeQuery("select * from " + tableName); + rs.next(); + assertEquals(rs.getInt(1), 8, "Value mismatch"); + + + if (null != stmt) + stmt.close(); + if (null != con) + con.close(); + } + + private String xmlTableName = "try_SQLXML_Table"; + + /** + * Tests XML query + * + * @throws SQLException + */ + @Test + public void testXmlQuery() throws SQLException { + assumeTrue("JDBC41".equals(Utils.getConfiguredProperty("JDBC_Version")), "Aborting test case as JDBC version is not compatible. "); + + Connection connection = DriverManager.getConnection(connectionString); + + Statement stmt = connection.createStatement(); + + dropTables(stmt); + createTable(stmt); + + String sql = "UPDATE " + xmlTableName + " SET [c2] = ?, [c3] = ?"; + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(sql); + + pstmt.setObject(1, null); + pstmt.setObject(2, null, Types.SQLXML); + pstmt.executeUpdate(); + + pstmt = (SQLServerPreparedStatement) connection.prepareStatement(sql); + pstmt.setObject(1, null, Types.SQLXML); + pstmt.setObject(2, null); + pstmt.executeUpdate(); + + pstmt = (SQLServerPreparedStatement) connection.prepareStatement(sql); + pstmt.setObject(1, null); + pstmt.setObject(2, null, Types.SQLXML); + pstmt.executeUpdate(); + } + + private void dropTables(Statement stmt) throws SQLException { + stmt.executeUpdate("if object_id('" + xmlTableName + "','U') is not null" + " drop table " + xmlTableName); + } + + private void createTable(Statement stmt) throws SQLException { + + String sql = "CREATE TABLE " + xmlTableName + " ([c1] int, [c2] xml, [c3] xml)"; + + stmt.execute(sql); + } @AfterAll public static void terminate() throws SQLException { SQLServerConnection con = (SQLServerConnection) DriverManager.getConnection(connectionString); Statement stmt = con.createStatement(); - stmt.executeUpdate("IF EXISTS (select * from sysobjects where id = object_id(N'" + tableName + "') and OBJECTPROPERTY(id, N'IsTable') = 1)" - + " DROP TABLE " + tableName); - stmt.executeUpdate("IF EXISTS (select * from sysobjects where id = object_id(N'" + procName + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" - + " DROP PROCEDURE " + procName); - + Utils.dropTableIfExists(tableName, stmt); + Utils.dropProcedureIfExists(procName, stmt); } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/RegressionTestAlwaysEncrypted.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/RegressionTestAlwaysEncrypted.java new file mode 100644 index 000000000..8fe6d0f9a --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/RegressionTestAlwaysEncrypted.java @@ -0,0 +1,313 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +/* TODO: Make possible to run automated (including certs, only works on Windows now etc.)*/ +/* +package com.microsoft.sqlserver.jdbc.unit.statement; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.Date; +import java.sql.DriverManager; +import java.sql.JDBCType; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.jdbc.SQLServerResultSet; +import com.microsoft.sqlserver.testframework.AbstractTest; + +@RunWith(JUnitPlatform.class) +public class RegressionTestAlwaysEncrypted extends AbstractTest { + String dateTable = "DateTable"; + String charTable = "CharTable"; + String numericTable = "NumericTable"; + Statement stmt = null; + Connection connection = null; + Date date; + String cekName = "CEK_Auto1"; // you need to change this to your CEK + long dateValue = 212921879801519L; + + @Test + public void alwaysEncrypted1() throws Exception { + + Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); + connection = DriverManager.getConnection(connectionString + ";trustservercertificate=true;columnEncryptionSetting=enabled;database=Tobias;"); + assertTrue(null != connection); + + stmt = ((SQLServerConnection) connection).createStatement(); + + date = new Date(dateValue); + + dropTable(); + createNumericTable(); + populateNumericTable(); + printNumericTable(); + + dropTable(); + createDateTable(); + populateDateTable(); + printDateTable(); + + dropTable(); + createNumericTable(); + populateNumericTableWithNull(); + printNumericTable(); + } + + @Test + public void alwaysEncrypted2() throws Exception { + + Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); + connection = DriverManager.getConnection(connectionString + ";trustservercertificate=true;columnEncryptionSetting=enabled;database=Tobias;"); + assertTrue(null != connection); + + stmt = ((SQLServerConnection) connection).createStatement(); + + date = new Date(dateValue); + + dropTable(); + createCharTable(); + populateCharTable(); + printCharTable(); + + dropTable(); + createDateTable(); + populateDateTable(); + printDateTable(); + + dropTable(); + createNumericTable(); + populateNumericTableSpecificSetter(); + printNumericTable(); + + } + + private void populateDateTable() { + + try { + String sql = "insert into " + dateTable + " values( " + "?" + ")"; + SQLServerPreparedStatement sqlPstmt = (SQLServerPreparedStatement) ((SQLServerConnection) connection).prepareStatement(sql, + ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, connection.getHoldability()); + sqlPstmt.setObject(1, date); + sqlPstmt.executeUpdate(); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + private void populateCharTable() { + + try { + String sql = "insert into " + charTable + " values( " + "?,?,?,?,?,?" + ")"; + SQLServerPreparedStatement sqlPstmt = (SQLServerPreparedStatement) ((SQLServerConnection) connection).prepareStatement(sql, + ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, connection.getHoldability()); + sqlPstmt.setObject(1, "hi"); + sqlPstmt.setObject(2, "sample"); + sqlPstmt.setObject(3, "hey"); + sqlPstmt.setObject(4, "test"); + sqlPstmt.setObject(5, "hello"); + sqlPstmt.setObject(6, "caching"); + sqlPstmt.executeUpdate(); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + private void populateNumericTable() throws Exception { + String sql = "insert into " + numericTable + " values( " + "?,?,?,?,?,?,?,?,?" + ")"; + SQLServerPreparedStatement sqlPstmt = (SQLServerPreparedStatement) ((SQLServerConnection) connection).prepareStatement(sql, + ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, connection.getHoldability()); + sqlPstmt.setObject(1, true); + sqlPstmt.setObject(2, false); + sqlPstmt.setObject(3, true); + + Integer value = 255; + sqlPstmt.setObject(4, value.shortValue(), JDBCType.TINYINT); + sqlPstmt.setObject(5, value.shortValue(), JDBCType.TINYINT); + sqlPstmt.setObject(6, value.shortValue(), JDBCType.TINYINT); + + sqlPstmt.setObject(7, Short.valueOf("1"), JDBCType.SMALLINT); + sqlPstmt.setObject(8, Short.valueOf("2"), JDBCType.SMALLINT); + sqlPstmt.setObject(9, Short.valueOf("3"), JDBCType.SMALLINT); + + sqlPstmt.executeUpdate(); + } + + private void populateNumericTableSpecificSetter() { + + try { + String sql = "insert into " + numericTable + " values( " + "?,?,?,?,?,?,?,?,?" + ")"; + SQLServerPreparedStatement sqlPstmt = (SQLServerPreparedStatement) ((SQLServerConnection) connection).prepareStatement(sql, + ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, connection.getHoldability()); + sqlPstmt.setBoolean(1, true); + sqlPstmt.setBoolean(2, false); + sqlPstmt.setBoolean(3, true); + + Integer value = 255; + sqlPstmt.setShort(4, value.shortValue()); + sqlPstmt.setShort(5, value.shortValue()); + sqlPstmt.setShort(6, value.shortValue()); + + sqlPstmt.setByte(7, Byte.valueOf("127")); + sqlPstmt.setByte(8, Byte.valueOf("127")); + sqlPstmt.setByte(9, Byte.valueOf("127")); + + sqlPstmt.executeUpdate(); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + private void populateNumericTableWithNull() { + + try { + String sql = "insert into " + numericTable + " values( " + "?,?,?" + ",?,?,?" + ",?,?,?" + ")"; + SQLServerPreparedStatement sqlPstmt = (SQLServerPreparedStatement) ((SQLServerConnection) connection).prepareStatement(sql, + ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, connection.getHoldability()); + sqlPstmt.setObject(1, null, java.sql.Types.BIT); + sqlPstmt.setObject(2, null, java.sql.Types.BIT); + sqlPstmt.setObject(3, null, java.sql.Types.BIT); + + sqlPstmt.setObject(4, null, java.sql.Types.TINYINT); + sqlPstmt.setObject(5, null, java.sql.Types.TINYINT); + sqlPstmt.setObject(6, null, java.sql.Types.TINYINT); + + sqlPstmt.setObject(7, null, java.sql.Types.SMALLINT); + sqlPstmt.setObject(8, null, java.sql.Types.SMALLINT); + sqlPstmt.setObject(9, null, java.sql.Types.SMALLINT); + + sqlPstmt.executeUpdate(); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + private void printDateTable() throws SQLException { + + stmt = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); + SQLServerResultSet rs = (SQLServerResultSet) stmt.executeQuery("select * from " + dateTable); + + while (rs.next()) { + System.out.println(rs.getObject(1)); + } + } + + private void printCharTable() throws SQLException { + stmt = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); + SQLServerResultSet rs = (SQLServerResultSet) stmt.executeQuery("select * from " + charTable); + + while (rs.next()) { + System.out.println(rs.getObject(1)); + System.out.println(rs.getObject(2)); + System.out.println(rs.getObject(3)); + System.out.println(rs.getObject(4)); + System.out.println(rs.getObject(5)); + System.out.println(rs.getObject(6)); + } + + } + + private void printNumericTable() throws SQLException { + stmt = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); + SQLServerResultSet rs = (SQLServerResultSet) stmt.executeQuery("select * from " + numericTable); + + while (rs.next()) { + System.out.println(rs.getObject(1)); + System.out.println(rs.getObject(2)); + System.out.println(rs.getObject(3)); + System.out.println(rs.getObject(4)); + System.out.println(rs.getObject(5)); + System.out.println(rs.getObject(6)); + } + + } + + private void createDateTable() throws SQLException { + + String sql = "create table " + dateTable + " (" + + "RandomizedDate date ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + ");"; + + try { + stmt.execute(sql); + } + catch (SQLException e) { + System.out.println(e); + } + } + + private void createCharTable() throws SQLException { + String sql = "create table " + charTable + " (" + "PlainChar char(20) null," + + "RandomizedChar char(20) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicChar char(20) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainVarchar varchar(50) null," + + "RandomizedVarchar varchar(50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicVarchar varchar(50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + ");"; + + try { + stmt.execute(sql); + } + catch (SQLException e) { + System.out.println(e.getMessage()); + } + } + + private void createNumericTable() throws SQLException { + String sql = "create table " + numericTable + " (" + "PlainBit bit null," + + "RandomizedBit bit ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicBit bit ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainTinyint tinyint null," + + "RandomizedTinyint tinyint ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicTinyint tinyint ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + "PlainSmallint smallint null," + + "RandomizedSmallint smallint ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicSmallint smallint ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + + ");"; + + try { + stmt.execute(sql); + } + catch (SQLException e) { + System.out.println(e.getMessage()); + } + } + + private void dropTable() throws SQLException { + stmt.executeUpdate("if object_id('" + dateTable + "','U') is not null" + " drop table " + dateTable); + stmt.executeUpdate("if object_id('" + charTable + "','U') is not null" + " drop table " + charTable); + stmt.executeUpdate("if object_id('" + numericTable + "','U') is not null" + " drop table " + numericTable); + } +} +*/ \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/StatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/StatementTest.java index 12020fd07..afb311ae9 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/StatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/StatementTest.java @@ -7,22 +7,29 @@ */ package com.microsoft.sqlserver.jdbc.unit.statement; +import static org.junit.Assert.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.io.StringReader; +import java.math.BigDecimal; +import java.sql.Blob; import java.sql.CallableStatement; +import java.sql.Clob; import java.sql.Connection; import java.sql.DriverManager; +import java.sql.NClob; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.SQLXML; import java.sql.Statement; import java.sql.Types; import java.util.ArrayList; import java.util.Random; +import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -31,6 +38,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; @@ -72,7 +80,7 @@ public void init() throws Exception { con.setAutoCommit(false); Statement stmt = con.createStatement(); try { - stmt.executeUpdate("DROP TABLE if exists " + tableName); + Utils.dropTableIfExists(tableName, stmt); } catch (SQLException e) { } @@ -89,7 +97,7 @@ public void terminate() throws Exception { Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement(); try { - stmt.executeUpdate("DROP TABLE if exists " + tableName); + Utils.dropTableIfExists(tableName, stmt); } catch (SQLException e) { } @@ -672,8 +680,7 @@ public void testCancelGetOutParams() throws Exception { Statement stmt = con.createStatement(); try { - stmt.executeUpdate("IF EXISTS (select * from sysobjects where id = object_id(N'" + procName - + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + procName); + Utils.dropProcedureIfExists(procName, stmt); } catch (Exception ex) { } @@ -709,6 +716,8 @@ public void testCancelGetOutParams() throws Exception { // Reexecute to prove CS is still good after last cancel cstmt.execute(); + + Utils.dropProcedureIfExists(procName, stmt); con.close(); } @@ -1040,12 +1049,12 @@ public void testConsecutiveQueries() throws Exception { } try { - stmt.executeUpdate("DROP TABLE if exists" + table1Name); + Utils.dropTableIfExists(table1Name, stmt); } catch (SQLException e) { } try { - stmt.executeUpdate("DROP TABLE if exists " + table2Name); + Utils.dropTableIfExists(table2Name, stmt); } catch (SQLException e) { } @@ -1073,7 +1082,7 @@ public void testConsecutiveQueries() throws Exception { * @throws Exception */ @Test - public void testLargeMaxRows_JDBC41() throws Exception { + public void testLargeMaxRowsJDBC41() throws Exception { assumeTrue("JDBC41".equals(Utils.getConfiguredProperty("JDBC_Version")), "Aborting test case as JDBC version is not compatible. "); Connection con = DriverManager.getConnection(connectionString); @@ -1112,7 +1121,7 @@ public void testLargeMaxRows_JDBC41() throws Exception { * @throws Exception */ @Test - public void testLargeMaxRows_JDBC42() throws Exception { + public void testLargeMaxRowsJDBC42() throws Exception { assumeTrue("JDBC42".equals(Utils.getConfiguredProperty("JDBC_Version")), "Aborting test case as JDBC version is not compatible. "); Connection dbcon = DriverManager.getConnection(connectionString); @@ -1132,7 +1141,7 @@ public void testLargeMaxRows_JDBC42() throws Exception { // SQL Server only supports integer limits for setting max rows // If the value MAX_VALUE + 1 is accepted, throw exception try { - newValue = new Long(java.lang.Integer.MAX_VALUE) + 1; + newValue = (long) Integer.MAX_VALUE + 1; dbstmt.setLargeMaxRows(newValue); throw new SQLException("setLargeMaxRows(): Long values should not be set"); } @@ -1163,10 +1172,23 @@ public void testLargeMaxRows_JDBC42() throws Exception { } } + @AfterEach + public void terminate() throws Exception { + try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement();) { + try { + Utils.dropTableIfExists(table1Name, stmt); + Utils.dropTableIfExists(table2Name, stmt); + } + catch (SQLException e) { + } + } + } } @Nested public class TCStatementCallable { + String name = RandomUtil.getIdentifier("p1"); + String procName = AbstractSQLGenerator.escapeIdentifier(name); /** * Tests CallableStatementMethods on jdbc41 @@ -1175,65 +1197,134 @@ public class TCStatementCallable { */ @Test public void testJdbc41CallableStatementMethods() throws Exception { - assumeTrue("JDBC41".equals(Utils.getConfiguredProperty("JDBC_Version")), "Aborting test case as JDBC version is not compatible. "); Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); // Prepare database setup - String name = RandomUtil.getIdentifier("p1"); - String procName = AbstractSQLGenerator.escapeIdentifier(name); - Connection conn = DriverManager.getConnection(connectionString); - Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE); - try { - stmt.executeUpdate("IF EXISTS (select * from sysobjects where id = object_id(N'" + procName - + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + procName); - } - catch (Exception ex) { - } - ; - String query = "create procedure " + procName - + " @col1Value varchar(512) OUTPUT, @col2Value varchar(512) OUTPUT AS BEGIN SET @col1Value='hello' SET @col2Value='world' END"; - stmt.execute(query); + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)) { + String query = "create procedure " + procName + " @col1Value varchar(512) OUTPUT," + " @col2Value int OUTPUT," + + " @col3Value float OUTPUT," + " @col4Value decimal(10,5) OUTPUT," + " @col5Value uniqueidentifier OUTPUT," + + " @col6Value xml OUTPUT," + " @col7Value varbinary(max) OUTPUT," + " @col8Value text OUTPUT," + " @col9Value ntext OUTPUT," + + " @col10Value varbinary(max) OUTPUT," + " @col11Value date OUTPUT," + " @col12Value time OUTPUT," + + " @col13Value datetime2 OUTPUT," + " @col14Value datetimeoffset OUTPUT" + " AS BEGIN " + " SET @col1Value = 'hello'" + + " SET @col2Value = 1" + " SET @col3Value = 2.0" + " SET @col4Value = 123.45" + + " SET @col5Value = '6F9619FF-8B86-D011-B42D-00C04FC964FF'" + " SET @col6Value = ''" + + " SET @col7Value = 0x63C34D6BCAD555EB64BF7E848D02C376" + " SET @col8Value = 'text'" + " SET @col9Value = 'ntext'" + + " SET @col10Value = 0x63C34D6BCAD555EB64BF7E848D02C376" + " SET @col11Value = '2017-05-19'" + + " SET @col12Value = '10:47:15.1234567'" + " SET @col13Value = '2017-05-19T10:47:15.1234567'" + + " SET @col14Value = '2017-05-19T10:47:15.1234567+02:00'" + " END"; + stmt.execute(query); + + // Test JDBC 4.1 methods for CallableStatement + try (CallableStatement cstmt = conn.prepareCall("{call " + procName + "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)}")) { + cstmt.registerOutParameter(1, java.sql.Types.VARCHAR); + cstmt.registerOutParameter(2, java.sql.Types.INTEGER); + cstmt.registerOutParameter(3, java.sql.Types.FLOAT); + cstmt.registerOutParameter(4, java.sql.Types.DECIMAL); + cstmt.registerOutParameter(5, microsoft.sql.Types.GUID); + cstmt.registerOutParameter(6, java.sql.Types.SQLXML); + cstmt.registerOutParameter(7, java.sql.Types.VARBINARY); + cstmt.registerOutParameter(8, java.sql.Types.CLOB); + cstmt.registerOutParameter(9, java.sql.Types.NCLOB); + cstmt.registerOutParameter(10, java.sql.Types.VARBINARY); + cstmt.registerOutParameter(11, java.sql.Types.DATE); + cstmt.registerOutParameter(12, java.sql.Types.TIME); + cstmt.registerOutParameter(13, java.sql.Types.TIMESTAMP); + cstmt.registerOutParameter(14, java.sql.Types.TIMESTAMP_WITH_TIMEZONE); + cstmt.execute(); + + assertEquals("hello", cstmt.getObject(1, String.class)); + assertEquals("hello", cstmt.getObject("col1Value", String.class)); + + assertEquals(Integer.valueOf(1), cstmt.getObject(2, Integer.class)); + assertEquals(Integer.valueOf(1), cstmt.getObject("col2Value", Integer.class)); + + assertEquals(2.0f, cstmt.getObject(3, Float.class), 0.0001f); + assertEquals(2.0f, cstmt.getObject("col3Value", Float.class), 0.0001f); + assertEquals(2.0d, cstmt.getObject(3, Double.class), 0.0001d); + assertEquals(2.0d, cstmt.getObject("col3Value", Double.class), 0.0001d); + + // BigDecimal#equals considers the number of decimal places + assertEquals(0, cstmt.getObject(4, BigDecimal.class).compareTo(new BigDecimal("123.45"))); + assertEquals(0, cstmt.getObject("col4Value", BigDecimal.class).compareTo(new BigDecimal("123.45"))); + + assertEquals(UUID.fromString("6F9619FF-8B86-D011-B42D-00C04FC964FF"), cstmt.getObject(5, UUID.class)); + assertEquals(UUID.fromString("6F9619FF-8B86-D011-B42D-00C04FC964FF"), cstmt.getObject("col5Value", UUID.class)); + + SQLXML sqlXml; + sqlXml = cstmt.getObject(6, SQLXML.class); + try { + assertEquals("", sqlXml.getString()); + } + finally { + sqlXml.free(); + } - // Test JDBC 4.1 methods for CallableStatement - CallableStatement cstmt = conn.prepareCall("{call " + procName + "(?, ?)}"); - cstmt.registerOutParameter(1, java.sql.Types.VARCHAR); - cstmt.registerOutParameter(2, java.sql.Types.VARCHAR); - cstmt.execute(); + Blob blob; + blob = cstmt.getObject(7, Blob.class); + try { + assertArrayEquals(new byte[] {0x63, (byte) 0xC3, 0x4D, 0x6B, (byte) 0xCA, (byte) 0xD5, 0x55, (byte) 0xEB, 0x64, (byte) 0xBF, + 0x7E, (byte) 0x84, (byte) 0x8D, 0x02, (byte) 0xC3, 0x76}, blob.getBytes(1, 16)); + } + finally { + blob.free(); + } - try { - String out1 = cstmt.getObject(1, String.class); - } - catch (Exception e) { + Clob clob; + clob = cstmt.getObject(8, Clob.class); + try { + assertEquals("text", clob.getSubString(1, 4)); + } + finally { + clob.free(); + } + + NClob nclob; + nclob = cstmt.getObject(9, NClob.class); + try { + assertEquals("ntext", nclob.getSubString(1, 5)); + } + finally { + nclob.free(); + } - fail(e.toString()); + assertArrayEquals(new byte[] {0x63, (byte) 0xC3, 0x4D, 0x6B, (byte) 0xCA, (byte) 0xD5, 0x55, (byte) 0xEB, 0x64, (byte) 0xBF, 0x7E, + (byte) 0x84, (byte) 0x8D, 0x02, (byte) 0xC3, 0x76}, cstmt.getObject(10, byte[].class)); + assertEquals(java.sql.Date.valueOf("2017-05-19"), cstmt.getObject(11, java.sql.Date.class)); + assertEquals(java.sql.Date.valueOf("2017-05-19"), cstmt.getObject("col11Value", java.sql.Date.class)); - } - try { - String out2 = cstmt.getObject("col2Value", String.class); - } - catch (Exception e) { + java.sql.Time expectedTime = new java.sql.Time(java.sql.Time.valueOf("10:47:15").getTime() + 123L); + assertEquals(expectedTime, cstmt.getObject(12, java.sql.Time.class)); + assertEquals(expectedTime, cstmt.getObject("col12Value", java.sql.Time.class)); - fail(e.toString()); - } + assertEquals(java.sql.Timestamp.valueOf("2017-05-19 10:47:15.1234567"), cstmt.getObject(13, java.sql.Timestamp.class)); + assertEquals(java.sql.Timestamp.valueOf("2017-05-19 10:47:15.1234567"), cstmt.getObject("col13Value", java.sql.Timestamp.class)); - try { - stmt.executeUpdate("IF EXISTS (select * from sysobjects where id = object_id(N'" + procName - + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + procName); + assertEquals("2017-05-19 10:47:15.1234567 +02:00", cstmt.getObject(14, microsoft.sql.DateTimeOffset.class).toString()); + assertEquals("2017-05-19 10:47:15.1234567 +02:00", cstmt.getObject("col14Value", microsoft.sql.DateTimeOffset.class).toString()); + } } - catch (Exception ex) { + } + + @AfterEach + public void terminate() throws Exception { + try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement()) { + try { + Utils.dropProcedureIfExists(procName, stmt); + } + catch (SQLException e) { + fail(e.toString()); + } } - ; - stmt.close(); - cstmt.close(); - conn.close(); } + } @Nested public class TCStatementParam { String tableNameTemp = RandomUtil.getIdentifier("TCStatementParam"); private final String tableName = AbstractSQLGenerator.escapeIdentifier(tableNameTemp); - String procNameTemp = RandomUtil.getIdentifier("p1"); + String procNameTemp = "TCStatementParam"; private final String procName = AbstractSQLGenerator.escapeIdentifier(procNameTemp); /** @@ -1254,10 +1345,8 @@ public void testStatementOutParamGetsTwice() throws Exception { log.fine("testStatementOutParamGetsTwice threw: " + e.getMessage()); } - stmt.executeUpdate( - "if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[sp_ouputP]') and OBJECTPROPERTY(id, N'IsProcedure') = 1) DROP PROCEDURE [sp_ouputP]"); - stmt.executeUpdate( - "CREATE PROCEDURE [sp_ouputP] ( @p2_smallint smallint, @p3_smallint_out smallint OUTPUT) AS SELECT @p3_smallint_out=@p2_smallint RETURN @p2_smallint + 1"); + stmt.executeUpdate("CREATE PROCEDURE " + procNameTemp + + " ( @p2_smallint smallint, @p3_smallint_out smallint OUTPUT) AS SELECT @p3_smallint_out=@p2_smallint RETURN @p2_smallint + 1"); ResultSet rs = stmt.getResultSet(); if (rs != null) { @@ -1267,7 +1356,7 @@ public void testStatementOutParamGetsTwice() throws Exception { else { assertEquals(stmt.isClosed(), false, "testStatementOutParamGetsTwice: statement should be open since no resultset."); } - CallableStatement cstmt = con.prepareCall("{ ? = CALL [sp_ouputP] (?,?)}"); + CallableStatement cstmt = con.prepareCall("{ ? = CALL " + procNameTemp + " (?,?)}"); cstmt.registerOutParameter(1, Types.INTEGER); cstmt.setObject(2, Short.valueOf("32"), Types.SMALLINT); cstmt.registerOutParameter(3, Types.SMALLINT); @@ -1295,12 +1384,10 @@ public void testStatementOutManyParamGetsTwiceRandomOrder() throws Exception { Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement(); - stmt.executeUpdate( - "if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[sp_ouputMP]') and OBJECTPROPERTY(id, N'IsProcedure') = 1) DROP PROCEDURE [sp_ouputMP]"); - stmt.executeUpdate( - "CREATE PROCEDURE [sp_ouputMP] ( @p2_smallint smallint, @p3_smallint_out smallint OUTPUT, @p4_smallint smallint OUTPUT, @p5_smallint_out smallint OUTPUT) AS SELECT @p3_smallint_out=@p2_smallint, @p5_smallint_out=@p4_smallint RETURN @p2_smallint + 1"); + stmt.executeUpdate("CREATE PROCEDURE " + procNameTemp + + " ( @p2_smallint smallint, @p3_smallint_out smallint OUTPUT, @p4_smallint smallint OUTPUT, @p5_smallint_out smallint OUTPUT) AS SELECT @p3_smallint_out=@p2_smallint, @p5_smallint_out=@p4_smallint RETURN @p2_smallint + 1"); - CallableStatement cstmt = con.prepareCall("{ ? = CALL [sp_ouputMP] (?,?, ?, ?)}"); + CallableStatement cstmt = con.prepareCall("{ ? = CALL " + procNameTemp + " (?,?, ?, ?)}"); cstmt.registerOutParameter(1, Types.INTEGER); cstmt.setObject(2, Short.valueOf("32"), Types.SMALLINT); cstmt.registerOutParameter(3, Types.SMALLINT); @@ -1317,10 +1404,6 @@ public void testStatementOutManyParamGetsTwiceRandomOrder() throws Exception { assertEquals(cstmt.getInt(3), 34, "Wrong value"); assertEquals(cstmt.getInt(5), 24, "Wrong value"); assertEquals(cstmt.getInt(1), 35, "Wrong value"); - - stmt.executeUpdate( - "if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[sp_ouputMP]') and OBJECTPROPERTY(id, N'IsProcedure') = 1) DROP PROCEDURE [sp_ouputMP]"); - } /** @@ -1333,12 +1416,10 @@ public void testStatementOutParamGetsTwiceInOut() throws Exception { Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement(); - stmt.executeUpdate( - "if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[sp_ouputP]') and OBJECTPROPERTY(id, N'IsProcedure') = 1) DROP PROCEDURE [sp_ouputP]"); - stmt.executeUpdate( - "CREATE PROCEDURE [sp_ouputP] ( @p2_smallint smallint, @p3_smallint_out smallint OUTPUT) AS SELECT @p3_smallint_out=@p3_smallint_out +1 RETURN @p2_smallint + 1"); + stmt.executeUpdate("CREATE PROCEDURE " + procNameTemp + + " ( @p2_smallint smallint, @p3_smallint_out smallint OUTPUT) AS SELECT @p3_smallint_out=@p3_smallint_out +1 RETURN @p2_smallint + 1"); - CallableStatement cstmt = con.prepareCall("{ ? = CALL [sp_ouputP] (?,?)}"); + CallableStatement cstmt = con.prepareCall("{ ? = CALL " + procNameTemp + " (?,?)}"); cstmt.registerOutParameter(1, Types.INTEGER); cstmt.setObject(2, Short.valueOf("1"), Types.SMALLINT); cstmt.setObject(3, Short.valueOf("100"), Types.SMALLINT); @@ -1351,7 +1432,6 @@ public void testStatementOutParamGetsTwiceInOut() throws Exception { cstmt.execute(); assertEquals(cstmt.getInt(1), 11, "Wrong value"); assertEquals(cstmt.getInt(3), 101, "Wrong value"); - } /** @@ -1365,19 +1445,6 @@ public void testResultSetParams() throws Exception { Connection conn = DriverManager.getConnection(connectionString); Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE); - try { - stmt.executeUpdate("drop table if exists " + tableName); - } - catch (Exception ex) { - } - ; - try { - stmt.executeUpdate("IF EXISTS (select * from sysobjects where id = object_id(N'" + procName - + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + procName); - } - catch (Exception ex) { - } - ; stmt.executeUpdate("create table " + tableName + " (col1 int, col2 text, col3 int identity(1,1) primary key)"); stmt.executeUpdate("Insert into " + tableName + " values(0, 'hello')"); stmt.executeUpdate("Insert into " + tableName + " values(0, 'hi')"); @@ -1392,20 +1459,6 @@ public void testResultSetParams() throws Exception { rs.next(); assertEquals(rs.getString(2), "hello", "Wrong value"); assertEquals(cstmt.getString(2), "hi", "Wrong value"); - - try { - stmt.executeUpdate("drop table if exists " + tableName); - } - catch (Exception ex) { - } - ; - try { - stmt.executeUpdate("IF EXISTS (select * from sysobjects where id = object_id(N'" + procName - + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + procName); - } - catch (Exception ex) { - } - ; } /** @@ -1415,26 +1468,10 @@ public void testResultSetParams() throws Exception { */ @Test public void testResultSetNullParams() throws Exception { - String temp = RandomUtil.getIdentifier("RetestResultSetNullParams"); - String tableName = AbstractSQLGenerator.escapeIdentifier(temp); - Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); Connection conn = DriverManager.getConnection(connectionString); Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE); - try { - stmt.executeUpdate("drop table if exists" + tableName); - } - catch (Exception ex) { - } - ; - try { - stmt.executeUpdate("IF EXISTS (select * from sysobjects where id = object_id(N'" + procName - + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + procName); - } - catch (Exception ex) { - } - ; stmt.executeUpdate("create table " + tableName + " (col1 int, col2 text, col3 int identity(1,1) primary key)"); stmt.executeUpdate("Insert into " + tableName + " values(0, 'hello')"); stmt.executeUpdate("Insert into " + tableName + " values(0, 'hi')"); @@ -1452,20 +1489,6 @@ public void testResultSetNullParams() throws Exception { throw ex; } ; - - try { - stmt.executeUpdate("drop table if exists " + tableName); - } - catch (Exception ex) { - } - ; - try { - stmt.executeUpdate("IF EXISTS (select * from sysobjects where id = object_id(N'" + procName - + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + procName); - } - catch (Exception ex) { - } - ; } /** @@ -1477,12 +1500,7 @@ public void testFailedToResumeTransaction() throws Exception { Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); Connection conn = DriverManager.getConnection(connectionString); Statement stmt = conn.createStatement(); - try { - stmt.executeUpdate("drop table if exists " + tableName); - } - catch (Exception ex) { - } - ; + stmt.executeUpdate("create table " + tableName + " (col1 int primary key)"); stmt.executeUpdate("Insert into " + tableName + " values(0)"); stmt.executeUpdate("Insert into " + tableName + " values(1)"); @@ -1503,12 +1521,6 @@ public void testFailedToResumeTransaction() throws Exception { } catch (SQLException ex) { } - try { - stmt.executeUpdate("drop table if exists " + tableName); - } - catch (Exception ex) { - } - ; conn.close(); } @@ -1522,19 +1534,6 @@ public void testResultSetErrors() throws Exception { Connection conn = DriverManager.getConnection(connectionString); Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE); - try { - stmt.executeUpdate("drop table if exists " + tableName); - } - catch (Exception ex) { - } - ; - try { - stmt.executeUpdate("IF EXISTS (select * from sysobjects where id = object_id(N'" + procName - + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + procName); - } - catch (Exception ex) { - } - ; stmt.executeUpdate("create table " + tableName + " (col1 int, col2 text, col3 int identity(1,1) primary key)"); stmt.executeUpdate("Insert into " + tableName + " values(0, 'hello')"); stmt.executeUpdate("Insert into " + tableName + " values(0, 'hi')"); @@ -1554,45 +1553,20 @@ public void testResultSetErrors() throws Exception { ; assertEquals(null, cstmt.getString(2), "Wrong value"); - - try { - stmt.executeUpdate("drop table if exists " + tableName); - } - catch (Exception ex) { - } - ; - try { - stmt.executeUpdate("IF EXISTS (select * from sysobjects where id = object_id(N'" + procName - + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + procName); - } - catch (Exception ex) { - } - ; } /** * Verify proper handling of row errors in ResultSets. */ @Test + @Disabled + // TODO: We are commenting this out due to random AppVeyor failures. We will investigate later. public void testRowError() throws Exception { Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); Connection conn = DriverManager.getConnection(connectionString); // Set up everything Statement stmt = conn.createStatement(); - try { - stmt.executeUpdate("drop table if exists " + tableName); - } - catch (Exception ex) { - } - ; - try { - stmt.executeUpdate("IF EXISTS (select * from sysobjects where id = object_id(N'" + procName - + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + procName); - } - catch (Exception ex) { - } - ; stmt.executeUpdate("create table " + tableName + " (col1 int primary key)"); stmt.executeUpdate("insert into " + tableName + " values(0)"); @@ -1680,23 +1654,22 @@ public void testRowError() throws Exception { testConn2.close(); testConn1.close(); } - - try { - stmt.executeUpdate("drop table if exists" + tableName); - } - catch (Exception ex) { - } - ; - try { - stmt.executeUpdate("IF EXISTS (select * from sysobjects where id = object_id(N'" + procName - + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + procName); - } - catch (Exception ex) { - } - ; stmt.close(); conn.close(); } + + @AfterEach + public void terminate() throws Exception { + try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement()) { + try { + Utils.dropTableIfExists(tableName, stmt); + Utils.dropProcedureIfExists(procName, stmt); + } + catch (SQLException e) { + fail(e.toString()); + } + } + } } @Nested @@ -1714,11 +1687,6 @@ private Connection createConnectionAndPopulateData() throws Exception { con = ds.getConnection(); Statement stmt = con.createStatement(); - try { - stmt.executeUpdate("DROP TABLE IF EXISTS " + tableName); - } - catch (SQLException e) { - } stmt.executeUpdate("CREATE TABLE " + tableName + "(col1_int int PRIMARY KEY IDENTITY(1,1), col2_varchar varchar(200), col3_varchar varchar(20) SPARSE NULL, col4_smallint smallint SPARSE NULL, col5_xml XML COLUMN_SET FOR ALL_SPARSE_COLUMNS, col6_nvarcharMax NVARCHAR(MAX), col7_varcharMax VARCHAR(MAX))"); @@ -1728,19 +1696,15 @@ private Connection createConnectionAndPopulateData() throws Exception { return con; } - private void cleanup(Connection con) throws Exception { - try { - if (con == null || con.isClosed()) { - con = DriverManager.getConnection(connectionString); + @AfterEach + public void terminate() throws Exception { + try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement()) { + try { + Utils.dropTableIfExists(tableName, stmt); + } + catch (SQLException e) { + fail(e.toString()); } - - con.createStatement().executeUpdate("DROP TABLE IF EXISTS " + tableName); - } - catch (SQLException e) { - } - finally { - if (con != null) - con.close(); } } @@ -1773,7 +1737,7 @@ public void testNBCROWNullsForLOBs() throws Exception { } } finally { - cleanup(con); + terminate(); } } @@ -1815,7 +1779,7 @@ public void testSparseColumnSetValues() throws Exception { } } finally { - cleanup(con); + terminate(); } } @@ -1858,7 +1822,7 @@ public void testSparseColumnSetIndex() throws Exception { } } finally { - cleanup(con); + terminate(); } } @@ -1870,66 +1834,64 @@ public void testSparseColumnSetIndex() throws Exception { */ @Test public void testSparseColumnSetForException() throws Exception { - if (new DBConnection(connectionString).getServerVersion() <= 9.0) { - log.fine("testSparseColumnSetForException skipped for Yukon"); + try (DBConnection conn = new DBConnection(connectionString)) { + if (conn.getServerVersion() <= 9.0) { + log.fine("testSparseColumnSetForException skipped for Yukon"); + } } - Connection con = null; - try { - con = createConnectionAndPopulateData(); - Statement stmt = con.createStatement(); - // enable isCloseOnCompletion - try { - stmt.closeOnCompletion(); - } - catch (Exception e) { + con = createConnectionAndPopulateData(); + Statement stmt = con.createStatement(); - throw new SQLException("testSparseColumnSetForException threw exception: ", e); + // enable isCloseOnCompletion + try { + stmt.closeOnCompletion(); + } + catch (Exception e) { - } + throw new SQLException("testSparseColumnSetForException threw exception: ", e); - String selectQuery = "SELECT * FROM " + tableName; - ResultSet rs = stmt.executeQuery(selectQuery); - rs.next(); + } - SQLServerResultSetMetaData rsmd = (SQLServerResultSetMetaData) rs.getMetaData(); - try { - // test that an exception is thrown when result set is closed - rs.close(); - rsmd.isSparseColumnSet(1); - assertEquals(true, false, "Should not reach here. An exception should have been thrown"); - } - catch (SQLException e) { - } + String selectQuery = "SELECT * FROM " + tableName; + ResultSet rs = stmt.executeQuery(selectQuery); + rs.next(); - // test that an exception is thrown when statement is closed - try { - rs = stmt.executeQuery(selectQuery); - rsmd = (SQLServerResultSetMetaData) rs.getMetaData(); + SQLServerResultSetMetaData rsmd = (SQLServerResultSetMetaData) rs.getMetaData(); + try { + // test that an exception is thrown when result set is closed + rs.close(); + rsmd.isSparseColumnSet(1); + assertEquals(true, false, "Should not reach here. An exception should have been thrown"); + } + catch (SQLException e) { + } - assertEquals(stmt.isClosed(), true, "testSparseColumnSetForException: statement should be closed since resultset is closed."); - stmt.close(); - rsmd.isSparseColumnSet(1); - assertEquals(true, false, "Should not reach here. An exception should have been thrown"); - } - catch (SQLException e) { - } + // test that an exception is thrown when statement is closed + try { + rs = stmt.executeQuery(selectQuery); + rsmd = (SQLServerResultSetMetaData) rs.getMetaData(); - // test that an exception is thrown when connection is closed - try { - rs = con.createStatement().executeQuery("SELECT * FROM " + tableName); - rsmd = (SQLServerResultSetMetaData) rs.getMetaData(); - con.close(); - rsmd.isSparseColumnSet(1); - assertEquals(true, false, "Should not reach here. An exception should have been thrown"); - } - catch (SQLException e) { - } + assertEquals(stmt.isClosed(), true, "testSparseColumnSetForException: statement should be closed since resultset is closed."); + stmt.close(); + rsmd.isSparseColumnSet(1); + assertEquals(true, false, "Should not reach here. An exception should have been thrown"); } - finally { - cleanup(con); + catch (SQLException e) { } + + // test that an exception is thrown when connection is closed + try { + rs = con.createStatement().executeQuery("SELECT * FROM " + tableName); + rsmd = (SQLServerResultSetMetaData) rs.getMetaData(); + con.close(); + rsmd.isSparseColumnSet(1); + assertEquals(true, false, "Should not reach here. An exception should have been thrown"); + } + catch (SQLException e) { + } + } /** @@ -1952,7 +1914,7 @@ public void testNBCRowForAllNulls() throws Exception { con = ds.getConnection(); Statement stmt = con.createStatement(); try { - stmt.executeUpdate("DROP TABLE IF EXISTS" + tableName); + Utils.dropTableIfExists(tableName, stmt); } catch (SQLException e) { } @@ -1977,7 +1939,7 @@ public void testNBCRowForAllNulls() throws Exception { } finally { - cleanup(con); + terminate(); } } @@ -2001,7 +1963,7 @@ public void testNBCROWWithRandomAccess() throws Exception { con = ds.getConnection(); Statement stmt = con.createStatement(); try { - stmt.executeUpdate("DROP TABLE IF EXISTS " + tableName); + Utils.dropTableIfExists(tableName, stmt); } catch (SQLException e) { } @@ -2021,7 +1983,7 @@ public void testNBCROWWithRandomAccess() throws Exception { Random r = new Random(); // randomly generate columns whose values would be set to a non null value - ArrayList nonNullColumns = new ArrayList(); + ArrayList nonNullColumns = new ArrayList<>(); nonNullColumns.add(1);// this is always non-null // Add approximately 10 non-null columns. The number should be low @@ -2083,7 +2045,7 @@ public void testNBCROWWithRandomAccess() throws Exception { } } finally { - cleanup(con); + terminate(); } } @@ -2300,25 +2262,7 @@ private void setup() throws Exception { Connection con = DriverManager.getConnection(connectionString); con.setAutoCommit(false); Statement stmt = con.createStatement(); - try { - stmt.executeUpdate("DROP TABLE if exists" + tableName); - } - catch (SQLException e) { - throw new SQLException(e); - } - try { - stmt.executeUpdate("DROP TABLE if exists" + table2Name); - } - catch (SQLException e) { - throw new SQLException(e); - } - try { - stmt.executeUpdate("IF EXISTS (select * from sysobjects where id = object_id(N'" + sprocName - + "') and OBJECTPROPERTY(id, N'IsProcedure') = 1)" + " DROP PROCEDURE " + sprocName); - } - catch (SQLException e) { - throw new SQLException(e); - } + try { stmt.executeUpdate("if EXISTS (SELECT * FROM sys.triggers where name = '" + triggerName + "') drop trigger " + triggerName); } @@ -2424,6 +2368,20 @@ public void testStatementInsertExecInsert() throws Exception { // which should have affected 1 (new) row in tableName. assertEquals(updateCount, 1, "Wrong update count"); } + + @AfterEach + public void terminate() throws Exception { + try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement();) { + try { + Utils.dropTableIfExists(tableName, stmt); + Utils.dropTableIfExists(table2Name, stmt); + Utils.dropProcedureIfExists(sprocName, stmt); + } + catch (SQLException e) { + fail(e.toString()); + } + } + } } @Nested @@ -2439,11 +2397,7 @@ private void setup() throws Exception { Connection con = DriverManager.getConnection(connectionString); con.setAutoCommit(false); Statement stmt = con.createStatement(); - try { - stmt.executeUpdate("DROP TABLE if exists" + tableName); - } - catch (SQLException e) { - } + try { stmt.executeUpdate("if EXISTS (SELECT * FROM sys.triggers where name = '" + triggerName + "') drop trigger " + triggerName); } @@ -2540,7 +2494,7 @@ public void testUpdateCountAfterRaiseError() throws Exception { * @throws Exception */ @Test - public void testUpdateCountAfterErrorInTrigger_LastUpdateCountFalse() throws Exception { + public void testUpdateCountAfterErrorInTriggerLastUpdateCountFalse() throws Exception { Connection con = DriverManager.getConnection(connectionString + ";lastUpdateCount = false"); PreparedStatement pstmt = con.prepareStatement("INSERT INTO " + tableName + " VALUES (5)"); @@ -2591,7 +2545,7 @@ public void testUpdateCountAfterErrorInTrigger_LastUpdateCountFalse() throws Exc * @throws Exception */ @Test - public void testUpdateCountAfterErrorInTrigger_LastUpdateCountTrue() throws Exception { + public void testUpdateCountAfterErrorInTriggerLastUpdateCountTrue() throws Exception { Connection con = DriverManager.getConnection(connectionString + ";lastUpdateCount = true"); PreparedStatement pstmt = con.prepareStatement("INSERT INTO " + tableName + " VALUES (5)"); @@ -2629,6 +2583,18 @@ public void testUpdateCountAfterErrorInTrigger_LastUpdateCountTrue() throws Exce pstmt.close(); con.close(); } + + @AfterEach + public void terminate() throws Exception { + try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement();) { + try { + Utils.dropTableIfExists(tableName, stmt); + } + catch (SQLException e) { + fail(e.toString()); + } + } + } } @Nested @@ -2653,12 +2619,6 @@ private void setup() throws Exception { throw new SQLException("setup threw exception: ", e); } - - try { - stmt.executeUpdate("DROP TABLE if exists" + tableName); - } - catch (SQLException e) { - } stmt.executeUpdate("CREATE TABLE " + tableName + " (col1 INT primary key)"); for (int i = 0; i < NUM_ROWS; i++) stmt.executeUpdate("INSERT INTO " + tableName + " (col1) VALUES (" + i + ")"); @@ -2677,26 +2637,36 @@ private void setup() throws Exception { @Test public void testNoCountWithExecute() throws Exception { // Ensure lastUpdateCount=true... - Connection con = DriverManager.getConnection(connectionString + ";lastUpdateCount = true"); - Statement stmt = con.createStatement(); - boolean isResultSet = stmt - .execute("set nocount on\n" + "insert into " + tableName + "(col1) values(" + (NUM_ROWS + 1) + ")\n" + "select 1"); + try (Connection con = DriverManager.getConnection(connectionString + ";lastUpdateCount = true"); + Statement stmt = con.createStatement();) { - assertEquals(true, isResultSet, "execute() said first result was an update count"); + boolean isResultSet = stmt + .execute("set nocount on\n" + "insert into " + tableName + "(col1) values(" + (NUM_ROWS + 1) + ")\n" + "select 1"); - ResultSet rs = stmt.getResultSet(); - while (rs.next()) - ; - rs.close(); + assertEquals(true, isResultSet, "execute() said first result was an update count"); - boolean moreResults = stmt.getMoreResults(); - assertEquals(false, moreResults, "next result is a ResultSet?"); + ResultSet rs = stmt.getResultSet(); + while (rs.next()); + rs.close(); - int updateCount = stmt.getUpdateCount(); - assertEquals(-1, updateCount, "only one result was expected..."); + boolean moreResults = stmt.getMoreResults(); + assertEquals(false, moreResults, "next result is a ResultSet?"); - stmt.close(); - con.close(); + int updateCount = stmt.getUpdateCount(); + assertEquals(-1, updateCount, "only one result was expected..."); + } + } + + @AfterEach + public void terminate() throws Exception { + try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement()) { + try { + Utils.dropTableIfExists(tableName, stmt); + } + catch (SQLException e) { + fail(e.toString()); + } + } } } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/Wrapper42Test.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/Wrapper42Test.java new file mode 100644 index 000000000..8322deb60 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/Wrapper42Test.java @@ -0,0 +1,96 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.unit.statement; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerCallableStatement; +import com.microsoft.sqlserver.jdbc.SQLServerCallableStatement42; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement42; +import com.microsoft.sqlserver.testframework.AbstractTest; + +/** + * Test SQLServerPreparedSatement42 and SQLServerCallableSatement42 classes + * + */ +@RunWith(JUnitPlatform.class) +public class Wrapper42Test extends AbstractTest { + static Connection connection = null; + double javaVersion = Double.parseDouble(System.getProperty("java.specification.version")); + static int major; + static int minor; + + /** + * Tests creation of SQLServerPreparedSatement42 object + * + * @throws SQLException + */ + @Test + public void PreparedSatement42Test() throws SQLException { + String sql = "SELECT SUSER_SNAME()"; + + PreparedStatement pstmt = connection.prepareStatement(sql); + + if (1.8d <= javaVersion && 4 == major && 2 == minor) { + assertTrue(pstmt instanceof SQLServerPreparedStatement42); + } + else { + assertTrue(pstmt instanceof SQLServerPreparedStatement); + } + } + + /** + * Tests creation of SQLServerCallableStatement42 object + * + * @throws SQLException + */ + @Test + public void CallableStatement42Test() throws SQLException { + String sql = "SELECT SUSER_SNAME()"; + + CallableStatement cstmt = connection.prepareCall(sql); + + if (1.8d <= javaVersion && 4 == major && 2 == minor) { + assertTrue(cstmt instanceof SQLServerCallableStatement42); + } + else { + assertTrue(cstmt instanceof SQLServerCallableStatement); + } + } + + @BeforeAll + private static void setupConnection() throws SQLException { + connection = DriverManager.getConnection(connectionString); + + DatabaseMetaData metadata = connection.getMetaData(); + major = metadata.getJDBCMajorVersion(); + minor = metadata.getJDBCMinorVersion(); + } + + @AfterAll + private static void terminateVariation() throws SQLException { + if (null != connection) { + connection.close(); + } + } + +} diff --git a/src/test/java/com/microsoft/sqlserver/testframework/AbstractSQLGenerator.java b/src/test/java/com/microsoft/sqlserver/testframework/AbstractSQLGenerator.java index dd7dc1c4b..e10ad94e0 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/AbstractSQLGenerator.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/AbstractSQLGenerator.java @@ -22,6 +22,7 @@ public abstract class AbstractSQLGenerator {// implements ISQLGenerator { protected static final String PRIMARY_KEY = "PRIMARY KEY"; protected static final String DEFAULT = "DEFAULT"; protected static final String COMMA = ","; + protected static final String QUESTION_MARK = "?"; // FIXME: Find good word for '. Better replaced by wrapIdentifier. protected static final String TICK = "'"; diff --git a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java index 8770ed352..008cb01c7 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java @@ -10,7 +10,12 @@ import java.sql.Connection; import java.util.Properties; +import java.util.logging.ConsoleHandler; +import java.util.logging.FileHandler; +import java.util.logging.Handler; +import java.util.logging.Level; import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -55,6 +60,8 @@ public abstract class AbstractTest { */ @BeforeAll public static void setup() throws Exception { + // Invoke fine logging... + invokeLogging(); applicationClientID = getConfiguredProperty("applicationClientID"); applicationKey = getConfiguredProperty("applicationKey"); @@ -77,7 +84,9 @@ public static void setup() throws Exception { try { Assertions.assertNotNull(connectionString, "Connection String should not be null"); + System.out.println(connectionString); connection = PrepUtil.getConnection(connectionString, info); + } catch (Exception e) { throw e; @@ -86,6 +95,7 @@ public static void setup() throws Exception { /** * Get the connection String + * * @return */ public static String getConnectionString() { @@ -133,4 +143,44 @@ public static String getConfiguredProperty(String key, return Utils.getConfiguredProperty(key, defaultValue); } + /** + * Invoke logging. + */ + public static void invokeLogging() { + Handler handler = null; + + String enableLogging = getConfiguredProperty("mssql_jdbc_logging", "false"); + + // If logging is not enable then return. + if (!"true".equalsIgnoreCase(enableLogging)) { + return; + } + + String loggingHandler = getConfiguredProperty("mssql_jdbc_logging_handler", "not_configured"); + + try { + // handler = new FileHandler("Driver.log"); + if ("console".equalsIgnoreCase(loggingHandler)) { + handler = new ConsoleHandler(); + } + else if ("file".equalsIgnoreCase(loggingHandler)) { + handler = new FileHandler("Driver.log"); + System.out.println("Look for Driver.log file in your classpath for detail logs"); + } + + if (handler != null) { + handler.setFormatter(new SimpleFormatter()); + handler.setLevel(Level.FINEST); + Logger.getLogger("").addHandler(handler); + } + // By default, Loggers also send their output to their parent logger.   + // Typically the root Logger is configured with a set of Handlers that essentially act as default handlers for all loggers.  + Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc"); + logger.setLevel(Level.FINEST); + } + catch (Exception e) { + System.err.println("Somehow could not invoke logging: " + e.getMessage()); + } + } + } diff --git a/src/test/java/com/microsoft/sqlserver/testframework/DBCoercion.java b/src/test/java/com/microsoft/sqlserver/testframework/DBCoercion.java index 505cf1b25..e367a94b8 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/DBCoercion.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/DBCoercion.java @@ -45,9 +45,8 @@ public DBCoercion(Class type) { public DBCoercion(Class type, int[] tempflags) { name = type.toString(); - type = type; - for (int i = 0; i < tempflags.length; i++) - flags.set(tempflags[i]); + this.type = type; + for (int tempflag : tempflags) flags.set(tempflag); } /** diff --git a/src/test/java/com/microsoft/sqlserver/testframework/DBCoercions.java b/src/test/java/com/microsoft/sqlserver/testframework/DBCoercions.java index c333d71df..ff8e03f8f 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/DBCoercions.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/DBCoercions.java @@ -19,7 +19,7 @@ public class DBCoercions extends DBItems { * constructor */ public DBCoercions() { - coercionsList = new ArrayList(); + coercionsList = new ArrayList<>(); } public DBCoercions(DBCoercion coercion) { diff --git a/src/test/java/com/microsoft/sqlserver/testframework/DBColumn.java b/src/test/java/com/microsoft/sqlserver/testframework/DBColumn.java index 7528e9e5d..f44744983 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/DBColumn.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/DBColumn.java @@ -78,7 +78,7 @@ void setSqlType(SqlType sqlType) { * number of rows */ void populateValues(int rows) { - columnValues = new ArrayList(); + columnValues = new ArrayList<>(); for (int i = 0; i < rows; i++) columnValues.add(sqlType.createdata()); } diff --git a/src/test/java/com/microsoft/sqlserver/testframework/DBConnection.java b/src/test/java/com/microsoft/sqlserver/testframework/DBConnection.java index 51e0c7aa2..a6ae11d07 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/DBConnection.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/DBConnection.java @@ -21,7 +21,7 @@ /* * Wrapper class for SQLServerConnection */ -public class DBConnection extends AbstractParentWrapper { +public class DBConnection extends AbstractParentWrapper implements AutoCloseable { private double serverversion = 0; // TODO: add Isolation Level diff --git a/src/test/java/com/microsoft/sqlserver/testframework/DBItems.java b/src/test/java/com/microsoft/sqlserver/testframework/DBItems.java index a64386ac4..be2999a1b 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/DBItems.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/DBItems.java @@ -22,8 +22,7 @@ public DBItems() { } public DBCoercion find(Class type) { - for (int i = 0; i < coercionsList.size(); i++) { - DBCoercion item = coercionsList.get(i); + for (DBCoercion item : coercionsList) { if (item.type() == type) return item; } diff --git a/src/test/java/com/microsoft/sqlserver/testframework/DBPreparedStatement.java b/src/test/java/com/microsoft/sqlserver/testframework/DBPreparedStatement.java index 6ff5fa6f1..99d9c5ab3 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/DBPreparedStatement.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/DBPreparedStatement.java @@ -30,6 +30,10 @@ public DBPreparedStatement(DBConnection dbconnection) { } /** + * set up internal PreparedStatement with query + * + * @param query + * @return * @throws SQLException * */ @@ -84,4 +88,22 @@ public DBResultSet executeQuery() throws SQLException { return dbresultSet; } + /** + * populate table with values using prepared statement + * + * @param table + * @return true if table is populated + */ + public boolean populateTable(DBTable table) { + return table.populateTableWithPreparedStatement(this); + } + + /** + * + * @return + * @throws SQLException + */ + public boolean execute() throws SQLException { + return pstmt.execute(); + } } \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/testframework/DBResultSet.java b/src/test/java/com/microsoft/sqlserver/testframework/DBResultSet.java index 78bccaaf4..d281effca 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/DBResultSet.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/DBResultSet.java @@ -36,7 +36,7 @@ * */ -public class DBResultSet extends AbstractParentWrapper { +public class DBResultSet extends AbstractParentWrapper implements AutoCloseable { // TODO: add cursors // TODO: add resultSet level holdability @@ -65,6 +65,14 @@ public class DBResultSet extends AbstractParentWrapper { resultSet = internal; } + DBResultSet(DBStatement dbstatement, + ResultSet internal, + DBTable table) { + super(dbstatement, internal, "resultSet"); + resultSet = internal; + currentTable = table; + } + DBResultSet(DBPreparedStatement dbpstmt, ResultSet internal) { super(dbpstmt, internal, "resultSet"); @@ -225,18 +233,18 @@ public void verifydata(int ordinal, switch (metaData.getColumnType(ordinal + 1)) { case java.sql.Types.BIGINT: assertTrue((((Long) expectedData).longValue() == ((Long) retrieved).longValue()), - "Unexpected bigint value, expected: " + ((Long) expectedData).longValue() + " .Retrieved: " + ((Long) retrieved).longValue()); + "Unexpected bigint value, expected: " + (Long) expectedData + " .Retrieved: " + (Long) retrieved); break; case java.sql.Types.INTEGER: assertTrue((((Integer) expectedData).intValue() == ((Integer) retrieved).intValue()), "Unexpected int value, expected : " - + ((Integer) expectedData).intValue() + " ,received: " + ((Integer) retrieved).intValue()); + + (Integer) expectedData + " ,received: " + (Integer) retrieved); break; case java.sql.Types.SMALLINT: case java.sql.Types.TINYINT: assertTrue((((Short) expectedData).shortValue() == ((Short) retrieved).shortValue()), "Unexpected smallint/tinyint value, expected: " - + " " + ((Short) expectedData).shortValue() + " received: " + ((Short) retrieved).shortValue()); + + " " + (Short) expectedData + " received: " + (Short) retrieved); break; case java.sql.Types.BIT: @@ -245,7 +253,7 @@ public void verifydata(int ordinal, else expectedData = false; assertTrue((((Boolean) expectedData).booleanValue() == ((Boolean) retrieved).booleanValue()), "Unexpected bit value, expected: " - + ((Boolean) expectedData).booleanValue() + " ,received: " + ((Boolean) retrieved).booleanValue()); + + (Boolean) expectedData + " ,received: " + (Boolean) retrieved); break; case java.sql.Types.DECIMAL: @@ -256,12 +264,12 @@ public void verifydata(int ordinal, case java.sql.Types.DOUBLE: assertTrue((((Double) expectedData).doubleValue() == ((Double) retrieved).doubleValue()), "Unexpected float value, expected: " - + ((Double) expectedData).doubleValue() + " received: " + ((Double) retrieved).doubleValue()); + + (Double) expectedData + " received: " + (Double) retrieved); break; case java.sql.Types.REAL: assertTrue((((Float) expectedData).floatValue() == ((Float) retrieved).floatValue()), - "Unexpected real value, expected: " + ((Float) expectedData).floatValue() + " received: " + ((Float) retrieved).floatValue()); + "Unexpected real value, expected: " + (Float) expectedData + " received: " + (Float) retrieved); break; case java.sql.Types.VARCHAR: @@ -310,7 +318,7 @@ else if (metaData.getColumnTypeName(ordinal + 1).equalsIgnoreCase("smalldatetime break; case java.sql.Types.BINARY: - assertTrue(parseByte((byte[]) expectedData, (byte[]) retrieved), + assertTrue(Utils.parseByte((byte[]) expectedData, (byte[]) retrieved), " unexpected BINARY value, expected: " + expectedData + " ,received: " + retrieved); break; @@ -324,15 +332,6 @@ else if (metaData.getColumnTypeName(ordinal + 1).equalsIgnoreCase("smalldatetime } } - private boolean parseByte(byte[] expectedData, - byte[] retrieved) { - assertTrue(Arrays.equals(expectedData, Arrays.copyOf(retrieved, expectedData.length)), " unexpected BINARY value, expected"); - for (int i = expectedData.length; i < retrieved.length; i++) { - assertTrue(0 == retrieved[i], "unexpected data BINARY"); - } - return true; - } - /** * * @param idx @@ -351,7 +350,7 @@ public Object getXXX(Object idx, } else if (idx instanceof Integer) { isInteger = true; - intOrdinal = ((Integer) idx).intValue(); + intOrdinal = (Integer) idx; } else { // Otherwise @@ -419,7 +418,7 @@ public boolean previous() throws SQLException { */ public void afterLast() throws SQLException { ((ResultSet) product()).afterLast(); - _currentrow = DBTable.getTotalRows(); + _currentrow = currentTable.getTotalRows(); } /** diff --git a/src/test/java/com/microsoft/sqlserver/testframework/DBSchema.java b/src/test/java/com/microsoft/sqlserver/testframework/DBSchema.java index 826098913..e08995346 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/DBSchema.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/DBSchema.java @@ -52,7 +52,7 @@ public class DBSchema { * @param autoGenerateSchema */ DBSchema(boolean autoGenerateSchema) { - sqlTypes = new ArrayList(); + sqlTypes = new ArrayList<>(); if (autoGenerateSchema) { // Exact Numeric sqlTypes.add(new SqlBigInt()); diff --git a/src/test/java/com/microsoft/sqlserver/testframework/DBStatement.java b/src/test/java/com/microsoft/sqlserver/testframework/DBStatement.java index 09c76dad4..00b006026 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/DBStatement.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/DBStatement.java @@ -21,7 +21,7 @@ * @author Microsoft * */ -public class DBStatement extends AbstractParentWrapper { +public class DBStatement extends AbstractParentWrapper implements AutoCloseable{ // TODO: support PreparedStatement and CallableStatement // TODO: add stmt level holdability @@ -74,6 +74,20 @@ public DBResultSet executeQuery(String sql) throws SQLException { return dbresultSet; } + /** + * execute 'Select * from ' the table + * + * @param table + * @return DBResultSet + * @throws SQLException + */ + public DBResultSet selectAll(DBTable table) throws SQLException { + String sql = "SELECT * FROM " + table.getEscapedTableName(); + ResultSet rs = statement.executeQuery(sql); + dbresultSet = new DBResultSet(this, rs, table); + return dbresultSet; + } + /** * * @param sql @@ -106,7 +120,7 @@ public void close() throws SQLException { if ((null != dbresultSet) && null != ((ResultSet) dbresultSet.product())) { ((ResultSet) dbresultSet.product()).close(); } - statement.close(); + //statement.close(); } /** diff --git a/src/test/java/com/microsoft/sqlserver/testframework/DBTable.java b/src/test/java/com/microsoft/sqlserver/testframework/DBTable.java index f1e28bb44..39c18785d 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/DBTable.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/DBTable.java @@ -11,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.fail; import java.sql.JDBCType; +import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; @@ -32,9 +33,10 @@ public class DBTable extends AbstractSQLGenerator { public static final Logger log = Logger.getLogger("DBTable"); String tableName; String escapedTableName; + String tableDefinition; List columns; int totalColumns; - static int totalRows = 3; // default row count set to 3 + int totalRows = 3; // default row count set to 3 DBSchema schema; /** @@ -84,7 +86,7 @@ public DBTable(boolean autoGenerateSchema, addColumns(); } else { - this.columns = new ArrayList(); + this.columns = new ArrayList<>(); } this.totalColumns = columns.size(); } @@ -107,7 +109,7 @@ private DBTable(DBTable sourceTable) { */ private void addColumns() { totalColumns = schema.getNumberOfSqlTypes(); - columns = new ArrayList(totalColumns); + columns = new ArrayList<>(totalColumns); for (int i = 0; i < totalColumns; i++) { SqlType sqlType = schema.getSqlType(i); @@ -121,7 +123,7 @@ private void addColumns() { */ private void addColumns(boolean unicode) { totalColumns = schema.getNumberOfSqlTypes(); - columns = new ArrayList(totalColumns); + columns = new ArrayList<>(totalColumns); for (int i = 0; i < totalColumns; i++) { SqlType sqlType = schema.getSqlType(i); @@ -142,7 +144,7 @@ private void addColumns(boolean unicode) { public String getTableName() { return tableName; } - + public List getColumns() { return this.columns; } @@ -156,11 +158,15 @@ public String getEscapedTableName() { return escapedTableName; } + public String getDefinitionOfColumns() { + return tableDefinition; + } + /** * * @return total rows in the table */ - public static int getTotalRows() { + public int getTotalRows() { return totalRows; } @@ -196,26 +202,40 @@ String createTableSql() { sb.add(CREATE_TABLE); sb.add(escapedTableName); sb.add(OPEN_BRACKET); + + StringJoiner sbDefinition = new StringJoiner(SPACE_CHAR); for (int i = 0; i < totalColumns; i++) { DBColumn column = getColumn(i); - sb.add(escapeIdentifier(column.getColumnName())); - sb.add(column.getSqlType().getName()); + sbDefinition.add(escapeIdentifier(column.getColumnName())); + sbDefinition.add(column.getSqlType().getName()); // add precision and scale if (VariableLengthType.Precision == column.getSqlType().getVariableLengthType()) { - sb.add(OPEN_BRACKET); - sb.add("" + column.getSqlType().getPrecision()); - sb.add(CLOSE_BRACKET); + sbDefinition.add(OPEN_BRACKET); + sbDefinition.add("" + column.getSqlType().getPrecision()); + sbDefinition.add(CLOSE_BRACKET); } else if (VariableLengthType.Scale == column.getSqlType().getVariableLengthType()) { - sb.add(OPEN_BRACKET); - sb.add("" + column.getSqlType().getPrecision()); - sb.add(COMMA); - sb.add("" + column.getSqlType().getScale()); - sb.add(CLOSE_BRACKET); + sbDefinition.add(OPEN_BRACKET); + sbDefinition.add("" + column.getSqlType().getPrecision()); + sbDefinition.add(COMMA); + sbDefinition.add("" + column.getSqlType().getScale()); + sbDefinition.add(CLOSE_BRACKET); } - - sb.add(COMMA); + else if (VariableLengthType.ScaleOnly == column.getSqlType().getVariableLengthType()) { + sbDefinition.add(OPEN_BRACKET); + sbDefinition.add("" + column.getSqlType().getScale()); + sbDefinition.add(CLOSE_BRACKET); + } + sbDefinition.add(COMMA); } + tableDefinition = sbDefinition.toString(); + + // Remove the last comma + int indexOfLastComma = tableDefinition.lastIndexOf(","); + tableDefinition = tableDefinition.substring(0, indexOfLastComma); + + sb.add(tableDefinition); + sb.add(CLOSE_BRACKET); return sb.toString(); } @@ -238,6 +258,56 @@ boolean populateTable(DBStatement dbstatement) { return false; } + /** + * using prepared statement to populate table with values + * + * @param dbstatement + * @return + */ + boolean populateTableWithPreparedStatement(DBPreparedStatement dbPStmt) { + try { + populateValues(); + + // create the insertion query + StringJoiner sb = new StringJoiner(SPACE_CHAR); + sb.add("INSERT"); + sb.add("INTO"); + sb.add(escapedTableName); + sb.add("VALUES"); + sb.add(OPEN_BRACKET); + for (int colNum = 0; colNum < totalColumns; colNum++) { + sb.add(QUESTION_MARK); + + if (colNum < totalColumns - 1) { + sb.add(COMMA); + } + } + sb.add(CLOSE_BRACKET); + String sql = sb.toString(); + + dbPStmt.prepareStatement(sql); + + // insert data + for (int i = 0; i < totalRows; i++) { + for (int colNum = 0; colNum < totalColumns; colNum++) { + if (passDataAsHex(colNum)) { + ((PreparedStatement) dbPStmt.product()).setBytes(colNum + 1, ((byte[]) (getColumn(colNum).getRowValue(i)))); + } + else { + dbPStmt.setObject(colNum + 1, String.valueOf(getColumn(colNum).getRowValue(i))); + } + } + dbPStmt.execute(); + } + + return true; + } + catch (SQLException ex) { + fail(ex.getMessage()); + } + return false; + } + private void populateValues() { // generate values for all columns for (int i = 0; i < totalColumns; i++) { diff --git a/src/test/java/com/microsoft/sqlserver/testframework/Utils.java b/src/test/java/com/microsoft/sqlserver/testframework/Utils.java index eb21d2864..229f7f4c6 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/Utils.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/Utils.java @@ -8,12 +8,15 @@ package com.microsoft.sqlserver.testframework; +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertTrue; + import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.CharArrayReader; -import java.io.IOException; -import java.io.InputStream; +import java.net.URI; +import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; @@ -128,8 +131,7 @@ public static String getConfiguredProperty(String key, public static SqlType find(Class javatype) { if (null != types) { types(); - for (int i = 0; i < types.size(); i++) { - SqlType type = types.get(i); + for (SqlType type : types) { if (type.getType() == javatype) return type; } @@ -146,8 +148,7 @@ public static SqlType find(String name) { if (null == types) types(); if (null != types) { - for (int i = 0; i < types.size(); i++) { - SqlType type = types.get(i); + for (SqlType type : types) { if (type.getName().equalsIgnoreCase(name)) return type; } @@ -161,7 +162,7 @@ public static SqlType find(String name) { */ public static ArrayList types() { if (null == types) { - types = new ArrayList(); + types = new ArrayList<>(); types.add(new SqlInt()); types.add(new SqlSmallInt()); @@ -249,5 +250,69 @@ public DBNCharacterStream(String value) { super(value); } } + + /** + * + * @return location of resource file + */ + public static String getCurrentClassPath() { + try { + String className = new Object() { + }.getClass().getEnclosingClass().getName(); + String location = Class.forName(className).getProtectionDomain().getCodeSource().getLocation().getPath() + "/"; + URI uri = new URI(location.toString()); + return uri.getPath(); + } + catch (Exception e) { + fail("Failed to get CSV file path. " + e.getMessage()); + } + return null; + } + + /** + * mimic "DROP TABLE IF EXISTS ..." for older versions of SQL Server + */ + public static void dropTableIfExists(String tableName, java.sql.Statement stmt) throws SQLException { + dropObjectIfExists(tableName, "IsTable", stmt); + } + + /** + * mimic "DROP PROCEDURE IF EXISTS ..." for older versions of SQL Server + */ + public static void dropProcedureIfExists(String procName, java.sql.Statement stmt) throws SQLException { + dropObjectIfExists(procName, "IsProcedure", stmt); + } + /** + * actually perform the "DROP TABLE / PROCEDURE" + */ + private static void dropObjectIfExists(String objectName, String objectProperty, java.sql.Statement stmt) throws SQLException { + StringBuilder sb = new StringBuilder(); + if (!objectName.startsWith("[")) { sb.append("["); } + sb.append(objectName); + if (!objectName.endsWith("]")) { sb.append("]"); } + String bracketedObjectName = sb.toString(); + String sql = String.format( + "IF EXISTS " + + "( " + + "SELECT * from sys.objects " + + "WHERE object_id = OBJECT_ID(N'%s') AND OBJECTPROPERTY(object_id, N'%s') = 1 " + + ") " + + "DROP %s %s ", + bracketedObjectName, + objectProperty, + "IsProcedure".equals(objectProperty) ? "PROCEDURE" : "TABLE", + bracketedObjectName); + stmt.executeUpdate(sql); + } + + public static boolean parseByte(byte[] expectedData, + byte[] retrieved) { + assertTrue(Arrays.equals(expectedData, Arrays.copyOf(retrieved, expectedData.length)), " unexpected BINARY value, expected"); + for (int i = expectedData.length; i < retrieved.length; i++) { + assertTrue(0 == retrieved[i], "unexpected data BINARY"); + } + return true; + } + } \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlBigInt.java b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlBigInt.java index e8fef48a8..84126f5da 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlBigInt.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlBigInt.java @@ -22,6 +22,6 @@ public SqlBigInt() { public Object createdata() { // TODO: include max value - return new Long(ThreadLocalRandom.current().nextLong(Long.MIN_VALUE, Long.MAX_VALUE)); + return ThreadLocalRandom.current().nextLong(Long.MIN_VALUE, Long.MAX_VALUE); } } \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlDateTime2.java b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlDateTime2.java index 2da5df99e..5050d68a2 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlDateTime2.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlDateTime2.java @@ -14,9 +14,9 @@ import java.sql.Timestamp; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; +import java.time.format.ResolverStyle; import java.time.temporal.ChronoField; import java.util.concurrent.ThreadLocalRandom; @@ -42,15 +42,16 @@ public SqlDateTime2() { generatePrecision(); formatter = new DateTimeFormatterBuilder().appendPattern(basePattern).appendFraction(ChronoField.NANO_OF_SECOND, 0, this.precision, true) .toFormatter(); - + formatter = formatter.withResolverStyle(ResolverStyle.STRICT); } public Object createdata() { Timestamp temp = new Timestamp(ThreadLocalRandom.current().nextLong(((Timestamp) minvalue).getTime(), ((Timestamp) maxvalue).getTime())); temp.setNanos(0); String timeNano = temp.toString().substring(0, temp.toString().length() - 1) + RandomStringUtils.randomNumeric(this.precision); + return timeNano; // can pass string rather than converting to LocalDateTime, but leaving // it unchanged for now to handle prepared statements - return LocalDateTime.parse(timeNano, formatter); +// return LocalDateTime.parse(timeNano, formatter); } } \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlFloat.java b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlFloat.java index 86ee22d53..a2216d0e9 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlFloat.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlFloat.java @@ -33,12 +33,11 @@ public SqlFloat() { } public Object createdata() { - // TODO: include max value - if (precision > 24) { + // for float in SQL Server, any precision <=24 is considered as real so the value must be within SqlTypeValue.REAL.minValue/maxValue + if (precision > 24) return Double.longBitsToDouble(ThreadLocalRandom.current().nextLong(((Double) minvalue).longValue(), ((Double) maxvalue).longValue())); - } else { - return new Float(ThreadLocalRandom.current().nextDouble(new Float(-3.4E38), new Float(+3.4E38))); + return ThreadLocalRandom.current().nextDouble((Float) SqlTypeValue.REAL.minValue, (Float) SqlTypeValue.REAL.maxValue); } } } \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlInt.java b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlInt.java index e988fc49e..2622f0745 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlInt.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlInt.java @@ -21,6 +21,6 @@ public SqlInt() { public Object createdata() { // TODO: include max value - return new Integer(ThreadLocalRandom.current().nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE)); + return ThreadLocalRandom.current().nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE); } } \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlReal.java b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlReal.java index cbdd5db90..5edaf59ac 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlReal.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlReal.java @@ -9,6 +9,7 @@ package com.microsoft.sqlserver.testframework.sqlType; import java.sql.JDBCType; +import java.util.concurrent.ThreadLocalRandom; public class SqlReal extends SqlFloat { @@ -16,4 +17,9 @@ public SqlReal() { super("real", JDBCType.REAL, 24, SqlTypeValue.REAL.minValue, SqlTypeValue.REAL.maxValue, SqlTypeValue.REAL.nullValue, VariableLengthType.Fixed, Float.class); } + + @Override + public Object createdata() { + return (float) ThreadLocalRandom.current().nextDouble((Float) minvalue, (Float) maxvalue); + } } \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlSmallDateTime.java b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlSmallDateTime.java index 392fbc2ca..c9ab21c3d 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlSmallDateTime.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlSmallDateTime.java @@ -35,6 +35,7 @@ public Object createdata() { ThreadLocalRandom.current().nextLong(((Timestamp) minvalue).getTime(), ((Timestamp) maxvalue).getTime())); // remove the random nanosecond value if any smallDateTime.setNanos(0); - return smallDateTime; + return smallDateTime.toString().substring(0,19);// ignore the nano second portion +// return smallDateTime; } } \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlSmallInt.java b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlSmallInt.java index d821c9d7c..7d2cebcae 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlSmallInt.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlSmallInt.java @@ -21,6 +21,6 @@ public SqlSmallInt() { public Object createdata() { // TODO: include max value - return new Short((short) ThreadLocalRandom.current().nextInt(Short.MIN_VALUE, Short.MAX_VALUE)); + return (short) ThreadLocalRandom.current().nextInt(Short.MIN_VALUE, Short.MAX_VALUE); } } \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlTime.java b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlTime.java index 8cde8932e..52cc9bcb4 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlTime.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlTime.java @@ -14,9 +14,9 @@ import java.sql.Time; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; +import java.time.format.ResolverStyle; import java.time.temporal.ChronoField; import java.util.concurrent.ThreadLocalRandom; @@ -38,19 +38,26 @@ public SqlTime() { catch (ParseException ex) { fail(ex.getMessage()); } - this.precision = 7; - this.variableLengthType = VariableLengthType.Precision; - generatePrecision(); - formatter = new DateTimeFormatterBuilder().appendPattern(basePattern).appendFraction(ChronoField.NANO_OF_SECOND, 0, this.precision, true) + this.scale = 7; + this.variableLengthType = VariableLengthType.ScaleOnly; + generateScale(); + + formatter = new DateTimeFormatterBuilder().appendPattern(basePattern).appendFraction(ChronoField.NANO_OF_SECOND, 0, this.scale, true) .toFormatter(); + formatter = formatter.withResolverStyle(ResolverStyle.STRICT); } public Object createdata() { Time temp = new Time(ThreadLocalRandom.current().nextLong(((Time) minvalue).getTime(), ((Time) maxvalue).getTime())); - String timeNano = temp.toString() + "." + RandomStringUtils.randomNumeric(this.precision); + String timeNano = temp.toString() + "." + RandomStringUtils.randomNumeric(this.scale); + return timeNano; + // can pass String rather than converting to loacTime, but leaving it // unchanged for now to handle prepared statements - return LocalTime.parse(timeNano, formatter); + /* + * converting string '20:53:44.9' to LocalTime results in 20:53:44.900, this extra scale causes failure + */ +// return LocalTime.parse(timeNano, formatter); } } \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlTinyInt.java b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlTinyInt.java index d8c7750dd..ca5ca58fb 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlTinyInt.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlTinyInt.java @@ -20,6 +20,6 @@ public SqlTinyInt() { public Object createdata() { // TODO: include max value - return new Short((short) ThreadLocalRandom.current().nextInt((short) minvalue, ((short) maxvalue))); + return (short) ThreadLocalRandom.current().nextInt((short) minvalue, ((short) maxvalue)); } } \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlType.java b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlType.java index e70f5fd0b..36480f7ec 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlType.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlType.java @@ -9,9 +9,6 @@ package com.microsoft.sqlserver.testframework.sqlType; import java.sql.JDBCType; -import java.sql.SQLTimeoutException; -import java.sql.SQLType; -import java.util.ArrayList; import java.util.BitSet; import java.util.concurrent.ThreadLocalRandom; @@ -19,7 +16,6 @@ import com.microsoft.sqlserver.testframework.DBCoercions; import com.microsoft.sqlserver.testframework.DBConnection; import com.microsoft.sqlserver.testframework.DBItems; -import com.microsoft.sqlserver.testframework.Utils; public abstract class SqlType extends DBItems { // TODO: add seed to generate random data -> will help to reproduce the @@ -225,7 +221,16 @@ void generatePrecision() { int maxPrecision = this.precision; this.precision = ThreadLocalRandom.current().nextInt(minPrecision, maxPrecision + 1); } - + + /** + * generates random precision for SQL types with scale + */ + void generateScale() { + int minScale = 1; + int maxScale = this.scale; + this.scale = ThreadLocalRandom.current().nextInt(minScale, maxScale + 1); + } + /** * @return */ diff --git a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlTypeValue.java b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlTypeValue.java index b1a7d9427..ae2b3345b 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlTypeValue.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlTypeValue.java @@ -17,16 +17,16 @@ */ enum SqlTypeValue { // minValue // maxValue // nullValue - BIGINT (new Long(Long.MIN_VALUE), new Long(Long.MAX_VALUE), new Long(0)), - INTEGER (new Integer(Integer.MIN_VALUE), new Integer(Integer.MAX_VALUE), new Integer(0)), - SMALLINT (new Short(Short.MIN_VALUE), new Short(Short.MAX_VALUE), new Short((short) 0)), - TINYINT (new Short((short) 0), new Short((short) 255), new Short((short) 0)), + BIGINT (Long.MIN_VALUE, Long.MAX_VALUE, 0L), + INTEGER (Integer.MIN_VALUE, Integer.MAX_VALUE, 0), + SMALLINT (Short.MIN_VALUE, Short.MAX_VALUE, (short) 0), + TINYINT ((short) 0, (short) 255, (short) 0), BIT (0, 1, null), DECIMAL (new BigDecimal("-1.0E38").add(new BigDecimal("1")), new BigDecimal("1.0E38").subtract(new BigDecimal("1")), null), MONEY (new BigDecimal("-922337203685477.5808"), new BigDecimal("+922337203685477.5807"), null), SMALLMONEY (new BigDecimal("-214748.3648"), new BigDecimal("214748.3647"), null), - FLOAT (new Double(-1.79E308), new Double(+1.79E308), new Double(0)), - REAL (new Float(-3.4E38), new Float(+3.4E38), new Float(0)), + FLOAT (-1.79E308, +1.79E308, 0d), + REAL ((float) -3.4E38, (float) +3.4E38, 0f), CHAR (null, null, null),// CHAR used by char, nchar, varchar, nvarchar BINARY (null, null, null), DATETIME ("17530101T00:00:00.000", "99991231T23:59:59.997", null), @@ -34,7 +34,7 @@ enum SqlTypeValue { TIME ("00:00:00.0000000", "23:59:59.9999999", null), SMALLDATETIME ("19000101T00:00:00", "20790606T23:59:59", null), DATETIME2 ("00010101T00:00:00.0000000", "99991231T23:59:59.9999999", null), - DATETIMEOFFSET ("0001-01-01 00:00:00", "9999-12-31 23:59:59", null), + DATETIMEOFFSET ("0001-01-01 00:00:00", "9999-12-31 23:59:59", null), ; Object minValue; diff --git a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/VariableLengthType.java b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/VariableLengthType.java index 26d3de103..fb12fa1ae 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/VariableLengthType.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/VariableLengthType.java @@ -15,5 +15,6 @@ public enum VariableLengthType { Fixed, // primitive types with fixed Length Precision, // variable length type that just has precision char/varchar Scale, // variable length type with scale and precision + ScaleOnly, // variable length type with just scale like Time Variable } diff --git a/src/test/java/com/microsoft/sqlserver/testframework/util/ComparisonUtil.java b/src/test/java/com/microsoft/sqlserver/testframework/util/ComparisonUtil.java new file mode 100644 index 000000000..c942f8c9a --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/testframework/util/ComparisonUtil.java @@ -0,0 +1,163 @@ +package com.microsoft.sqlserver.testframework.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.math.BigDecimal; +import java.sql.Date; +import java.sql.JDBCType; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; + +import com.microsoft.sqlserver.jdbc.SQLServerResultSetMetaData; +import com.microsoft.sqlserver.testframework.DBConnection; +import com.microsoft.sqlserver.testframework.DBResultSet; +import com.microsoft.sqlserver.testframework.DBTable; +import com.microsoft.sqlserver.testframework.Utils; + +public class ComparisonUtil { + + /** + * test if source table and destination table are the same + * + * @param con + * @param srcTable + * @param destTable + * @throws SQLException + */ + public static void compareSrcTableAndDestTableIgnoreRowOrder(DBConnection con, + DBTable srcTable, + DBTable destTable) throws SQLException { + DBResultSet srcResultSetCount = con.createStatement().executeQuery("SELECT COUNT(*) FROM " + srcTable.getEscapedTableName() + ";"); + DBResultSet dstResultSetCount = con.createStatement().executeQuery("SELECT COUNT(*) FROM " + destTable.getEscapedTableName() + ";"); + srcResultSetCount.next(); + dstResultSetCount.next(); + int srcRows = srcResultSetCount.getInt(1); + int destRows = dstResultSetCount.getInt(1); + + if (srcRows != destRows) { + fail("Souce table and Destination table have different number of rows."); + } + + if (srcTable.getColumns().size() != destTable.getColumns().size()) { + fail("Souce table and Destination table have different number of columns."); + } + + DBResultSet srcResultSet = con.createStatement().executeQuery("SELECT * FROM " + srcTable.getEscapedTableName() + " ORDER BY [" + + srcTable.getColumnName(1) + "], [" + srcTable.getColumnName(2) + "],[" + srcTable.getColumnName(3) + "];"); + DBResultSet dstResultSet = con.createStatement().executeQuery("SELECT * FROM " + destTable.getEscapedTableName() + " ORDER BY [" + + destTable.getColumnName(1) + "], [" + destTable.getColumnName(2) + "],[" + destTable.getColumnName(3) + "];"); + + while (srcResultSet.next() && dstResultSet.next()) { + for (int i = 0; i < destTable.getColumns().size(); i++) { + SQLServerResultSetMetaData srcMeta = (SQLServerResultSetMetaData) ((ResultSet) srcResultSet.product()).getMetaData(); + SQLServerResultSetMetaData destMeta = (SQLServerResultSetMetaData) ((ResultSet) dstResultSet.product()).getMetaData(); + + int srcJDBCTypeInt = srcMeta.getColumnType(i + 1); + int destJDBCTypeInt = destMeta.getColumnType(i + 1); + + // verify column types + if (srcJDBCTypeInt != destJDBCTypeInt) { + fail("Souce table and Destination table have different number of columns."); + } + + Object expectedValue = srcResultSet.getObject(i + 1); + Object actualValue = dstResultSet.getObject(i + 1); + + compareExpectedAndActual(destJDBCTypeInt, expectedValue, actualValue); + } + } + } + + /** + * validate if both expected and actual value are same + * + * @param dataType + * @param expectedValue + * @param actualValue + */ + public static void compareExpectedAndActual(int dataType, + Object expectedValue, + Object actualValue) { + // Bulkcopy doesn't guarantee order of insertion - if we need to test several rows either use primary key or + // validate result based on sql JOIN + + if ((null == expectedValue) || (null == actualValue)) { + // if one value is null other should be null too + assertEquals(expectedValue, actualValue, "Expected null in source and destination"); + } + else + switch (dataType) { + case java.sql.Types.BIGINT: + assertTrue((((Long) expectedValue).longValue() == ((Long) actualValue).longValue()), "Unexpected bigint value"); + break; + + case java.sql.Types.INTEGER: + assertTrue((((Integer) expectedValue).intValue() == ((Integer) actualValue).intValue()), "Unexpected int value"); + break; + + case java.sql.Types.SMALLINT: + case java.sql.Types.TINYINT: + assertTrue((((Short) expectedValue).shortValue() == ((Short) actualValue).shortValue()), "Unexpected smallint/tinyint value"); + break; + + case java.sql.Types.BIT: + assertTrue((((Boolean) expectedValue).booleanValue() == ((Boolean) actualValue).booleanValue()), "Unexpected bit value"); + break; + + case java.sql.Types.DECIMAL: + case java.sql.Types.NUMERIC: + assertTrue(0 == (((BigDecimal) expectedValue).compareTo((BigDecimal) actualValue)), + "Unexpected decimal/numeric/money/smallmoney value"); + break; + + case java.sql.Types.DOUBLE: + assertTrue((((Double) expectedValue).doubleValue() == ((Double) actualValue).doubleValue()), "Unexpected float value"); + break; + + case java.sql.Types.REAL: + assertTrue((((Float) expectedValue).floatValue() == ((Float) actualValue).floatValue()), "Unexpected real value"); + break; + + case java.sql.Types.VARCHAR: + case java.sql.Types.NVARCHAR: + assertTrue(((((String) expectedValue).trim()).equals(((String) actualValue).trim())), "Unexpected varchar/nvarchar value "); + break; + + case java.sql.Types.CHAR: + case java.sql.Types.NCHAR: + assertTrue(((((String) expectedValue).trim()).equals(((String) actualValue).trim())), "Unexpected char/nchar value "); + break; + + case java.sql.Types.BINARY: + case java.sql.Types.VARBINARY: + assertTrue(Utils.parseByte((byte[]) expectedValue, (byte[]) actualValue), "Unexpected bianry/varbinary value "); + break; + + case java.sql.Types.TIMESTAMP: + assertTrue((((Timestamp) expectedValue).getTime() == (((Timestamp) actualValue).getTime())), + "Unexpected datetime/smalldatetime/datetime2 value"); + break; + + case java.sql.Types.DATE: + assertTrue((((Date) expectedValue).getDate() == (((Date) actualValue).getDate())), "Unexpected datetime value"); + break; + + case java.sql.Types.TIME: + assertTrue(((Time) expectedValue).getTime() == ((Time) actualValue).getTime(), "Unexpected time value "); + break; + + case microsoft.sql.Types.DATETIMEOFFSET: + assertTrue(0 == ((microsoft.sql.DateTimeOffset) expectedValue).compareTo((microsoft.sql.DateTimeOffset) actualValue), + "Unexpected time value "); + break; + + default: + fail("Unhandled JDBCType " + JDBCType.valueOf(dataType)); + break; + } + } +} diff --git a/src/test/java/com/microsoft/sqlserver/testframework/util/RandomData.java b/src/test/java/com/microsoft/sqlserver/testframework/util/RandomData.java new file mode 100644 index 000000000..21e12991b --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/testframework/util/RandomData.java @@ -0,0 +1,797 @@ +package com.microsoft.sqlserver.testframework.util; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Random; + +import microsoft.sql.DateTimeOffset; + +/** + * Utility class for generating random data for testing + */ +public class RandomData { + + private static Random r = new Random(); + + public static boolean returnNull = (0 == r.nextInt(5)); // 20% chance of return null + public static boolean returnFullLength = (0 == r.nextInt(2)); // 50% chance of return full length for char/nchar and binary types + public static boolean returnMinMax = (0 == r.nextInt(5)); // 20% chance of return Min/Max value + public static boolean returnZero = (0 == r.nextInt(10)); // 10% chance of return zero + + private static String specicalCharSet = "ÀÂÃÄËßîðÐ"; + private static String normalCharSet = "1234567890-=!@#$%^&*()_+qwertyuiop[]\\asdfghjkl;'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:\"ZXCVBNM<>?"; + + private static String unicodeCharSet = "♠♣♥♦林花謝了春紅太匆匆無奈朝我附件为放假哇额外放我放问역사적으로본래한민족의영역은만주와연해주의일부를포함하였으나会和太空特工我來寒雨晚來風胭脂淚留人醉幾時重自是人生長恨水長東ྱོགས་སུ་འཁོར་བའི་ས་ཟླུུམ་ཞིག་ལ་ངོས་འཛིན་དགོས་ཏེ།ངག་ཕྱོαβγδεζηθικλμνξοπρστυφχψ太陽系の年齢もまた隕石の年代測定に依拠するので"; + + private static String numberCharSet = "1234567890"; + private static String numberCharSet2 = "123456789"; + + /** + * Utility method for generating a random boolean. + * + * @param nullable + * @return + */ + public static Boolean generateBoolean(boolean nullable) { + if (nullable) { + if (returnNull) { + return null; + } + } + + return r.nextBoolean(); + } + + /** + * Utility method for generating a random int. + * + * @param nullable + * @return + */ + public static Integer generateInt(boolean nullable) { + if (nullable) { + if (returnNull) { + return null; + } + } + + if (returnZero) { + return 0; + } + + if (returnMinMax) { + if (r.nextBoolean()) { + return 2147483647; + } + else { + return -2147483648; + } + } + + // can be either negative or positive + return r.nextInt(); + } + + /** + * Utility method for generating a random long. + * + * @param nullable + * @return + */ + public static Long generateLong(boolean nullable) { + if (nullable) { + if (returnNull) { + return null; + } + } + + if (returnZero) { + return 0L; + } + + if (returnMinMax) { + if (r.nextBoolean()) { + return 9223372036854775807L; + } + else { + return -9223372036854775808L; + } + } + + // can be either negative or positive + return r.nextLong(); + } + + /** + * Utility method for generating a random tinyint. + * + * @param nullable + * @return + */ + public static Short generateTinyint(boolean nullable) { + Integer value = pickInt(nullable, 255, 0); + + if (null != value) { + return value.shortValue(); + } + else { + return null; + } + } + + /** + * Utility method for generating a random short. + * + * @param nullable + * @return + */ + public static Short generateSmallint(boolean nullable) { + Integer value = pickInt(nullable, 32767, -32768); + + if (null != value) { + return value.shortValue(); + } + else { + return null; + } + } + + /** + * Utility method for generating a random BigDecimal. + * + * @param precision + * @param scale + * @param nullable + * @return + */ + public static BigDecimal generateDecimalNumeric(int precision, + int scale, + boolean nullable) { + + if (nullable) { + if (returnNull) { + return null; + } + } + + if (returnZero) { + return BigDecimal.ZERO.setScale(scale); + + } + + if (returnMinMax) { + BigInteger n; + if (r.nextBoolean()) { + n = BigInteger.TEN.pow(precision); + if (scale > 0) + return new BigDecimal(n, scale).subtract(new BigDecimal("" + Math.pow(10, -scale)).setScale(scale, BigDecimal.ROUND_HALF_UP)) + .negate(); + else + return new BigDecimal(n, scale).subtract(new BigDecimal("1")).negate(); + } + else { + n = BigInteger.TEN.pow(precision); + if (scale > 0) + return new BigDecimal(n, scale).subtract(new BigDecimal("" + Math.pow(10, -scale)).setScale(scale, BigDecimal.ROUND_HALF_UP)) + .negate(); + else + return new BigDecimal(n, scale).subtract(new BigDecimal("1")).negate(); + + } + + } + BigInteger n = BigInteger.TEN.pow(precision); + if (r.nextBoolean()) { + return new BigDecimal(newRandomBigInteger(n, r, precision), scale); + } + return (new BigDecimal(newRandomBigInteger(n, r, precision), scale).negate()); + + } + + /** + * Utility method for generating a random float. + * + * @param nullable + * @return + */ + public static Float generateReal(boolean nullable) { + Double doubleValue = generateFloat(24, nullable); + + if (null != doubleValue) { + return doubleValue.floatValue(); + } + else { + return null; + } + } + + /** + * Utility method for generating a random double. + * + * @param n + * integer + * @param nullable + * @return + */ + public static Double generateFloat(Integer n, + boolean nullable) { + if (nullable) { + if (returnNull) { + return null; + } + } + + if (returnZero) { + return new Double(0); + } + + // only 2 options: 24 or 53 + // The default value of n is 53. If 1<=n<=24, n is treated as 24. If 25<=n<=53, n is treated as 53. + // https://msdn.microsoft.com/en-us/library/ms173773.aspx + if (null == n) { + n = 53; + } + else if (25 <= n && 53 >= n) { + n = 53; + } + else { + n = 24; + } + + if (returnMinMax) { + if (53 == n) { + if (r.nextBoolean()) { + if (r.nextBoolean()) { + return Double.valueOf("1.79E+308"); + } + else { + return Double.valueOf("2.23E-308"); + } + } + else { + if (r.nextBoolean()) { + return Double.valueOf("-2.23E-308"); + } + else { + return Double.valueOf("-1.79E+308"); + } + } + } + else { + if (r.nextBoolean()) { + if (r.nextBoolean()) { + return Double.valueOf("3.40E+38"); + } + else { + return Double.valueOf("1.18E-38"); + } + } + else { + if (r.nextBoolean()) { + return Double.valueOf("-1.18E-38"); + } + else { + return Double.valueOf("-3.40E+38"); + } + } + } + } + + String intPart = "" + r.nextInt(10); + + // generate n bits of binary data and convert to long, then use the long as decimal part + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < n; i++) { + sb.append(r.nextInt(2)); + } + long longValue = Long.parseLong(sb.toString(), 2); + String stringValue = intPart + "." + longValue; + + return Double.valueOf(stringValue); + } + + /** + * Utility method for generating a random Money. + * + * @param nullable + * @return + */ + public static BigDecimal generateMoney(boolean nullable) { + String charSet = numberCharSet; + BigDecimal max = new BigDecimal("922337203685477.5807"); + BigDecimal min = new BigDecimal("-922337203685477.5808"); + float multiplier = 10000; + return generateMoneyOrSmallMoney(nullable, max, min, multiplier, charSet); + } + + /** + * Utility method for generating a random SmallMoney. + * + * @param nullable + * @return + */ + public static BigDecimal generateSmallMoney(boolean nullable) { + String charSet = numberCharSet; + BigDecimal max = new BigDecimal("214748.3647"); + BigDecimal min = new BigDecimal("-214748.3648"); + float multiplier = (float) (1.0 / 10000.0); + return generateMoneyOrSmallMoney(nullable, max, min, multiplier, charSet); + } + + /** + * Utility method for generating a random char or Nchar. + * + * @param columnLength + * @param nullable + * @param encrypted + * @return + */ + public static String generateCharTypes(String columnLength, + boolean nullable, + boolean encrypted) { + String charSet = normalCharSet; + + return buildCharOrNChar(columnLength, nullable, encrypted, charSet, 8001); + } + + public static String generateNCharTypes(String columnLength, + boolean nullable, + boolean encrypted) { + String charSet = specicalCharSet + normalCharSet + unicodeCharSet; + + return buildCharOrNChar(columnLength, nullable, encrypted, charSet, 4001); + } + + /** + * Utility method for generating a random binary. + * + * @param columnLength + * @param nullable + * @param encrypted + * @return + */ + public static byte[] generateBinaryTypes(String columnLength, + boolean nullable, + boolean encrypted) { + int maxBound = 8001; + + if (nullable) { + if (returnNull) { + return null; + } + } + + // if column is encrypted, string value cannot be "", not supported. + int minimumLength = 0; + if (encrypted) { + minimumLength = 1; + } + + int length; + if (columnLength.toLowerCase().equals("max")) { + // 50% chance of return value longer than 8000/4000 + if (r.nextBoolean()) { + length = r.nextInt(100000) + maxBound; + byte[] bytes = new byte[length]; + r.nextBytes(bytes); + return bytes; + } + else { + length = r.nextInt(maxBound - minimumLength) + minimumLength; + byte[] bytes = new byte[length]; + r.nextBytes(bytes); + return bytes; + } + } + else { + int columnLengthInt = Integer.parseInt(columnLength); + if (returnFullLength) { + length = columnLengthInt; + byte[] bytes = new byte[length]; + r.nextBytes(bytes); + return bytes; + } + else { + length = r.nextInt(columnLengthInt - minimumLength) + minimumLength; + byte[] bytes = new byte[length]; + r.nextBytes(bytes); + return bytes; + } + } + } + + /** + * Utility method for generating a random date. + * + * @param nullable + * @return + */ + public static Date generateDate(boolean nullable) { + if (nullable) { + if (returnNull) { + return null; + } + } + + long max = Timestamp.valueOf("9999-12-31 00:00:00.000").getTime(); + long min = Timestamp.valueOf("0001-01-01 00:00:00.000").getTime(); + + if (returnMinMax) { + if (r.nextBoolean()) { + return new Date(max); + } + else { + return new Date(min); + } + } + + while (true) { + long longValue = r.nextLong(); + + if (longValue >= min && longValue <= max) { + return new Date(longValue); + } + } + } + + /** + * Utility method for generating a random timestamp. + * + * @param nullable + * @return + */ + public static Timestamp generateDatetime(boolean nullable) { + long max = Timestamp.valueOf("9999-12-31 23:59:59.997").getTime(); + long min = Timestamp.valueOf("1753-01-01 00:00:00.000").getTime(); + + return generateTimestamp(nullable, max, min); + } + + /** + * Utility method for generating a random datetimeoffset. + * + * @param nullable + * @return + */ + public static DateTimeOffset generateDatetimeoffset(Integer precision, + boolean nullable) { + if (null == precision) { + precision = 7; + } + + DateTimeOffset maxDTS = calculateDateTimeOffsetMinMax("max", precision, "9999-12-31 23:59:59"); + DateTimeOffset minDTS = calculateDateTimeOffsetMinMax("min", precision, "0001-01-01 00:00:00"); + + long max = maxDTS.getTimestamp().getTime(); + long min = minDTS.getTimestamp().getTime(); + + Timestamp ts = generateTimestamp(nullable, max, min); + + if (null == ts) { + return null; + } + + if (returnMinMax) { + if (r.nextBoolean()) { + return maxDTS; + } + else { + // return minDTS; + return calculateDateTimeOffsetMinMax("min", precision, "0001-01-01 00:00:00.0000000"); + } + } + + int precisionDigits = buildPrecision(precision, numberCharSet2); + ts.setNanos(precisionDigits); + + int randomTimeZoneInMinutes = r.nextInt(1681) - 840; + + return microsoft.sql.DateTimeOffset.valueOf(ts, randomTimeZoneInMinutes); + } + + /** + * Utility method for generating a random small datetime. + * + * @param nullable + * @return + */ + public static Timestamp generateSmalldatetime(boolean nullable) { + long max = Timestamp.valueOf("2079-06-06 23:59:00").getTime(); + long min = Timestamp.valueOf("1900-01-01 00:00:00").getTime(); + + return generateTimestamp(nullable, max, min); + } + + /** + * Utility method for generating a random datetime. + * + * @param precision + * @param nullable + * @return + */ + public static Timestamp generateDatetime2(Integer precision, + boolean nullable) { + if (null == precision) { + precision = 7; + } + + long max = Timestamp.valueOf("9999-12-31 23:59:59").getTime(); + long min = Timestamp.valueOf("0001-01-01 00:00:00").getTime(); + + Timestamp ts = generateTimestamp(nullable, max, min); + + if (null == ts) { + return ts; + } + + if (returnMinMax) { + if (ts.getTime() == max) { + int precisionDigits = buildPrecision(precision, "9"); + ts.setNanos(precisionDigits); + return ts; + } + else { + ts.setNanos(0); + return ts; + } + } + + int precisionDigits = buildPrecision(precision, numberCharSet2); // not to use 0 in the random data for now. E.g creates 9040330 and when set + // it is 904033. + ts.setNanos(precisionDigits); + return ts; + } + + /** + * Utility method for generating a random time. + * + * @param precision + * @param nullable + * @return + */ + public static Time generateTime(Integer precision, + boolean nullable) { + if (null == precision) { + precision = 7; + } + + long max = Timestamp.valueOf("9999-12-31 23:59:59").getTime(); + long min = Timestamp.valueOf("0001-01-01 00:00:00").getTime(); + + Timestamp ts = generateTimestamp(nullable, max, min); + + if (null == ts) { + return null; + } + + if (returnMinMax) { + if (ts.getTime() == max) { + int precisionDigits = buildPrecision(precision, "9"); + ts.setNanos(precisionDigits); + return new Time(ts.getTime()); + } + else { + ts.setNanos(0); + return new Time(ts.getTime()); + } + } + + int precisionDigits = buildPrecision(precision, numberCharSet); + ts.setNanos(precisionDigits); + return new Time(ts.getTime()); + } + + private static int buildPrecision(int precision, + String charSet) { + String stringValue = calculatePrecisionDigits(precision, charSet); + return Integer.parseInt(stringValue); + } + + private static String calculatePrecisionDigits(int precision, + String charSet) { + // setNanos(999999900) gives 00:00:00.9999999 + // so, this value has to be 9 digits + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < precision; i++) { + char c = pickRandomChar(charSet); + sb.append(c); + } + + for (int i = sb.length(); i < 9; i++) { + sb.append("0"); + } + + return sb.toString(); + } + + private static Timestamp generateTimestamp(boolean nullable, + long max, + long min) { + if (nullable) { + if (returnNull) { + return null; + } + } + + if (returnMinMax) { + if (r.nextBoolean()) { + return new Timestamp(max); + } + else { + return new Timestamp(min); + } + } + + while (true) { + long longValue = r.nextLong(); + + if (longValue >= min && longValue <= max) { + return new Timestamp(longValue); + } + } + } + + private static BigDecimal generateMoneyOrSmallMoney(boolean nullable, + BigDecimal max, + BigDecimal min, + float multiplier, + String charSet) { + if (nullable) { + if (returnNull) { + return null; + } + } + + if (returnZero) { + return BigDecimal.ZERO.setScale(4); + } + + if (returnMinMax) { + if (r.nextBoolean()) { + return max; + } + else { + return min; + } + } + + long intPart = (long) (r.nextInt() * multiplier); + + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < 4; i++) { + char c = pickRandomChar(charSet); + sb.append(c); + } + + return new BigDecimal(intPart + "." + sb.toString()); + } + + private static DateTimeOffset calculateDateTimeOffsetMinMax(String maxOrMin, + Integer precision, + String tsMinMax) { + int providedTimeZoneInMinutes; + if (maxOrMin.toLowerCase().equals("max")) { + providedTimeZoneInMinutes = 840; + } + else { + providedTimeZoneInMinutes = -840; + } + + Timestamp tsMax = Timestamp.valueOf(tsMinMax); + + Calendar cal = Calendar.getInstance(); + long offset = cal.get(Calendar.ZONE_OFFSET); // in milliseconds + + // max Timestamp + difference of current time zone and GMT - provided time zone in milliseconds + tsMax = new Timestamp(tsMax.getTime() + offset - (providedTimeZoneInMinutes * 60 * 1000)); + + if (maxOrMin.toLowerCase().equals("max")) { + int precisionDigits = buildPrecision(precision, "9"); + tsMax.setNanos(precisionDigits); + } + + return microsoft.sql.DateTimeOffset.valueOf(tsMax, providedTimeZoneInMinutes); + } + + private static Integer pickInt(boolean nullable, + int max, + int min) { + if (nullable) { + if (returnNull) { + return null; + } + } + + if (returnZero) { + return 0; + } + + if (returnMinMax) { + if (r.nextBoolean()) { + return max; + } + else { + return min; + } + } + + return (int) r.nextInt(max - min) + min; + } + + private static String buildCharOrNChar(String columnLength, + boolean nullable, + boolean encrypted, + String charSet, + int maxBound) { + + if (nullable) { + if (returnNull) { + return null; + } + } + + // if column is encrypted, string value cannot be "", not supported. + int minimumLength = 0; + if (encrypted) { + minimumLength = 1; + } + + int length; + if (columnLength.toLowerCase().equals("max")) { + // 50% chance of return value longer than 8000/4000 + if (r.nextBoolean()) { + length = r.nextInt(100000) + maxBound; + return buildRandomString(length, charSet); + } + else { + length = r.nextInt(maxBound - minimumLength) + minimumLength; + return buildRandomString(length, charSet); + } + } + else { + int columnLengthInt = Integer.parseInt(columnLength); + if (returnFullLength) { + length = columnLengthInt; + return buildRandomString(length, charSet); + } + else { + length = r.nextInt(columnLengthInt - minimumLength) + minimumLength; + return buildRandomString(length, charSet); + } + } + } + + private static String buildRandomString(int length, + String charSet) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < length; i++) { + char c = pickRandomChar(charSet); + sb.append(c); + } + + return sb.toString(); + } + + private static char pickRandomChar(String charSet) { + int charSetLength = charSet.length(); + + int randomIndex = r.nextInt(charSetLength); + return charSet.charAt(randomIndex); + } + + private static BigInteger newRandomBigInteger(BigInteger n, + Random rnd, + int precision) { + BigInteger r; + do { + r = new BigInteger(n.bitLength(), rnd); + } + while (r.toString().length() != precision); + + return r; + } +} diff --git a/src/test/java/com/microsoft/sqlserver/testframework/util/Util.java b/src/test/java/com/microsoft/sqlserver/testframework/util/Util.java new file mode 100644 index 000000000..f1e5167c4 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/testframework/util/Util.java @@ -0,0 +1,292 @@ +package com.microsoft.sqlserver.testframework.util; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.util.Calendar; + +import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.SQLServerDatabaseMetaData; +import com.microsoft.sqlserver.jdbc.SQLServerStatementColumnEncryptionSetting; + +/** + * Utility class for testing + */ +public class Util { + + /** + * Utility method for generating a prepared statement + * + * @param connection + * connection object + * @param sql + * SQL string + * @param stmtColEncSetting + * SQLServerStatementColumnEncryptionSetting object + * @return + */ + public static PreparedStatement getPreparedStmt(Connection connection, + String sql, + SQLServerStatementColumnEncryptionSetting stmtColEncSetting) throws SQLException { + if (null == stmtColEncSetting) { + return ((SQLServerConnection) connection).prepareStatement(sql); + } + else { + return ((SQLServerConnection) connection).prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, + connection.getHoldability(), stmtColEncSetting); + } + } + + /** + * Utility method for a statement + * + * @param connection + * connection object + * @param sql + * SQL string + * @param stmtColEncSetting + * SQLServerStatementColumnEncryptionSetting object + * @return + */ + public static Statement getStatement(Connection connection, + SQLServerStatementColumnEncryptionSetting stmtColEncSetting) throws SQLException { + // default getStatement assumes resultSet is type_forward_only and concur_read_only + if (null == stmtColEncSetting) { + return ((SQLServerConnection) connection).createStatement(); + } + else { + return ((SQLServerConnection) connection).createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, + connection.getHoldability(), stmtColEncSetting); + } + } + + /** + * Utility method for a scrollable statement + * + * @param connection + * connection object + * @return + */ + public static Statement getScrollableStatement(Connection connection) throws SQLException { + return ((SQLServerConnection) connection).createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); + } + + /** + * Utility method for a scrollable statement + * + * @param connection + * connection object + * @param stmtColEncSetting + * SQLServerStatementColumnEncryptionSetting object + * @return + */ + public static Statement getScrollableStatement(Connection connection, + SQLServerStatementColumnEncryptionSetting stmtColEncSetting) throws SQLException { + return ((SQLServerConnection) connection).createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.TYPE_SCROLL_SENSITIVE, + ResultSet.CONCUR_UPDATABLE, stmtColEncSetting); + } + + /** + * Utility method for a statement + * + * @param connection + * connection object + * @param stmtColEncSetting + * SQLServerStatementColumnEncryptionSetting object + * @param rsScrollSensitivity + * @param rsConcurrence + * @return + */ + public static Statement getStatement(Connection connection, + SQLServerStatementColumnEncryptionSetting stmtColEncSetting, + int rsScrollSensitivity, + int rsConcurrence) throws SQLException { + // overloaded getStatement allows setting resultSet type + if (null == stmtColEncSetting) { + return ((SQLServerConnection) connection).createStatement(rsScrollSensitivity, rsConcurrence, connection.getHoldability()); + } + else { + return ((SQLServerConnection) connection).createStatement(rsScrollSensitivity, rsConcurrence, connection.getHoldability(), + stmtColEncSetting); + } + } + + /** + * Utility method for a callable statement + * + * @param connection + * connection object + * @param stmtColEncSetting + * SQLServerStatementColumnEncryptionSetting object + * @param sql + * @return + */ + public static CallableStatement getCallableStmt(Connection connection, + String sql, + SQLServerStatementColumnEncryptionSetting stmtColEncSetting) throws SQLException { + if (null == stmtColEncSetting) { + return ((SQLServerConnection) connection).prepareCall(sql); + } + else { + return ((SQLServerConnection) connection).prepareCall(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, + connection.getHoldability(), stmtColEncSetting); + } + } + + /** + * Utility method for a datetime value + * + * @param value + * @return + */ + public static Object roundSmallDateTimeValue(Object value) { + if (value == null) { + return null; + } + + Calendar cal; + java.sql.Timestamp ts = null; + int nanos = -1; + + if (value instanceof Calendar) { + cal = (Calendar) value; + } + else { + ts = (java.sql.Timestamp) value; + cal = Calendar.getInstance(); + cal.setTimeInMillis(ts.getTime()); + nanos = ts.getNanos(); + } + + // round to the nearest minute + double seconds = cal.get(Calendar.SECOND) + (nanos == -1 ? ((double) cal.get(Calendar.MILLISECOND) / 1000) : ((double) nanos / 1000000000)); + if (seconds > 29.998) { + cal.set(Calendar.MINUTE, cal.get(Calendar.MINUTE) + 1); + } + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + nanos = 0; + + // required to force computation + cal.getTimeInMillis(); + + // return appropriate value + if (value instanceof Calendar) { + return cal; + } + else { + ts.setTime(cal.getTimeInMillis()); + ts.setNanos(nanos); + return ts; + } + } + + /** + * Utility method for a datetime value + * + * @param value + * @return + */ + public static Object roundDatetimeValue(Object value) { + if (value == null) + return null; + Timestamp ts = value instanceof Timestamp ? (Timestamp) value : new Timestamp(((Calendar) value).getTimeInMillis()); + int millis = ts.getNanos() / 1000000; + int lastDigit = (int) (millis % 10); + switch (lastDigit) { + // 0, 1 -> 0 + case 1: + ts.setNanos((millis - 1) * 1000000); + break; + + // 2, 3, 4 -> 3 + case 2: + ts.setNanos((millis + 1) * 1000000); + break; + case 4: + ts.setNanos((millis - 1) * 1000000); + break; + + // 5, 6, 7, 8 -> 7 + case 5: + ts.setNanos((millis + 2) * 1000000); + break; + case 6: + ts.setNanos((millis + 1) * 1000000); + break; + case 8: + ts.setNanos((millis - 1) * 1000000); + break; + + // 9 -> 0 with overflow + case 9: + ts.setNanos(0); + ts.setTime(ts.getTime() + millis + 1); + break; + + // default, i.e. 0, 3, 7 -> 0, 3, 7 + // don't change the millis but make sure that any + // sub-millisecond digits are zeroed out + default: + ts.setNanos((millis) * 1000000); + } + if (value instanceof Calendar) { + ((Calendar) value).setTimeInMillis(ts.getTime()); + ((Calendar) value).getTimeInMillis(); + return value; + } + return ts; + } + + /** + * Utility function for safely closing open resultset/statement/connection + * + * @param ResultSet + * @param Statement + * @param Connection + */ + public static void close(ResultSet rs, + Statement stmt, + Connection con) { + if (rs != null) { + try { + rs.close(); + + } + catch (SQLException e) { + System.out.println("The result set cannot be closed."); + } + } + if (stmt != null) { + try { + stmt.close(); + } + catch (SQLException e) { + System.out.println("The statement cannot be closed."); + } + } + if (con != null) { + try { + con.close(); + } + catch (SQLException e) { + System.out.println("The data source connection cannot be closed."); + } + } + } + + /** + * Utility function for checking if the system supports JDBC 4.2 + * + * @param con + * @return + */ + public static boolean supportJDBC42(Connection con) throws SQLException { + SQLServerDatabaseMetaData meta = (SQLServerDatabaseMetaData) con.getMetaData(); + return (meta.getJDBCMajorVersion() >= 4 && meta.getJDBCMinorVersion() >= 2); + } +} diff --git a/src/test/resources/BulkCopyCSVTestInput.csv b/src/test/resources/BulkCopyCSVTestInput.csv index f43d20d14..2d28e77bc 100644 --- a/src/test/resources/BulkCopyCSVTestInput.csv +++ b/src/test/resources/BulkCopyCSVTestInput.csv @@ -1,6 +1,6 @@ -bit,tinyint,smallint,int,bigint,float(53),real,decimal(18-6),numeric(18-4),money(20-4),smallmoney(20-4),char(11),nchar(10),varchar(50),nvarchar(10),binary(5),varbinary(25) -1,2,-32768,0,0,-1.78E307,-3.4E38,22.335600,22.3356,-922337203685477.5808,-214748.3648,a5()b,௵ஷஇமண,test to test csv files,ࢨहश,6163686974,6163686974 -,,,,,,,,,,,,,,,, -0,5,32767,1,12,-2.23E-308,-1.18E-38,33.552695,33.5526,922337203685477.5807,0.0000,what!,ৡਐਲ,123 norma black street,Ӧ NӦ,5445535455,54455354 -0,255,0,-2147483648,-9223372036854775808,2.23E-308,0.0,33.503288,33.5032,0.0000,1.0011,no way,Ӧ NӦ,baker street Mr.Homls,àĂ,303C2D3988,303C2D39 -1,5,0,2147483647,9223372036854775807,12.0,3.4E38,33.000501,33.0005,1.0001,214748.3647,l l l l l |,Ȣʗʘ,test to test csv files,௵ஷஇமண,7E7D7A7B20,7E7D7A7B \ No newline at end of file +bit,tinyint,smallint,int,bigint,float(53),real,decimal(18-6),numeric(18-4),money(20-4),smallmoney(20-4),char(11),nchar(10),varchar(50),nvarchar(10),binary(5),varbinary(25),date,datetime,datetime2(7),smalldatetime,datetimeoffset(7),time(16-7) +1,2,-32768,0,0,-1.78E307,-3.4E38,22.335600,22.3356,-922337203685477.5808,-214748.3648,a5()b,௵ஷஇமண,test to test csv files,ࢨहश,6163686974,6163686974,1922-11-02,2004-05-23 14:25:10.487,2007-05-02 19:58:47.1234567,2004-05-23 14:25:00.0,2025-12-10 12:32:10.1234567 +01:00,12:23:48.1234567 +,,,,,,,,,,,,,,,,,,,,,, +0,5,32767,1,12,-2.23E-308,-1.18E-38,33.552695,33.5526,922337203685477.5807,0.0000,what!,ৡਐਲ,123 norma black street,Ӧ NӦ,5445535455,54455354,9999-12-31,9999-12-31 23:59:59.997,9999-12-31 23:59:59.9999999,2079-06-06 23:59:00.0,9999-12-31 23:59:00.0000000 +00:00,23:59:59.9990000 +0,255,0,-2147483648,-9223372036854775808,2.23E-308,0.0,33.503288,33.5032,0.0000,1.0011,no way,Ӧ NӦ,baker street Mr.Homls,àĂ,303C2D3988,303C2D39,0001-01-01,1973-01-01 00:00:00.0,0001-01-01 00:00:00.0000000,1900-01-01 00:00:00.0,0001-01-01 00:00:00.0000000 +00:00,00:00:00.0000000 +1,5,0,2147483647,9223372036854775807,12.0,3.4E38,33.000501,33.0005,1.0001,214748.3647,l l l l l |,Ȣʗʘ,test to test csv files,௵ஷஇமண,7E7D7A7B20,7E7D7A7B,2017-04-18,2014-10-11 20:13:12.123,2017-10-12 09:38:17.7654321,2014-10-11 20:13:00.0,2017-01-06 10:52:20.7654321 +03:00,18:02:16.7654321 diff --git a/src/test/resources/BulkCopyCSVTestInputNoColumnName.csv b/src/test/resources/BulkCopyCSVTestInputNoColumnName.csv new file mode 100644 index 000000000..4c66c771b --- /dev/null +++ b/src/test/resources/BulkCopyCSVTestInputNoColumnName.csv @@ -0,0 +1,5 @@ +1,2,-32768,0,0,-1.78E307,-3.4E38,22.335600,22.3356,-922337203685477.5808,-214748.3648,a5()b,௵ஷஇமண,test to test csv files,ࢨहश,6163686974,6163686974,1922-11-02,2004-05-23 14:25:10.487,2007-05-02 19:58:47.1234567,2004-05-23 14:25:00.0,2025-12-10 12:32:10.1234567 +01:00,12:23:48.1234567 +,,,,,,,,,,,,,,,,,,,,,, +0,5,32767,1,12,-2.23E-308,-1.18E-38,33.552695,33.5526,922337203685477.5807,0.0000,what!,ৡਐਲ,123 norma black street,Ӧ NӦ,5445535455,54455354,9999-12-31,9999-12-31 23:59:59.997,9999-12-31 23:59:59.9999999,2079-06-06 23:59:00.0,9999-12-31 23:59:00.0000000 +00:00,23:59:59.9990000 +0,255,0,-2147483648,-9223372036854775808,2.23E-308,0.0,33.503288,33.5032,0.0000,1.0011,no way,Ӧ NӦ,baker street Mr.Homls,àĂ,303C2D3988,303C2D39,0001-01-01,1973-01-01 00:00:00.0,0001-01-01 00:00:00.0000000,1900-01-01 00:00:00.0,0001-01-01 00:00:00.0000000 +00:00,00:00:00.0000000 +1,5,0,2147483647,9223372036854775807,12.0,3.4E38,33.000501,33.0005,1.0001,214748.3647,l l l l l |,Ȣʗʘ,test to test csv files,௵ஷஇமண,7E7D7A7B20,7E7D7A7B,2017-04-18,2014-10-11 20:13:12.123,2017-10-12 09:38:17.7654321,2014-10-11 20:13:00.0,2017-01-06 10:52:20.7654321 +03:00,18:02:16.7654321