From 6c66e55f767a3cf25984ae7cb5dbb38123079489 Mon Sep 17 00:00:00 2001 From: Shashwat Agarwal Date: Mon, 4 Mar 2024 21:41:57 +0530 Subject: [PATCH] Release #1.0.0.ALPHA-SNAPSHOT --- .github/ISSUE_TEMPLATE/bug_report.md | 23 + .github/ISSUE_TEMPLATE/feature_request.md | 20 + .github/workflows/build.yaml | 28 + .github/workflows/detekt.yaml | 21 + .gitignore | 4 + CONTRIBUTING.md | 42 + LICENSE | 201 +++++ NOTICE | 1 + README.md | 58 ++ detekt.yml | 798 ++++++++++++++++++ pom.xml | 385 +++++++++ r2dbi-core/pom.xml | 204 +++++ .../kotlin/com/udaan/r2dbi/APIAnnotations.kt | 24 + .../kotlin/com/udaan/r2dbi/ArgumentBinders.kt | 71 ++ .../kotlin/com/udaan/r2dbi/ColumnMappers.kt | 51 ++ .../com/udaan/r2dbi/DynamicProxyFactory.kt | 54 ++ .../com/udaan/r2dbi/DynamicProxyImpl.kt | 86 ++ .../kotlin/com/udaan/r2dbi/FluentR2Dbi.kt | 170 ++++ .../com/udaan/r2dbi/ParameterisedSql.kt | 71 ++ .../src/main/kotlin/com/udaan/r2dbi/R2Dbi.kt | 139 +++ .../main/kotlin/com/udaan/r2dbi/RowMappers.kt | 76 ++ .../main/kotlin/com/udaan/r2dbi/SqlBatch.kt | 39 + .../com/udaan/r2dbi/SqlBatchMethodHandler.kt | 67 ++ .../com/udaan/r2dbi/SqlExecutionContext.kt | 127 +++ .../com/udaan/r2dbi/SqlInvocationHandler.kt | 22 + .../com/udaan/r2dbi/SqlMethodHandler.kt | 147 ++++ .../udaan/r2dbi/SqlMethodHandlerFactory.kt | 38 + .../com/udaan/r2dbi/SqlParameterCustomizer.kt | 34 + .../main/kotlin/com/udaan/r2dbi/SqlQuery.kt | 33 + .../com/udaan/r2dbi/SqlQueryMethodHandler.kt | 28 + .../main/kotlin/com/udaan/r2dbi/SqlUpdate.kt | 34 + .../com/udaan/r2dbi/SqlUpdateMethodHandler.kt | 28 + .../com/udaan/r2dbi/StatementContext.kt | 76 ++ .../kotlin/com/udaan/r2dbi/Transaction.kt | 37 + .../r2dbi/TransactionInvocationDecorator.kt | 27 + .../kotlin/com/udaan/r2dbi/binders/Bind.kt | 35 + .../com/udaan/r2dbi/binders/BindFactory.kt | 86 ++ .../com/udaan/r2dbi/binders/BindPojo.kt | 27 + .../udaan/r2dbi/binders/BindPojoFactory.kt | 55 ++ .../com/udaan/r2dbi/mappers/ColumnName.kt | 25 + .../udaan/r2dbi/mappers/ColumnNameMatcher.kt | 52 ++ .../com/udaan/r2dbi/mappers/EnumMapper.kt | 63 ++ .../kotlin/com/udaan/r2dbi/mappers/Nested.kt | 21 + .../com/udaan/r2dbi/mappers/PojoMapper.kt | 263 ++++++ .../r2dbi/mappers/R2DbcNativeTypeMapper.kt | 73 ++ .../udaan/r2dbi/mappers/SingleColumnMapper.kt | 53 ++ .../sql/annotations/UseArgumentBinder.kt | 28 + .../sql/annotations/UseInvocationDecorator.kt | 35 + .../sql/annotations/UseRowMapperFactory.kt | 29 + .../sql/annotations/UseSqlMethodHandler.kt | 35 + .../r2dbi/sql/interfaces/ArgumentBinder.kt | 46 + .../r2dbi/sql/interfaces/ColumnMapper.kt | 40 + .../udaan/r2dbi/sql/interfaces/RowMapper.kt | 33 + .../r2dbi/sql/interfaces/ScopedExecutor.kt | 79 ++ .../sql/interfaces/SqlExecutionCallback.kt | 23 + .../sql/interfaces/SqlInvocationDecorator.kt | 22 + .../udaan/r2dbi/utils/AnnotationHelpers.kt | 42 + .../udaan/r2dbi/utils/ReflectionUtilities.kt | 137 +++ .../kotlin/com/udaan/r2dbi/BaseTestClass.kt | 60 ++ .../test/kotlin/com/udaan/r2dbi/MssqlTests.kt | 68 ++ .../kotlin/com/udaan/r2dbi/PostgreSQLTests.kt | 90 ++ .../com/udaan/r2dbi/R2DbiTestExtensionBase.kt | 103 +++ .../udaan/r2dbi/TestDynamicInterfaceBase.kt | 332 ++++++++ .../com/udaan/r2dbi/TestFluentR2DbiBase.kt | 59 ++ .../kotlin/com/udaan/r2dbi/TestGenerics.kt | 58 ++ .../kotlin/com/udaan/r2dbi/TestMappersBase.kt | 121 +++ .../com/udaan/r2dbi/TestParametrisedSql.kt | 35 + .../r2dbi/TestSqlExecutionContextBase.kt | 54 ++ .../internal/AzureSqlEdgeContainerProvider.kt | 183 ++++ .../r2dbi/internal/LocalSqlServerExtension.kt | 33 + .../internal/SqlDatabaseContainerProvider.kt | 78 ++ .../r2dbi/internal/SqlDatabaseExtension.kt | 40 + .../com/udaan/r2dbi/testDao/ConfigData.kt | 61 ++ .../com/udaan/r2dbi/testDao/TestQueryDao.kt | 102 +++ .../com/udaan/r2dbi/testDao/TestUpdateDao.kt | 36 + .../container-license-acceptance.txt | 2 + .../src/test/resources/logback-test.xml | 39 + 77 files changed, 6120 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/build.yaml create mode 100644 .github/workflows/detekt.yaml create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 detekt.yml create mode 100644 pom.xml create mode 100644 r2dbi-core/pom.xml create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/APIAnnotations.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/ArgumentBinders.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/ColumnMappers.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/DynamicProxyFactory.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/DynamicProxyImpl.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/FluentR2Dbi.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/ParameterisedSql.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/R2Dbi.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/RowMappers.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlBatch.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlBatchMethodHandler.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlExecutionContext.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlInvocationHandler.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlMethodHandler.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlMethodHandlerFactory.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlParameterCustomizer.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlQuery.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlQueryMethodHandler.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlUpdate.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlUpdateMethodHandler.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/StatementContext.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/Transaction.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/TransactionInvocationDecorator.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/binders/Bind.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/binders/BindFactory.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/binders/BindPojo.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/binders/BindPojoFactory.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/ColumnName.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/ColumnNameMatcher.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/EnumMapper.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/Nested.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/PojoMapper.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/R2DbcNativeTypeMapper.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/SingleColumnMapper.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/annotations/UseArgumentBinder.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/annotations/UseInvocationDecorator.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/annotations/UseRowMapperFactory.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/annotations/UseSqlMethodHandler.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/ArgumentBinder.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/ColumnMapper.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/RowMapper.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/ScopedExecutor.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/SqlExecutionCallback.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/SqlInvocationDecorator.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/utils/AnnotationHelpers.kt create mode 100644 r2dbi-core/src/main/kotlin/com/udaan/r2dbi/utils/ReflectionUtilities.kt create mode 100644 r2dbi-core/src/test/kotlin/com/udaan/r2dbi/BaseTestClass.kt create mode 100644 r2dbi-core/src/test/kotlin/com/udaan/r2dbi/MssqlTests.kt create mode 100644 r2dbi-core/src/test/kotlin/com/udaan/r2dbi/PostgreSQLTests.kt create mode 100644 r2dbi-core/src/test/kotlin/com/udaan/r2dbi/R2DbiTestExtensionBase.kt create mode 100644 r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestDynamicInterfaceBase.kt create mode 100644 r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestFluentR2DbiBase.kt create mode 100644 r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestGenerics.kt create mode 100644 r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestMappersBase.kt create mode 100644 r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestParametrisedSql.kt create mode 100644 r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestSqlExecutionContextBase.kt create mode 100644 r2dbi-core/src/test/kotlin/com/udaan/r2dbi/internal/AzureSqlEdgeContainerProvider.kt create mode 100644 r2dbi-core/src/test/kotlin/com/udaan/r2dbi/internal/LocalSqlServerExtension.kt create mode 100644 r2dbi-core/src/test/kotlin/com/udaan/r2dbi/internal/SqlDatabaseContainerProvider.kt create mode 100644 r2dbi-core/src/test/kotlin/com/udaan/r2dbi/internal/SqlDatabaseExtension.kt create mode 100644 r2dbi-core/src/test/kotlin/com/udaan/r2dbi/testDao/ConfigData.kt create mode 100644 r2dbi-core/src/test/kotlin/com/udaan/r2dbi/testDao/TestQueryDao.kt create mode 100644 r2dbi-core/src/test/kotlin/com/udaan/r2dbi/testDao/TestUpdateDao.kt create mode 100644 r2dbi-core/src/test/resources/container-license-acceptance.txt create mode 100644 r2dbi-core/src/test/resources/logback-test.xml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..d3b3685 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,23 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: ''' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +A unit test case that can reproduce the issue + +**Expected behavior** +Description of what you expected to happen. + +**Logs** +If applicable, add exception / logs / screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..82f05f6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: ''' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +Description of what you want to happen. + +**Describe alternatives you've considered** +Description of any alternative solutions or features you've considered. + +**Pull requests** +A pull request will be really appreciated diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..124d855 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,28 @@ +# This workflow will build a Java/Kotlin project with Maven +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Build project + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v3 + - name: setup java 8 + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '8' + cache: 'maven' + - name: Build with Maven + run: mvn -B package --file pom.xml diff --git a/.github/workflows/detekt.yaml b/.github/workflows/detekt.yaml new file mode 100644 index 0000000..825d95c --- /dev/null +++ b/.github/workflows/detekt.yaml @@ -0,0 +1,21 @@ +name: code-review +on: [pull_request] + +jobs: + detekt: + name: Check Code Quality + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Clone repo + uses: actions/checkout@v3 + with: + fetch-depth: 1 + ref: ${{ github.head_ref }} + - name: detekt + uses: alaegin/Detekt-Action@v1.18.1.2 + with: + github_token: ${{ secrets.github_token }} + detekt_config: detekt.yml \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc4b7f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.iml +.idea +target +.flattened-pom.xml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2494dd3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,42 @@ + +We're glad you're thinking about contributing to the project. +Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. + +## Contributing to the project +We use [GitHub](https://github.com/udaan-com/r2dbi) as our central development hub. +[Issues](https://github.com/udaan-com/r2dbi/issues), and [Pull Requests](https://github.com/udaan-com/r2dbi/pulls) all happen here. + +If you find a bug that you can reproduce, please file an issue with the project. +Contributions in the form of bug reports along with potential fixes are greatly appreciated. +You're encouraged to submit a pull request with your proposed solution. +Including a test that showcases both the bug and the fix will expedite the process. + +We may provide feedback on PRs asking you to make changes to your code even if it was not obvious +when you wrote the code and there were no documented rules. This is unfortunate and we apologize for +that in advance. + +We value backwards compatibility for our API. Large PRs that affect the public API will receive a lot of scrutiny. + +### Coding guidelines + +* R2dbi is a library and any dependency that we use, we also force upon our users. Minimize the footprint of external dependencies; +* Prefer stateless, immutable objects (`data` classes) over anything else. +* Ensure that the code is thread-safe wherever required. Clearly document the thread-safety requirement in the changes. +* Maintain backward compatability all the time. If an API must be discouraged, mark it `@Deprecated` and keep it functionally intact +* Make minimal changes to the code. Ensure that the internals of the library are not exposed. Use `internal`/`private` to limit the scope aggressively. +* Any new public interface/method should be marked with [`@ExperimentalAPI`] to tell users to not rely on it + +*Please run `mvn clean install` locally before opening a PR. Your local build run from the command line should pass.* + +### Testing + +* JUnit 5 and `testcontainers` are used for all testing +* code is tested for `mssql` and `postgreSQL` using testcontainers. +* ensure that the functionality is test for both the databases + +## Development Setup + +Most modern IDEs configure themselves correctly by importing the repository as an Apache Maven project. + +### Building +use `mvn clean install` to build and install the package locally diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/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/NOTICE b/NOTICE new file mode 100644 index 0000000..8d41037 --- /dev/null +++ b/NOTICE @@ -0,0 +1 @@ +Copyright © 2024 udaan and the R2dbi project authors diff --git a/README.md b/README.md new file mode 100644 index 0000000..803b3fc --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# R2Dbi +R2Dbi is a Kotlin library inspired by [JDBI](https://github.com/jdbi/jdbi) and built on Reactive Relational Database Connectivity ([R2DBC](https://r2dbc.io/)) SPI. +R2DBC brings [Reactive Programming APIs](https://projectreactor.io/) to relational databases. R2DBI simplifies database operations by offering a +declarative style similar to [JDBI SqlObjects](https://jdbi.org/#sql-objects). +It provides easy-to-use interfaces for executing `SQL queries` and mapping `data` to `objects`. + +While primarily designed for Kotlin, it may also work with Java, although it hasn't been tested yet. +R2dbi started out to be used in declarative mode (ie define an annotated `interface`). The fluent interface is still +being developed and hence not ready to use. It is available to experiment and play with. + +# License +R2Dbi is licensed under the commercial friendly [Apache 2.0 license](LICENSE). + +# Basic Usage +define the SQL to execute and the shape of the results - by creating an annotated `interface`. +```kotlin +import kotlinx.coroutines.flow.Flow +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux + +// Declare the API using annotations on a Java interface +interface UserDao { + // Using kotlinx.coroutines.flow.Flow + @SqlQuery("SELECT 1") + fun getOne(): Flow + + @SqlQuery("SELECT * FROM 'user' where name = :name") + fun findUser(@Bind("name") name: String): Flow; + + // Using reactor.core.publisher.Flux + @SqlQuery("SELECT * FROM 'user'") + fun getAllUsers(): Flux; + +} +``` + +This library supports both `kotlinx.coroutines.flow.Flow` and `org.reactivestreams.Publisher` / `reactor.core.publisher.Flux` +as the return type. Note that no other return type (especially blocking) is supported. + +You look at [TestQueryDao.kt](r2dbi-core%2Fsrc%2Ftest%2Fkotlin%2Fcom%2Fudaan%2Fr2dbi%2FtestDao%2FTestQueryDao.kt) and +[TestDynamicInterfaceBase.kt](r2dbi-core%2Fsrc%2Ftest%2Fkotlin%2Fcom%2Fudaan%2Fr2dbi%2FTestDynamicInterfaceBase.kt) to checkout +more examples. + +> 🚧
+> Before utilizing this library, it's crucial to grasp the fundamentals of Coroutines and Project Reactor. +>
+> For Instance, without subscribing to a `Publisher` or attempting to materialize a value of a `Flow`, the underlying SQL query won't execute. + +# Building, Testing, Contributing +see [CONTRIBUTING.md](CONTRIBUTING.md) + +# Versioning +TODO + +# Project Members +[Shashwat Agarwal](https://github.com/shashwata) + + diff --git a/detekt.yml b/detekt.yml new file mode 100644 index 0000000..6099b05 --- /dev/null +++ b/detekt.yml @@ -0,0 +1,798 @@ +build: + maxIssues: 0 + excludeCorrectable: false + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +config: + validation: true + warningsAsErrors: false + # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' + excludes: '' + +processors: + active: true + exclude: + - 'DetektProgressListener' + # - 'KtFileCountProcessor' + # - 'PackageCountProcessor' + # - 'ClassCountProcessor' + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ProjectComplexityProcessor' + # - 'ProjectCognitiveComplexityProcessor' + # - 'ProjectLLOCProcessor' + # - 'ProjectCLOCProcessor' + # - 'ProjectLOCProcessor' + # - 'ProjectSLOCProcessor' + # - 'LicenseHeaderLoaderExtension' + +console-reports: + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + # - 'FindingsReport' + - 'FileBasedFindingsReport' + +output-reports: + active: true + exclude: + # - 'TxtOutputReport' + # - 'XmlOutputReport' + # - 'HtmlOutputReport' + +comments: + active: true + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: false + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + DeprecatedBlockTag: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + UndocumentedPublicClass: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + UndocumentedPublicFunction: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + UndocumentedPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + +complexity: + active: true + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ComplexMethod: + active: true + threshold: 20 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + LabeledExpression: + active: false + ignoredLabels: [] + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 60 + ignoreAnnotated: [] + LongParameterList: + active: true + excludes: ['**/Connection.kt'] + functionThreshold: 6 + constructorThreshold: 7 + ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotated: [] + MethodOverloading: + active: false + threshold: 6 + NamedArguments: + active: false + threshold: 3 + NestedBlockDepth: + active: true + threshold: 4 + ReplaceSafeCallChainWithRun: + active: false + StringLiteralDuplication: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + thresholdInFiles: 11 + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + +coroutines: + active: true + GlobalCoroutineUsage: + active: false + RedundantSuspendModifier: + active: false + SleepInsteadOfDelay: + active: false + SuspendFunWithFlowReturnType: + active: false + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + NotImplementedDeclaration: + active: false + ObjectExtendsThrowable: + active: false + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(e).*' + TooGenericExceptionThrown: + active: true + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' + +formatting: + active: true + android: false + autoCorrect: true + AnnotationOnSeparateLine: + active: false + autoCorrect: true + AnnotationSpacing: + active: false + autoCorrect: true + ArgumentListWrapping: + active: false + autoCorrect: true + indentSize: 4 + maxLineLength: 120 + ChainWrapping: + active: true + autoCorrect: true + CommentSpacing: + active: true + autoCorrect: true + EnumEntryNameCase: + active: false + autoCorrect: true + Filename: + active: true + FinalNewline: + active: true + autoCorrect: true + insertFinalNewLine: true + ImportOrdering: + active: false + autoCorrect: true + layout: '*,java.**,javax.**,kotlin.**,^' + Indentation: + active: false + autoCorrect: true + indentSize: 4 + continuationIndentSize: 4 + MaximumLineLength: + active: true + maxLineLength: 120 + ignoreBackTickedIdentifier: false + ModifierOrdering: + active: true + autoCorrect: true + MultiLineIfElse: + active: true + autoCorrect: true + NoBlankLineBeforeRbrace: + active: true + autoCorrect: true + NoConsecutiveBlankLines: + active: true + autoCorrect: true + NoEmptyClassBody: + active: true + autoCorrect: true + NoEmptyFirstLineInMethodBlock: + active: false + autoCorrect: true + NoLineBreakAfterElse: + active: true + autoCorrect: true + NoLineBreakBeforeAssignment: + active: true + autoCorrect: true + NoMultipleSpaces: + active: true + autoCorrect: true + NoSemicolons: + active: true + autoCorrect: true + NoTrailingSpaces: + active: true + autoCorrect: true + NoUnitReturn: + active: true + autoCorrect: true + NoUnusedImports: + active: true + autoCorrect: true + NoWildcardImports: + active: true + PackageName: + active: true + autoCorrect: true + ParameterListWrapping: + active: true + autoCorrect: true + indentSize: 4 + maxLineLength: 120 + SpacingAroundAngleBrackets: + active: false + autoCorrect: true + SpacingAroundColon: + active: true + autoCorrect: true + SpacingAroundComma: + active: true + autoCorrect: true + SpacingAroundCurly: + active: true + autoCorrect: true + SpacingAroundDot: + active: true + autoCorrect: true + SpacingAroundDoubleColon: + active: false + autoCorrect: true + SpacingAroundKeyword: + active: true + autoCorrect: true + SpacingAroundOperators: + active: true + autoCorrect: true + SpacingAroundParens: + active: true + autoCorrect: true + SpacingAroundRangeOperator: + active: true + autoCorrect: true + SpacingAroundUnaryOperator: + active: false + autoCorrect: true + SpacingBetweenDeclarationsWithAnnotations: + active: false + autoCorrect: true + SpacingBetweenDeclarationsWithComments: + active: false + autoCorrect: true + StringTemplate: + active: true + autoCorrect: true + +naming: + active: true + BooleanPropertyNaming: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + ignoreOverridden: true + EnumNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + forbiddenName: [] + FunctionMaxLength: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)' + excludeClassPattern: '$^' + ignoreOverridden: true + ignoreAnnotated: + - 'Composable' + FunctionParameterNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + ignoreOverridden: true + InvalidPackageDeclaration: + active: false + excludes: ['**/*.kts'] + rootPackage: '' + MatchingDeclarationName: + active: true + mustBeFirst: true + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: false + NonBooleanPropertyPrefixedWithIs: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + ObjectPropertyNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + maximumVariableNameLength: 64 + VariableMinLength: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + minimumVariableNameLength: 1 + VariableNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + ignoreOverridden: true + +performance: + active: true + ArrayPrimitive: + active: true + ForEachOnRange: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + SpreadOperator: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + AvoidReferentialEquality: + active: false + forbiddenTypePatterns: + - 'kotlin.String' + CastToNullableType: + active: false + Deprecation: + active: false + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: + active: false + DuplicateCaseInWhenExpression: + active: true + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: false + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: false + IgnoredReturnValue: + active: false + restrictToAnnotatedMethods: true + returnValueAnnotations: + - '*.CheckResult' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - '*.CanIgnoreReturnValue' + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: false + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + excludeAnnotatedProperties: [] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: false + MissingWhenCase: + active: true + allowElseExpression: true + NullableToStringCall: + active: false + RedundantElseInWhen: + active: true + UnconditionalJumpStatementInLoop: + active: false + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: false + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + UnsafeCast: + active: true + UnusedUnaryOperator: + active: false + UselessPostfixExpression: + active: false + WrongEqualsTypeParameter: + active: true + +style: + active: true + ClassOrdering: + active: false + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: 'to' + DataClassShouldBeImmutable: + active: false + DestructuringDeclarationWithTooManyEntries: + active: false + maxDestructuringEntries: 3 + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: false + ExplicitCollectionElementAccessMethod: + active: false + ExplicitItLambdaParameter: + active: false + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenComment: + active: true + values: + - 'FIXME:' + - 'STOPSHIP:' + - 'TODO:' + allowedPatterns: '' + ForbiddenImport: + active: false + imports: [] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: false + methods: + - 'kotlin.io.print' + - 'kotlin.io.println' + ForbiddenPublicDataClass: + active: true + excludes: ['**'] + ignorePackages: + - '*.internal' + - '*.internal.*' + ForbiddenVoid: + active: false + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: '' + excludeAnnotatedFunction: + - 'dagger.Provides' + LibraryCodeMustSpecifyReturnType: + active: true + excludes: ['**'] + LibraryEntitiesShouldNotBePublic: + active: true + excludes: ['**'] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: false + ignoreLocalVariableDeclaration: false + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + ignoreRanges: false + ignoreExtensionFunctions: true + MandatoryBracesIfStatements: + active: false + MandatoryBracesLoops: + active: false + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: false + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + ObjectLiteralToLambda: + active: false + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + OptionalWhenBraces: + active: false + PreferToOverPairSyntax: + active: false + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: false + RedundantHigherOrderMapUsage: + active: false + RedundantVisibilityModifierRule: + active: false + ReturnCount: + active: true + max: 2 + excludedFunctions: 'equals' + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: false + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: false + ThrowsCount: + active: true + max: 2 + excludeGuardClauses: false + TrailingWhitespace: + active: false + UnderscoresInNumericLiterals: + active: false + acceptableDecimalLength: 5 + UnnecessaryAbstractClass: + active: true + excludeAnnotatedClasses: + - 'dagger.Module' + UnnecessaryAnnotationUseSiteTarget: + active: false + UnnecessaryApply: + active: true + UnnecessaryFilter: + active: false + UnnecessaryInheritance: + active: true + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + UntilInsteadOfRangeTo: + active: false + UnusedImports: + active: false + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: true + allowedNames: '(_|ignored|expected|serialVersionUID)' + UseArrayLiteralsInAnnotations: + active: false + UseCheckNotNull: + active: false + UseCheckOrError: + active: false + UseDataClass: + active: false + excludeAnnotatedClasses: [] + allowVars: false + UseEmptyCounterpart: + active: false + UseIfEmptyOrIfBlank: + active: false + UseIfInsteadOfWhen: + active: false + UseIsNullOrEmpty: + active: false + UseOrEmpty: + active: false + UseRequire: + active: false + UseRequireNotNull: + active: false + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + WildcardImport: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + excludeImports: + - 'java.util.*' + - 'kotlinx.android.synthetic.*' diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e06949e --- /dev/null +++ b/pom.xml @@ -0,0 +1,385 @@ + + + + + 4.0.0 + + com.udaan.r2dbi + r2dbi + 1.0.0.ALPHA-SNAPSHOT + pom + + A JDBI like client for R2DBC + https://github.com/udaan-com/r2dbi + + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + scm:git:${project.scm.url} + scm:git:${project.scm.url} + git@github.com:udaan-com/r2dbi.git + HEAD + + + + + Shashwat Agarwal + shashwat@udaan.com + Udaan + https://www.udaan.com + + + + + r2dbi-core + + + + UTF-8 + UTF-8 + + 1.8 + 3.0.2 + 5.6.3 + 1.2.3 + 12.4.1.jre8 + 8.0.16 + 42.2.8 + 1.0.0.RELEASE + 1.0.1.RELEASE + 1.0.2.RELEASE + 1.0.2.RELEASE + 0.8.0.RELEASE + 0.8.0.RELEASE + 1.19.3 + 1.9.20 + 1.7.3 + + + + + + + io.r2dbc + r2dbc-spi + ${r2dbc-spi.version} + + + io.r2dbc + r2dbc-pool + ${r2dbc-pool.version} + + + org.postgresql + r2dbc-postgresql + ${r2dbc-postgresql.version} + + + io.r2dbc + r2dbc-mssql + ${r2dbc-mssql.version} + + + + + + + org.jetbrains.kotlin + kotlin-bom + ${kotlin.version} + pom + import + + + + org.jetbrains.kotlinx + kotlinx-coroutines-bom + ${kotlinx.coroutines} + pom + import + + + + + + + io.projectreactor + reactor-bom + 2022.0.9 + pom + import + + + + io.projectreactor.netty + reactor-netty + 1.1.9 + + + + io.projectreactor.netty + reactor-netty-core + 1.1.9 + + + + + + org.junit + junit-bom + ${junit.version} + pom + import + + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + pom + import + + + + com.google.code.findbugs + jsr305 + ${jsr305.version} + provided + + + + ch.qos.logback + logback-classic + ${logback.version} + test + + + + dev.miku + r2dbc-mysql + ${r2dbc-mysql.version} + test + + + + com.microsoft.sqlserver + mssql-jdbc + ${mssql-jdbc.version} + test + + + + org.postgresql + postgresql + ${postgresql.version} + test + + + + io.projectreactor + reactor-test + 3.3.0.RELEASE + test + + + + io.r2dbc + r2dbc-spi-test + ${r2dbc-spi.version} + test + + + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + + org.jetbrains.dokka + dokka-maven-plugin + 1.9.10 + + + attach-javadocs + package + + javadocJar + + + + + + + https://projectreactor.io/docs/core/release/api/ + + + https://www.reactive-streams.org/reactive-streams-1.0.2-javadoc/ + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + true + + + compile + + + test-compile + + + + ${java.version} + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + ossrh + https://oss.sonatype.org/ + true + + + + + org.apache.maven.plugins + maven-release-plugin + 2.5.3 + + deploy + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + --pinentry-mode + loopback + + + + + + + + org.codehaus.mojo + flatten-maven-plugin + 1.5.0 + + bom + + remove + remove + remove + interpolate + + + + + flatten + process-resources + + flatten + + + + flatten.clean + clean + + clean + + + + + + + + + + org.codehaus.mojo + flatten-maven-plugin + + + + + + + dist-ossrh + + + ossrh-release + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + diff --git a/r2dbi-core/pom.xml b/r2dbi-core/pom.xml new file mode 100644 index 0000000..7705a64 --- /dev/null +++ b/r2dbi-core/pom.xml @@ -0,0 +1,204 @@ + + + + + 4.0.0 + + + com.udaan.r2dbi + r2dbi + 1.0.0.ALPHA-SNAPSHOT + + + r2dbi-core + 1.0.0.ALPHA-SNAPSHOT + jar + + + + io.r2dbc + r2dbc-spi + + + io.r2dbc + r2dbc-pool + + + org.jetbrains.kotlin + kotlin-stdlib + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactive + + + org.jetbrains.kotlin + kotlin-reflect + + + + io.projectreactor.netty + reactor-netty + + + + io.projectreactor.netty + reactor-netty-core + + + + com.google.code.findbugs + jsr305 + provided + + + + + ch.qos.logback + logback-classic + test + + + dev.miku + r2dbc-mysql + test + + + com.microsoft.sqlserver + mssql-jdbc + test + + + org.postgresql + postgresql + test + + + io.projectreactor + reactor-test + test + + + io.r2dbc + r2dbc-spi-test + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.postgresql + r2dbc-postgresql + test + + + io.r2dbc + r2dbc-mssql + test + + + org.testcontainers + r2dbc + test + + + org.testcontainers + mssqlserver + test + + + org.testcontainers + mysql + test + + + org.testcontainers + postgresql + test + + + org.jetbrains.kotlin + kotlin-test + test + + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + + org.apache.maven.plugins + maven-deploy-plugin + + + org.jetbrains.dokka + dokka-maven-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.1 + + random + + **/*Tests.java + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.codehaus.mojo + flatten-maven-plugin + + + + + ${project.basedir} + + LICENSE + NOTICE + + META-INF + + + + + diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/APIAnnotations.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/APIAnnotations.kt new file mode 100644 index 0000000..09d4719 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/APIAnnotations.kt @@ -0,0 +1,24 @@ +/* + * 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 com.udaan.r2dbi + + +/** + * Experimental API, implementation will change in future + */ +@RequiresOptIn +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) +annotation class ExperimentalAPI diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/ArgumentBinders.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/ArgumentBinders.kt new file mode 100644 index 0000000..30cb515 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/ArgumentBinders.kt @@ -0,0 +1,71 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.sql.annotations.UseArgumentBinder +import com.udaan.r2dbi.sql.interfaces.ArgumentBinder +import com.udaan.r2dbi.sql.interfaces.ArgumentBinderFactory +import com.udaan.r2dbi.sql.interfaces.MethodArg +import com.udaan.r2dbi.utils.findAnnotatedWith +import com.udaan.r2dbi.utils.findAnnotationWhichIsAnnotatedWith +import java.lang.reflect.Method +import java.lang.reflect.Parameter +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap +import kotlin.reflect.full.primaryConstructor + +class ArgumentBinders { + private val cache: ConcurrentMap> = ConcurrentHashMap() + + fun getArgBindersFor(sqlInterface: Class<*>, method: Method): List { + val params = method.parameters + + return params.mapIndexedNotNull { index, param: Parameter -> + val (argAnnotation, _) = param.findAnnotationWhichIsAnnotatedWith() + ?: return@mapIndexedNotNull null + + val arg = MethodArg(param.name, param.type, param.parameterizedType, index) + + @Suppress("UNCHECKED_CAST") + val factory = getBinderFactoryFor(argAnnotation) + as ArgumentBinderFactory + + val argBinder = factory.buildForParameter(argAnnotation, sqlInterface, method, arg) + BoundedArgumentBinder(arg, argBinder) + } + } + + private fun getBinderFactoryFor(annotation: Annotation): ArgumentBinderFactory { + return cache.computeIfAbsent(annotation) { + val factoryClass = annotation.findAnnotatedWith() + ?.value + ?: throw IllegalArgumentException("Not a valid UseArgumentBinder annotation. It is not annotated with ${UseArgumentBinder::class.simpleName}") + + factoryClass.primaryConstructor?.call() + ?: throw IllegalArgumentException("Cannot create an ArgumentBinderFactory instance. No primary constructor available.") + } + } +} + +class BoundedArgumentBinder( + private val methodArg: MethodArg, + private val argumentBinder: ArgumentBinder +) : ArgumentBinder by argumentBinder { + fun getIndex() = methodArg.index + + fun getArgType() = methodArg.type + fun getParameterisedType() = methodArg.parameterizedType +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/ColumnMappers.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/ColumnMappers.kt new file mode 100644 index 0000000..4e2d501 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/ColumnMappers.kt @@ -0,0 +1,51 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.sql.interfaces.ColumnMapper +import com.udaan.r2dbi.sql.interfaces.ColumnMapperFactory +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.reflect.KClass + +class ColumnMappers { + private val cache = ConcurrentHashMap, ColumnMapper<*>?>() + + //NOTE:shashwat - ColumnMapperFactory should be polled according to the order they were added + //Usually, when a column mapper factory is registered, there is an order in which the user + //imagines them to be looked. First the internal factories should be used + // and then the external registered factories + private val factories = ConcurrentHashMap, ColumnMapperFactory>() + private val factoriesList = ConcurrentLinkedQueue() + + fun registerColumnMapperFactory(factory: ColumnMapperFactory): ColumnMappers { + factories.computeIfAbsent(factory::class) { + factoriesList.add(factory) + factory + } + return this + } + + fun getColumnMapperFor(clazz: Class<*>): ColumnMapper<*>? { + return cache.computeIfAbsent(clazz, this::createRowMapperForClass) + } + + private fun createRowMapperForClass(t: Class<*>): ColumnMapper<*>? { + return factoriesList.firstNotNullOfOrNull { + it.build(t) + } + } +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/DynamicProxyFactory.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/DynamicProxyFactory.kt new file mode 100644 index 0000000..60fd59d --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/DynamicProxyFactory.kt @@ -0,0 +1,54 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.sql.interfaces.ScopedExecutor +import java.lang.reflect.Proxy +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap +import java.util.function.Supplier + +internal class DynamicProxyFactory internal constructor(private val r2dbi: R2Dbi) { + private val cache: ConcurrentMap, Any> = ConcurrentHashMap() + private val sqlMethodHandlerFactory = SqlMethodHandlerFactory() + + fun create(sqlInterface: Class): T { + @Suppress("UNCHECKED_CAST") + return cache.computeIfAbsent(sqlInterface) { + createWithScopeSupplier(sqlInterface, r2dbi.onDemandScopedExecutorSupplier()) + } as T + } + + internal fun createWithScopeSupplier(sqlInterface: Class, scopedExecutorSupplier: Supplier): T { + if (!sqlInterface.isInterface) { + throw IllegalArgumentException("${sqlInterface.simpleName} is not an interface class") + } + + @Suppress("UNCHECKED_CAST") + return Proxy.newProxyInstance( + sqlInterface.classLoader, arrayOf(sqlInterface), + DynamicProxyImpl( + sqlInterface, + scopedExecutorSupplier, + sqlMethodHandlerFactory + ) + ) as T + } + + inline fun create(): T { + return create(T::class.java) + } +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/DynamicProxyImpl.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/DynamicProxyImpl.kt new file mode 100644 index 0000000..c6e0998 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/DynamicProxyImpl.kt @@ -0,0 +1,86 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.sql.annotations.UseInvocationDecorator +import com.udaan.r2dbi.sql.interfaces.ScopedExecutor +import com.udaan.r2dbi.utils.findAllAnnotationsWhichIsAnnotatedWith +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Method +import java.util.function.Supplier +import kotlin.reflect.full.primaryConstructor + +class DynamicProxyImpl internal constructor( + private val sqlInterface: Class, + private val scopedExecutorSupplier: Supplier, + private val sqlMethodHandlerFactory: SqlMethodHandlerFactory +) : InvocationHandler { + private val allHandlers: Map = getMethodHandlers() + + override fun invoke(proxy: Any, method: Method, args: Array?): Any? { + if (EQUALS_METHOD.equals(method)) { + return proxy === args?.get(0) + } + + if (HASHCODE_METHOD.equals(method)) { + return System.identityHashCode(proxy) + } + + if (TOSTRING_METHOD.equals(method)) { + return "R2dbi on demand proxy for ${sqlInterface.getName()}@${ + Integer.toHexString(System.identityHashCode(proxy)) + }" + } + + val sqlInvocationHandler = findMethodHandler(method) + val scopedExecutor = scopedExecutorSupplier.get() + + return sqlInvocationHandler.invoke(scopedExecutor, proxy, args ?: arrayOf()) + } + + private fun getMethodHandlers(): Map { + return sqlInterface.methods.associateWith { method -> + val handler = sqlMethodHandlerFactory.createHandler(sqlInterface, method) + decorateInvocationHandler(method, handler) + } + } + + private fun decorateInvocationHandler( + method: Method, + handler: SqlInvocationHandler + ): SqlInvocationHandler { + val decorators = method.findAllAnnotationsWhichIsAnnotatedWith() + .mapNotNull { (annotation, useAnnotation) -> + useAnnotation.value.primaryConstructor?.call(annotation) + } + + val finalHandler = decorators.fold(handler) { invocationHandler, sqlInvocationDecorator -> + sqlInvocationDecorator.decorate(invocationHandler) + } + + return finalHandler + } + + private fun findMethodHandler(method: Method): SqlInvocationHandler { + return allHandlers.get(method) + ?: throw IllegalStateException("No handler found for interface: ${sqlInterface.simpleName}, method: ${method.name} ") + } +} + +private val objectClass = Object::class.java +private val TOSTRING_METHOD = objectClass.getMethod("toString") +private val HASHCODE_METHOD = objectClass.getMethod("hashCode") +private val EQUALS_METHOD = objectClass.getMethod("equals", objectClass) diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/FluentR2Dbi.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/FluentR2Dbi.kt new file mode 100644 index 0000000..6b2f59b --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/FluentR2Dbi.kt @@ -0,0 +1,170 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.binders.BindPojoImpl +import com.udaan.r2dbi.utils.wrapPrimitiveType +import io.r2dbc.spi.Result +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.reactive.asFlow +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.util.* +import java.util.function.Consumer + +@ExperimentalAPI +class FluentR2Dbi(private val sqlExecutionContextMono: Mono) { + + fun sql(sql: String): SqlStatement { + val statementContextMono = sqlExecutionContextMono.map { + StatementState(it.createStatementContext(sql), it) + } + + return SqlStatement(statementContextMono) + } +} + +class SqlStatement internal constructor(statementStateMono: Mono) { + private var thisStatementStateMono: Mono = statementStateMono + + fun bindName(name: String, value: Any): SqlStatement { + thisStatementStateMono = thisStatementStateMono.map { + it.statementContext.bind(name, value) + it + } + return this + } + + fun bindPojo(pojo: Any): SqlStatement { + thisStatementStateMono = thisStatementStateMono.map { + BindPojoImpl().bindArg(it.statementContext, pojo) + it + } + + return this + } + + fun execute(): ResultBearing { + val resultFlux = thisStatementStateMono.flatMapMany {state -> + val result = state.statementContext.execute() + Flux.from(result).map { + ResultState(it, state.sqlExecutionContext) + } + } + + return ResultBearing(resultFlux) + } +} + +class ResultBearing internal constructor(private val resultSateFlux: Flux) { + fun subscribeRowsUpdated(onLongValue: Consumer) { + mapToRowsUpdated() + .subscribe(onLongValue) + } + + fun rowsUpdated(): Flow { + return mapToRowsUpdated() + .asFlow() + } + + fun subscribeNotNull(clazz: Class, onValue: Consumer) { + mapRowsInternal(clazz) + .subscribe { + when (it) { + is Optional<*> -> {} + else -> onValue.accept(it as T) + } + } + } + + fun subscribeRows(clazz: Class, onValue: Consumer) { + mapRowsInternal(clazz) + .subscribe { + when (it) { + is Optional<*> -> onValue.accept(null) + else -> onValue.accept(it as T) + } + } + } + + fun mapToNotNull(clazz: Class): Flow { + @Suppress("UNCHECKED_CAST") + return (mapRowsInternal(clazz).filter { + it !is Optional<*> + } as Flux) + .asFlow() + } + + private fun mapToRowsUpdated(): Flux { + return scopedExecution { (result, _) -> + result.rowsUpdated + } + } + + private fun mapRowsInternal(clazz: Class): Flux { + if (clazz.isPrimitive) { + return mapRowsInternal(wrapPrimitiveType(clazz)) + } + + return scopedExecution { (result, sqlExecutionContext) -> + val rowMapper = sqlExecutionContext.findRowMapper(clazz) + ?: return@scopedExecution Flux.error(IllegalArgumentException("No row mapper found for ${clazz.name}")) + + result.map { row, _ -> + val value = rowMapper.map(row, sqlExecutionContext) + value?.let { + value + } ?: Optional.empty() + } + } + } + + private fun scopedExecution(fn: (ResultState) -> Publisher): Flux { + return Flux.usingWhen( + resultSateFlux, + { + withTryCatch(fn, it) + }, + + ) { + it.sqlExecutionContext.close() + } + } + + private fun withTryCatch( + fn: (ResultState) -> Publisher, + it: ResultState + ): Publisher { + return try { + fn(it) + } catch (e: Exception) { + Flux.error(e) + } + } +} + + +internal data class ResultState( + val result: Result, + val sqlExecutionContext: SqlExecutionContext +) + +internal data class StatementState( + val statementContext: StatementContext, + val sqlExecutionContext: SqlExecutionContext +) + diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/ParameterisedSql.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/ParameterisedSql.kt new file mode 100644 index 0000000..7567992 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/ParameterisedSql.kt @@ -0,0 +1,71 @@ +/* + * 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 com.udaan.r2dbi + +internal data class SqlParameter(val index: Int, val name: String) + +class ParameterisedSql internal constructor(sql: String, private val customizer: SqlParameterCustomizer) { + private val sqlWithoutComments = singleLineCommentsRegex.replace(sql) { "" }.trim() + + internal val parameters: Map = mutableMapOf().also { mutableMap -> + var indexToUse = 0 + val stringWithNoQuotedStrings = quotedStringRegex.replace(sqlWithoutComments) { "" } + + paramRegex + .findAll(stringWithNoQuotedStrings) + .forEach { match -> + val group = match.groups[1] + if (group != null) { + //Handling a special case where '::' is used - such as in postgres queries to cast a field to a type + if (group.range.first > 2 && stringWithNoQuotedStrings[group.range.first - 2] == ':') { + // do nothing; ignore + } else { + val name = group.value + mutableMap.computeIfAbsent(name) { + val indexForParam = indexToUse++ + SqlParameter(indexForParam, it) + } + } + } + } + } + + internal val finalSql = if (paramRegex.containsMatchIn(sql)) { + paramRegex.replace(sql) { r -> + r.groups[1]?.let { + parameters[it.value] + }?.let { + customizer.getParameterName(it) + } ?: r.value + } + } else { + sql + } + + internal val arguments = parameters.mapValues { customizer.getArgumentName(it.value) } +} + +internal class ParameterisedSqlFactory( + private val customizer: SqlParameterCustomizer +) { + fun create(sql: String): ParameterisedSql = ParameterisedSql(sql, customizer) +} + +private const val paramIdentifier = ":" +private val paramRegex = "$paramIdentifier(\\p{Alpha}[\\w_]{0,127})".toRegex() +private val singleLineCommentsRegex = "(/\\*.*?\\*/)".toRegex() +private val quotedStringRegex = "((['\"]).*?\\2)".toRegex() + diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/R2Dbi.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/R2Dbi.kt new file mode 100644 index 0000000..4535207 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/R2Dbi.kt @@ -0,0 +1,139 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.mappers.EnumMapperFactory +import com.udaan.r2dbi.mappers.PojoMapperFactory +import com.udaan.r2dbi.mappers.R2DbcNativeTypeMapperFactory +import com.udaan.r2dbi.sql.interfaces.ScopedExecutor +import com.udaan.r2dbi.sql.interfaces.SqlExecutionCallback +import com.udaan.r2dbi.utils.isKotlinClass +import io.r2dbc.pool.ConnectionPool +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.util.function.Supplier + +class R2Dbi private constructor(private val sqlConnectionPool: ConnectionPool) { + private val rowMappers: RowMappers = RowMappers() + private val columnMappers: ColumnMappers = ColumnMappers() + private val dynamicProxyFactory by lazy { DynamicProxyFactory(this) } + + private val parameterCustomizer: SqlParameterCustomizer by lazy { + findParameterCustomizer() + } + + fun getRowMappers() = rowMappers + + fun getColumnMappers() = columnMappers + + fun execute(fn: SqlExecutionCallback): Flux { + return Flux.usingWhen( + openExecutionContext(), + fn::withContext, + SqlExecutionContext::close + ) + } + + fun onDemand(sqlInterface: Class): T { + return getFactory().create(sqlInterface) + } + + internal fun parameterCustomizer() = parameterCustomizer + + @ExperimentalAPI + internal fun attachTo(sqlInterface: Class, scopedExecutor: ScopedExecutor): T { + return getFactory().createWithScopeSupplier(sqlInterface, scopedExecutor.thisSupplier()) + } + + @ExperimentalAPI + internal fun createScopedExecutor(): ScopedExecutor = OnDemandScopedExecutor() + + @ExperimentalAPI + internal fun open(): FluentR2Dbi = FluentR2Dbi(openExecutionContext()) + + internal fun onDemandScopedExecutorSupplier(): Supplier { + return Supplier { + OnDemandScopedExecutor() + } + } + + private fun openExecutionContext(): Mono = sqlConnectionPool + .create() + .map { conn -> + SqlExecutionContext(conn, this) + } + + private fun getFactory(): DynamicProxyFactory { + return dynamicProxyFactory + } + + private fun findParameterCustomizer(): SqlParameterCustomizer { + return when(sqlConnectionPool.metadata.name) { + "PostgreSQL" -> PostgreSQLParamCustomizer + "MySQL" -> SqlParameterCustomizer { "?${it.name}" } + "Microsoft SQL Server" -> SqlParameterCustomizer { "@${it.name}" } + else -> { + SqlParameterCustomizer { "@${it.name}" } + } + } + } + + companion object { + fun withPool(sqlConnectionPool: ConnectionPool): R2Dbi { + return R2Dbi(sqlConnectionPool).also { + it.getColumnMappers() + .registerColumnMapperFactory(R2DbcNativeTypeMapperFactory()) + .registerColumnMapperFactory(EnumMapperFactory()) + it.getRowMappers() + .registerRowMapperFactory(DefaultKotlinClassRowMapperFactory()) + } + } + } + + private inner class OnDemandScopedExecutor : ScopedExecutor { + override fun use(fn: SqlExecutionCallback): Publisher { + return execute(CatchingSqlExecutionCallback(fn)) + } + } +} + +private class CatchingSqlExecutionCallback(private val fn: SqlExecutionCallback) : SqlExecutionCallback { + override fun withContext(sqlExecutionContext: SqlExecutionContext): Publisher { + return try { + fn.withContext(sqlExecutionContext) + } catch (e: Exception) { + Mono.error(e) + } + } +} + +internal object PostgreSQLParamCustomizer : SqlParameterCustomizer { + override fun getParameterName(parameter: SqlParameter): String { + return "$${parameter.index + 1}" + } + + override fun getArgumentName(parameter: SqlParameter): String { + return "$${parameter.index + 1}" + } + +} + +private class DefaultKotlinClassRowMapperFactory : PojoMapperFactory() { + override fun isSupportedType(type: Class<*>): Boolean { + return type.isKotlinClass() + } +} \ No newline at end of file diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/RowMappers.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/RowMappers.kt new file mode 100644 index 0000000..1b4b197 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/RowMappers.kt @@ -0,0 +1,76 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.sql.interfaces.RowMapper +import com.udaan.r2dbi.sql.interfaces.RowMapperFactory +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.reflect.KClass +import kotlin.reflect.full.primaryConstructor + +class RowMappers { + private val cache = ConcurrentHashMap, RowMapper<*>?>() + + //NOTE:shashwat - RowMapperFactories should be polled according to the order they were added + //Usually, when a row mapper factory is registered, there is an order in which the user + //imagines them to be looked. First the internal factories should be used + // and then the external registered factories + private val rowMapperFactories = ConcurrentHashMap, RowMapperFactory>() + private val rowMapperFactoriesList = ConcurrentLinkedQueue() + + + fun registerRowMapperFactory(rowMapperFactory: RowMapperFactory): RowMappers { + rowMapperFactories.computeIfAbsent(rowMapperFactory::class) { + rowMapperFactoriesList.add(rowMapperFactory) + rowMapperFactory + } + return this + } + + fun registerRowMapperFactory(rowMapperFactoryClass: KClass): RowMappers { + rowMapperFactories.computeIfAbsent(rowMapperFactoryClass) { + //TODO -> !! operator used + val mapperFactory = it.primaryConstructor!!.call() + rowMapperFactoriesList.add(mapperFactory) + mapperFactory + } + return this + } + + fun registerRowMapper(klass: Class<*>, rowMapper: RowMapper<*>): RowMappers { + cache.putIfAbsent(klass, rowMapper) + return this + } + + fun getRowMapperFor(klass: Class<*>): RowMapper<*>? { + return cache.computeIfAbsent(klass, this::createRowMapperForClass) + } + + fun getRowMapperFor(klass: Class<*>, factoryClass: KClass): RowMapper<*>? { + registerRowMapperFactory(factoryClass) + return cache.computeIfAbsent(klass) { + rowMapperFactories[factoryClass] + ?.build(klass) + } + } + + private fun createRowMapperForClass(t: Class<*>): RowMapper<*>? { + return rowMapperFactoriesList.firstNotNullOfOrNull { + it.build(t) + } + } +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlBatch.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlBatch.kt new file mode 100644 index 0000000..27d8add --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlBatch.kt @@ -0,0 +1,39 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.sql.annotations.UseSqlMethodHandler + +/** + * Used to indicate that a method should execute SQL batch. + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) +@UseSqlMethodHandler(value = SqlBatchMethodHandler::class) +annotation class SqlBatch( + /** + * The sql to be executed. + * + * @return the SQL string (or name) + */ + val value: String, + + /** + * Whether to return updated rows or not + * If set to true, then, the method's return type should be Long + */ + val returnUpdatedRows: Boolean = false +) diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlBatchMethodHandler.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlBatchMethodHandler.kt new file mode 100644 index 0000000..ee83819 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlBatchMethodHandler.kt @@ -0,0 +1,67 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.utils.isIterableOrArray +import com.udaan.r2dbi.utils.iteratorFrom +import java.lang.reflect.Method + +internal class SqlBatchMethodHandler(method: Method) : SqlMethodHandler(method) { + private val batchAnnotation: SqlBatch = method.getAnnotation(SqlBatch::class.java) + private val listMethodArgBinder = getListMethodArg() + + override fun getSqlString(): String { + return batchAnnotation.value + } + + override fun returnUpdatedCount(): Boolean = batchAnnotation.returnUpdatedRows + + override fun bindArgs( + statementContext: StatementContext, + args: Array + ) { + val listArgIterator = listMethodArgBinder?.let { + args.getOrNull(it.getIndex()) + }?.let { + iteratorFrom(it) + } ?: return super.bindArgs(statementContext, args) + + for (argValue in listArgIterator) { + args.forEachIndexed { index, value -> + val (bindValue, methodArgBinder) = if (index == listMethodArgBinder.getIndex()) { + argValue to listMethodArgBinder + } else { + value to getArgBinderAtIndex(index) + } + + methodArgBinder.bindArg(statementContext, bindValue) + } + + if (listArgIterator.hasNext()) { + statementContext.addNewBindings() + } + } + } + + private fun getListMethodArg(): BoundedArgumentBinder? { + val list = filterArgBinders { isIterableOrArray(it.getArgType()) } + return when (list.size) { + 0 -> null + 1 -> list.first() + else -> throw IllegalArgumentException("Only 1 list element is allowed, has more than 1 ${list.map { it.getIndex() }}") + } + } +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlExecutionContext.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlExecutionContext.kt new file mode 100644 index 0000000..7b16b6c --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlExecutionContext.kt @@ -0,0 +1,127 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.mappers.SingleColumnMapper +import com.udaan.r2dbi.sql.annotations.UseRowMapperFactory +import com.udaan.r2dbi.sql.interfaces.ColumnMapper +import com.udaan.r2dbi.sql.interfaces.RowMapper +import com.udaan.r2dbi.sql.interfaces.ScopedExecutor +import com.udaan.r2dbi.sql.interfaces.SqlExecutionCallback +import io.r2dbc.spi.Closeable +import io.r2dbc.spi.Connection +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.util.concurrent.atomic.AtomicBoolean + +class SqlExecutionContext( + private val connection: Connection, + private val r2dbi: R2Dbi +) : Closeable { + private val parameterisedSqlFactory = ParameterisedSqlFactory(r2dbi.parameterCustomizer()) + + private val isInTransaction = AtomicBoolean(false) + + internal fun inTransaction(isolationLevel: TransactionIsolationLevel): ScopedExecutor { + val didStartTransaction = isInTransaction.compareAndSet(false, true) + + val transactionMono = if (didStartTransaction) { + val isolationL = isolationLevel.r2dbcLevel ?: connection.transactionIsolationLevel + Mono.from( + connection.beginTransaction(isolationL) + ).thenReturn(true) + } else { + Mono.just(false) + } + + return TransactionScope(transactionMono) + } + + internal fun getParameterisedSqlFactory(): ParameterisedSqlFactory { + return parameterisedSqlFactory + } + fun createStatementContext(sql: String): StatementContext { + return createStatementContext(parameterisedSqlFactory.create(sql)) + } + + fun createStatementContext(sql: ParameterisedSql): StatementContext { + val statement = connection.createStatement(sql.finalSql) + return StatementContext(sql, statement) + } + + fun resolveRowMapper(type: Class<*>, rowMapperAnnotation: UseRowMapperFactory): RowMapper<*>? { + return r2dbi.getRowMappers().getRowMapperFor(type, rowMapperAnnotation.value) + } + + fun findRowMapper(type: Class<*>): RowMapper<*>? { + return r2dbi.getRowMappers().getRowMapperFor(type) + ?: findColumnMapper(type)?.let { + val rowMapper = SingleColumnMapper.mapperOf(it) + r2dbi.getRowMappers().registerRowMapper(type, rowMapper) + rowMapper + } + } + + fun findColumnMapper(type: Class<*>): ColumnMapper<*>? { + return r2dbi.getColumnMappers().getColumnMapperFor(type) + } + + override fun close(): Publisher { + return connection.close() + } + + private inner class TransactionScope( + private val transactionPublisher: Publisher, + ) : ScopedExecutor { + override fun use(fn: SqlExecutionCallback): Flux { + return Flux.usingWhen( + transactionPublisher, + { + fn.withContext(this@SqlExecutionContext) + }, + { didStartTransaction -> + doFinally(didStartTransaction) { + connection.commitTransaction() + } + }, + { didStartTransaction, _ -> + doFinally(didStartTransaction) { + connection.rollbackTransaction() + } + }, + { didStartTransaction -> + doFinally(didStartTransaction) { + connection.rollbackTransaction() + } + } + ) + } + + private fun doFinally(didStartTransaction: Boolean, callback: () -> Publisher<*>): Mono<*> { + return if (didStartTransaction) { + Mono.from( + callback() + ).doFinally { + isInTransaction.compareAndSet(true, false) + } + } else { + Mono.empty() + } + } + } +} + diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlInvocationHandler.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlInvocationHandler.kt new file mode 100644 index 0000000..894f062 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlInvocationHandler.kt @@ -0,0 +1,22 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.sql.interfaces.ScopedExecutor + +internal fun interface SqlInvocationHandler { + fun invoke(scopedExecutor: ScopedExecutor, target: Any, args: Array): Any? +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlMethodHandler.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlMethodHandler.kt new file mode 100644 index 0000000..7f8e730 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlMethodHandler.kt @@ -0,0 +1,147 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.sql.annotations.UseRowMapperFactory +import com.udaan.r2dbi.sql.interfaces.RowMapper +import com.udaan.r2dbi.sql.interfaces.ScopedExecutor +import com.udaan.r2dbi.utils.unwrapGenericArg +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.reactive.asFlow +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import java.lang.reflect.Method +import java.util.concurrent.atomic.AtomicReference + +internal abstract class SqlMethodHandler(private val method: Method) : SqlInvocationHandler { + private val declaringClass = method.declaringClass + private val flowOrPublisherReturnType = method.returnType + private val returnType: Class<*> = unwrapReturnType(method) + + private val methodRowMapperAnnotation: UseRowMapperFactory? = method.getAnnotation(UseRowMapperFactory::class.java) + private val classRowMapperAnnotation: Array = + declaringClass.getAnnotationsByType(UseRowMapperFactory::class.java) + + private val methodArgBinders = argBinders.getArgBindersFor(declaringClass, method) + + private val resolvedRowMapper = AtomicReference>() + + private val parameterisedSqlRef = AtomicReference() + + override fun invoke(scopedExecutor: ScopedExecutor, target: Any, args: Array): Any? { + val publisher = scopedExecutor.use { executionContext -> + val statementContext = prepareSqlStatement(executionContext, args) + + val publisher: Publisher<*> = if (returnUpdatedCount()) { + statementContext.executeUpdate() + } else { + val rowMapper = resolvedRowMapper.get() + ?: resolvedRowMapper.updateAndGet { r -> + r ?: executionContext.getRowMapper() + } + + statementContext.executeQueryAndMap { it, _ -> + rowMapper.map(it, executionContext) + ?: throw IllegalStateException("Null value found for returnType: ${returnType.name} when mapping result in method: ${method.name} in clazz: ${declaringClass.name}") + } + } + + publisher + } + return castToReturnType(publisher) + } + + private fun prepareSqlStatement(sqlExecutionContext: SqlExecutionContext, args: Array): StatementContext { + val parameterisedSql = parameterisedSqlRef.computeIfAbsent { + sqlExecutionContext.getParameterisedSqlFactory().create( + getSqlString() + ) + } + val statementContext = sqlExecutionContext.createStatementContext(parameterisedSql) + bindArgs(statementContext, args) + + return statementContext + } + + private fun castToReturnType(publisher: Publisher): Any? { + return if (Flow::class.java.isAssignableFrom(flowOrPublisherReturnType)) { + publisher.asFlow() + } else if (Flux::class.java.isAssignableFrom(flowOrPublisherReturnType)) { + Flux.from(publisher) + } else { + publisher + } + } + + protected abstract fun getSqlString(): String + + protected abstract fun returnUpdatedCount(): Boolean + + protected open fun bindArgs( + statementContext: StatementContext, + args: Array + ) { + args.forEachIndexed { index, value -> + val argBinder = getArgBinderAtIndex(index) + argBinder.bindArg(statementContext, value) + } + } + + protected fun filterArgBinders(predicate: (BoundedArgumentBinder) -> Boolean): List = + methodArgBinders.filter(predicate) + + protected fun getArgBinderAtIndex(index: Int): BoundedArgumentBinder { + return methodArgBinders.find { it.getIndex() == index } + ?: throw IllegalStateException("No method arg found at index $index, sqlInterface: ${declaringClass.simpleName}, method: ${method.name}") + } + + private fun SqlExecutionContext.getRowMapper(): RowMapper { + return methodRowMapperAnnotation + ?.let { resolveRowMapper(returnType, it) } + ?: resolveRowMapperFromClassAnnotation() + ?: findRowMapper(returnType) + ?: throw IllegalStateException("No rowMapper found for type: ${returnType.typeName}, in interface: ${declaringClass.simpleName}, mthod: ${method.name}") + } + + private fun SqlExecutionContext.resolveRowMapperFromClassAnnotation(): RowMapper<*>? { + return classRowMapperAnnotation.firstNotNullOfOrNull { + resolveRowMapper(returnType, it) + } + } + + companion object { + internal val argBinders = ArgumentBinders() + } +} + +private fun unwrapReturnType(method: Method): Class<*> { + if (!Flow::class.java.isAssignableFrom(method.returnType) && !Publisher::class.java.isAssignableFrom(method.returnType)) { + throw IllegalArgumentException("Expected Flow or instance of Publisher as return type") + } + + return method.genericReturnType.unwrapGenericArg() + ?: throw IllegalStateException("Unable to unwrap return type ${method.genericReturnType} for class ${method.declaringClass.simpleName}, method: ${method.name}") +} + +private fun AtomicReference.computeIfAbsent(block: () -> T): T { + return this.get() + ?: synchronized(this) { + this.get() + ?: this.updateAndGet { + block() + } + } +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlMethodHandlerFactory.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlMethodHandlerFactory.kt new file mode 100644 index 0000000..f17e61a --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlMethodHandlerFactory.kt @@ -0,0 +1,38 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.sql.annotations.UseSqlMethodHandler +import com.udaan.r2dbi.utils.findAnnotationWhichIsAnnotatedWith +import java.lang.reflect.Method +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap +import kotlin.reflect.full.primaryConstructor + +internal class SqlMethodHandlerFactory { + private val cache: ConcurrentMap = ConcurrentHashMap() + fun createHandler(sqlInterface: Class<*>, method: Method): SqlMethodHandler { + val (annotation, annotatedWith) = method.findAnnotationWhichIsAnnotatedWith() + ?: throw IllegalStateException("No annotation found using UseSqlMethodHandler for interface: ${sqlInterface.simpleName}, method: ${method.name}") + + return cache.computeIfAbsent(method) { + val sqlHandlerConstructor = annotatedWith.value.primaryConstructor + ?: throw IllegalStateException("No primary constructor found for ${annotatedWith.value.simpleName}; in interface: ${sqlInterface.simpleName}, method: ${method.name}, annotation: $annotation") + + sqlHandlerConstructor.call(method) + } + } +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlParameterCustomizer.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlParameterCustomizer.kt new file mode 100644 index 0000000..8a4049e --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlParameterCustomizer.kt @@ -0,0 +1,34 @@ +/* + * 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 com.udaan.r2dbi + +internal fun interface SqlParameterCustomizer { + /** + * parameterName is the placeholder string that are added to the sql to be substituted later + * usually, parameterName is prefixed with some prefix + * (ex: @, $, ? etc) + */ + fun getParameterName(parameter: SqlParameter): String + + /** + * argumentName is the string that are used to identify parameters that are added as placeholder in the sql + * The argumentName is usually the string after removing the prefix (as mentioned in getParameterName + * In case of Postgres R2DBC Driver, the argumentName retains the prefix and hence the need for specialisation + */ + fun getArgumentName(parameter: SqlParameter): String { + return parameter.name + } +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlQuery.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlQuery.kt new file mode 100644 index 0000000..5756969 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlQuery.kt @@ -0,0 +1,33 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.sql.annotations.UseSqlMethodHandler + +/** + * Used to indicate that a method should execute a query. + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) +@UseSqlMethodHandler(value = SqlQueryMethodHandler::class) +annotation class SqlQuery( + /** + * The query to be executed. + * + * @return the SQL string (or name) + */ + val value: String +) diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlQueryMethodHandler.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlQueryMethodHandler.kt new file mode 100644 index 0000000..3583e1a --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlQueryMethodHandler.kt @@ -0,0 +1,28 @@ +/* + * 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 com.udaan.r2dbi + +import java.lang.reflect.Method + +internal class SqlQueryMethodHandler(method: Method) : SqlMethodHandler(method) { + private val queryAnnotation: SqlQuery = method.getAnnotation(SqlQuery::class.java) + + override fun getSqlString(): String { + return queryAnnotation.value + } + + override fun returnUpdatedCount(): Boolean = false +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlUpdate.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlUpdate.kt new file mode 100644 index 0000000..a3f8711 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlUpdate.kt @@ -0,0 +1,34 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.sql.annotations.UseSqlMethodHandler + +/** + * Used to indicate that a method should execute a non-query sql statement (INSERT, DELETE, UPDATE etc). + * The method annotated with SqlUpdate, must have a Flow<Long> return type + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) +@UseSqlMethodHandler(value = SqlUpdateMethodHandler::class) +annotation class SqlUpdate( + /** + * The sql to be executed. + * + * @return the SQL string (or name) + */ + val value: String +) diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlUpdateMethodHandler.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlUpdateMethodHandler.kt new file mode 100644 index 0000000..51e80e2 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/SqlUpdateMethodHandler.kt @@ -0,0 +1,28 @@ +/* + * 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 com.udaan.r2dbi + +import java.lang.reflect.Method + +internal class SqlUpdateMethodHandler(method: Method) : SqlMethodHandler(method) { + private val updateAnnotation: SqlUpdate = method.getAnnotation(SqlUpdate::class.java) + + override fun getSqlString(): String { + return updateAnnotation.value + } + + override fun returnUpdatedCount(): Boolean = true +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/StatementContext.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/StatementContext.kt new file mode 100644 index 0000000..5a92113 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/StatementContext.kt @@ -0,0 +1,76 @@ +/* + * 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 com.udaan.r2dbi + +import io.r2dbc.spi.Result +import io.r2dbc.spi.Row +import io.r2dbc.spi.RowMetadata +import io.r2dbc.spi.Statement +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux + +class StatementContext( + private val parameterisedSql: ParameterisedSql, + private val statement: Statement +) { + + fun bind(argName: String, value: Any): StatementContext { + statement.bind(argName(argName), value) + return this + } + + fun bindNull(argName: String, clazz: Class<*>): StatementContext { + statement.bindNull(argName(argName), clazz) + return this + } + + fun bindSqlParam(binder: (sqlParam: String) -> Unit): StatementContext { + parameterisedSql.parameters.forEach { (_, param) -> + binder(param.name) + } + return this + } + + fun addNewBindings(): StatementContext { + statement.add() + return this + } + + private fun argName(name: String): String { + return parameterisedSql.arguments[name] + ?: throw IllegalArgumentException("No bind-parameter found for $name") + } + + internal fun execute(): Publisher = statement.execute() + + fun executeUpdate(): Flux { + val resultPublisher = statement.execute() + return Flux.from(resultPublisher) + .flatMap { + it.rowsUpdated + } + } + + fun executeQueryAndMap(mapper: (Row, RowMetadata) -> T): Flux { + val resultPublisher = statement.execute() + return Flux.from(resultPublisher) + .flatMap { + it.map { row, rowMetadata -> + mapper(row, rowMetadata) + } + } + } +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/Transaction.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/Transaction.kt new file mode 100644 index 0000000..03f1230 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/Transaction.kt @@ -0,0 +1,37 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.sql.annotations.UseInvocationDecorator +import io.r2dbc.spi.IsolationLevel + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +@UseInvocationDecorator(TransactionInvocationDecorator::class) +annotation class Transaction( + /** + * @return the transaction isolation level. If not specified, invoke with the default isolation level. + */ + val level: TransactionIsolationLevel = TransactionIsolationLevel.DEFAULT, +) + +enum class TransactionIsolationLevel(internal val r2dbcLevel: IsolationLevel?) { + READ_COMMITTED(IsolationLevel.READ_COMMITTED), + READ_UNCOMMITTED(IsolationLevel.READ_UNCOMMITTED), + REPEATABLE_READ(IsolationLevel.REPEATABLE_READ), + SERIALIZABLE(IsolationLevel.SERIALIZABLE), + DEFAULT(null) +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/TransactionInvocationDecorator.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/TransactionInvocationDecorator.kt new file mode 100644 index 0000000..66ff266 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/TransactionInvocationDecorator.kt @@ -0,0 +1,27 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.sql.interfaces.SqlInvocationDecorator + +internal class TransactionInvocationDecorator(private val transaction: Transaction) : SqlInvocationDecorator { + override fun decorate(invocationHandler: SqlInvocationHandler): SqlInvocationHandler { + return SqlInvocationHandler { scopedExecutor, target, args -> + val txScope = scopedExecutor.inTransaction(transaction.level) + invocationHandler.invoke(txScope, target, args) + } + } +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/binders/Bind.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/binders/Bind.kt new file mode 100644 index 0000000..674643a --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/binders/Bind.kt @@ -0,0 +1,35 @@ +/* + * 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 com.udaan.r2dbi.binders + +import com.udaan.r2dbi.sql.annotations.UseArgumentBinder + +/** + * Binds the annotated argument as a named parameter, and as a positional parameter. + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.VALUE_PARAMETER) +@UseArgumentBinder(value = BindFactory::class) +annotation class Bind( + + /** + * The name to bind the argument to. It is an error to omit the name + * when there is no parameter naming information in your class files. + * + * @return the name to which the argument will be bound. + */ + val value: String +) diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/binders/BindFactory.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/binders/BindFactory.kt new file mode 100644 index 0000000..f3052f8 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/binders/BindFactory.kt @@ -0,0 +1,86 @@ +/* + * 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 com.udaan.r2dbi.binders + +import com.udaan.r2dbi.StatementContext +import com.udaan.r2dbi.sql.interfaces.ArgumentBinder +import com.udaan.r2dbi.sql.interfaces.ArgumentBinderFactory +import com.udaan.r2dbi.sql.interfaces.MethodArg +import io.r2dbc.spi.R2dbcType +import java.lang.reflect.Method + +class BindFactory : ArgumentBinderFactory { + override fun buildForParameter( + annotation: Bind, + sqlInterface: Class<*>, + method: Method, + methodArg: MethodArg + ): ArgumentBinder { + return BindImpl(annotation, methodArg) + } +} + +class BindImpl(private val annotation: Bind, private val methodArg: MethodArg) : ArgumentBinder { + override fun bindArg(statementContext: StatementContext, value: Any?) { + val argName = annotation.value + value?.let { argValue: Any -> + //TODO: This is a bad way of writing code; need to fix it by breaking it into an abstraction that + // supports binding to different types extensible. See example from JDBI -> Arguments.java + /** + * public Arguments(final ConfigRegistry registry) { + * factories = new CopyOnWriteArrayList<>(); + * this.registry = registry; + * + * // register built-in factories, priority of factories is by reverse registration order + * + * // the null factory must be interrogated last to preserve types! + * register(new UntypedNullArgumentFactory()); + * + * register(new PrimitivesArgumentFactory()); + * register(new BoxedArgumentFactory()); + * register(new SqlArgumentFactory()); + * register(new InternetArgumentFactory()); + * register(new SqlTimeArgumentFactory()); + * register(new JavaTimeArgumentFactory()); + * register(new SqlArrayArgumentFactory()); + * register(new CharSequenceArgumentFactory()); // register before EssentialsArgumentFactory which handles String + * register(new EssentialsArgumentFactory()); + * register(new JavaTimeZoneIdArgumentFactory()); + * register(new NVarcharArgumentFactory()); + * register(new EnumArgumentFactory()); + * register(new OptionalArgumentFactory()); + * register(new DirectArgumentFactory()); + * } + */ + + R2dbcType.values().find { r2DbcType -> + r2DbcType.javaType.isAssignableFrom(argValue.javaClass) + }?.let { + statementContext.bind(argName, argValue) + } ?: let { + if (argValue is Enum<*>) { + statementContext.bind(argName, argValue.name) + } else { + null + } + } ?: throw IllegalArgumentException("Cannot bind value type: ${value.javaClass.name} as it is not supported by ${annotation.javaClass.name} annotation") + + } ?: statementContext.bindNull(argName, methodArg.type) + } +} + + + diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/binders/BindPojo.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/binders/BindPojo.kt new file mode 100644 index 0000000..cdfea51 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/binders/BindPojo.kt @@ -0,0 +1,27 @@ +/* + * 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 com.udaan.r2dbi.binders + +import com.udaan.r2dbi.sql.annotations.UseArgumentBinder + +/** + * Binds the properties of an object to a SQL statement. + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.VALUE_PARAMETER) +@UseArgumentBinder(value = BindPojoFactory::class) +annotation class BindPojo + diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/binders/BindPojoFactory.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/binders/BindPojoFactory.kt new file mode 100644 index 0000000..ac43ef8 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/binders/BindPojoFactory.kt @@ -0,0 +1,55 @@ +/* + * 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 com.udaan.r2dbi.binders + +import com.udaan.r2dbi.StatementContext +import com.udaan.r2dbi.sql.interfaces.ArgumentBinder +import com.udaan.r2dbi.sql.interfaces.ArgumentBinderFactory +import com.udaan.r2dbi.sql.interfaces.MethodArg +import com.udaan.r2dbi.utils.toClass +import java.lang.reflect.Method +import kotlin.reflect.full.memberProperties + +class BindPojoFactory : ArgumentBinderFactory { + override fun buildForParameter( + annotation: BindPojo, + sqlInterface: Class<*>, + method: Method, + methodArg: MethodArg + ): ArgumentBinder { + return BindPojoImpl() + } +} + +class BindPojoImpl : ArgumentBinder { + override fun bindArg(statementContext: StatementContext, value: Any?) { + value ?: throw IllegalArgumentException("value cannot be null for BindPojo") + + val javaClass = value.javaClass + val properties = javaClass.kotlin.memberProperties + + statementContext.bindSqlParam { sqlParam -> + val property = properties.find { it.name == sqlParam } ?: return@bindSqlParam + + val propValue = property.getter.call(value) + val propType = toClass(property.returnType) + ?: throw IllegalArgumentException("Unable to determine class of parameter ${property.name} of pojo: ${javaClass.name}") + propValue?.let { + statementContext.bind(sqlParam, it) + } ?: statementContext.bindNull(sqlParam, propType) + } + } +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/ColumnName.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/ColumnName.kt new file mode 100644 index 0000000..ddd4108 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/ColumnName.kt @@ -0,0 +1,25 @@ +/* + * 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 com.udaan.r2dbi.mappers + +/** + * Specify the mapping name for a property or parameter explicitly. This annotation is respected + * by PojoMapper (RowMapper) to map column names in Result to fields of a class + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) +annotation class ColumnName(val value: String) + diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/ColumnNameMatcher.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/ColumnNameMatcher.kt new file mode 100644 index 0000000..b4e0188 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/ColumnNameMatcher.kt @@ -0,0 +1,52 @@ +/* + * 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 com.udaan.r2dbi.mappers + + +/* TODO:shashwat + need to implement ColumnNameMatcher that can detect if the + ColumnName is compatible with the Class's field name basis established + variable / column naming conventions (eg: snake_case or CamelCase) + This will reduce the need for annotating field names with @ColumnName annotation +*/ +private interface ColumnNameMatcher { + /** + * Returns whether the column name fits the given Java identifier name. + * + * @param columnName the SQL column name + * @param fieldName the Java property, field, or parameter name + * @return whether the given names are logically equivalent + */ + fun columnNameMatches(columnName: String?, fieldName: String?): Boolean + +// /** +// * Return whether the column name starts with the given prefix, according to the matching strategy of this +// * `ColumnNameMatcher`. This method is used by reflective mappers to short-circuit nested mapping when no +// * column names begin with the nested prefix. +// * +// * By default, this method returns `columnName.startWith(prefix)`. Third party implementations should override +// * this method to match prefixes by the same criteria as [.columnNameMatches]. +// * +// * @param columnName the column name to test +// * @param prefix the prefix to test for +// * @return whether the column name begins with the prefix. +// * @since 3.5.0 +// */ +// fun columnNameStartsWith(columnName: String, prefix: String?): Boolean { +// return columnName.startsWith(prefix!!) +// } +} + diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/EnumMapper.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/EnumMapper.kt new file mode 100644 index 0000000..b7621af --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/EnumMapper.kt @@ -0,0 +1,63 @@ +/* + * 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 com.udaan.r2dbi.mappers + +import com.udaan.r2dbi.SqlExecutionContext +import com.udaan.r2dbi.sql.interfaces.ColumnGetter +import com.udaan.r2dbi.sql.interfaces.ColumnMapper +import com.udaan.r2dbi.sql.interfaces.ColumnMapperFactory +import io.r2dbc.spi.Row +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap + +class EnumMapper(private val enumClass: Class<*>) : ColumnMapper?> { + private val enumsCache: ConcurrentMap> = ConcurrentHashMap() + override fun map(row: Row, columnIndex: Int, sqlExecutionContext: SqlExecutionContext): Enum<*>? { + return map(row, sqlExecutionContext) { + row.get(columnIndex, String::class.java) + } + } + + override fun map(row: Row, columnName: String, sqlExecutionContext: SqlExecutionContext): Enum<*>? { + return map(row, sqlExecutionContext) { + row.get(columnName, String::class.java) + } + } + + private fun map(row: Row, sqlExecutionContext: SqlExecutionContext, columnGetter: ColumnGetter): Enum<*>? { + val value = columnGetter.get(row) + ?: return null + + @Suppress("UNCHECKED_CAST") + val enumConstants = enumClass.enumConstants as Array> + return enumsCache.computeIfAbsent(value) { enumValue -> + enumConstants.firstOrNull { it.name == enumValue } + } + } +} + + +class EnumMapperFactory : ColumnMapperFactory { + override fun build(type: Class<*>): ColumnMapper<*>? { + return if (type.isEnum) { + EnumMapper(type) + } else { + null + } + + } +} + diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/Nested.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/Nested.kt new file mode 100644 index 0000000..bbf006d --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/Nested.kt @@ -0,0 +1,21 @@ +/* + * 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 com.udaan.r2dbi.mappers + + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) +annotation class Nested diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/PojoMapper.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/PojoMapper.kt new file mode 100644 index 0000000..6d6dda6 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/PojoMapper.kt @@ -0,0 +1,263 @@ +/* + * 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 com.udaan.r2dbi.mappers + +import com.udaan.r2dbi.SqlExecutionContext +import com.udaan.r2dbi.sql.interfaces.RowMapper +import com.udaan.r2dbi.sql.interfaces.RowMapperFactory +import com.udaan.r2dbi.utils.* +import io.r2dbc.spi.ColumnMetadata +import io.r2dbc.spi.Row +import java.util.concurrent.ConcurrentHashMap +import kotlin.reflect.KMutableProperty1 +import kotlin.reflect.KParameter +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.memberProperties +import kotlin.reflect.jvm.javaField + +/** + * A very basic Pojo mapper that can construct a class with a default constructor or mutable members + * NOTE: At present, it does not support nested objects + */ +class PojoMapper(private val clazz: Class<*>) : RowMapper { + private val kClass = clazz.kotlin + private val constructor = findConstructor(kClass) + private val constructorMapper = getBoundedConstructorParameterMapper() + private val memberMapper = getBoundedFieldMapper() + + override fun map(row: Row, sqlExecutionContext: SqlExecutionContext): Any? { + val instance = constructorMapper.map(row, sqlExecutionContext) + ?: return null + + memberMapper.map(instance, row, sqlExecutionContext) + return instance + } + + private inner class BoundedConstructorMapper(columnProperties: Map) : + BoundedMapperBase(clazz, columnProperties) { + + fun map(row: Row, sqlExecutionContext: SqlExecutionContext): Any? { + val (paramValues, mandatoryMissingParams) = mapPropToValue(row, sqlExecutionContext) + val args = paramValues.toMap() + + if (args.isNotEmpty() && mandatoryMissingParams.isNotEmpty()) { + // It seems that we found values for only a few of the constructor parameters. + // Given that we cannot construct the object, it is better to throw an exception here + val missingParams = mandatoryMissingParams.joinToString { it.second.resolvedName } + throw IllegalStateException("No value found for mandatory param: $missingParams in mappedType: ${clazz.name}") + } + + if (args.isEmpty() && mandatoryMissingParams.isNotEmpty()) { + // It seems that we did not find any value for constructor parameters + // that will enable us to construct the object. + // But the constructor seems to have mandatory parameters. + // So we can only return `null` from here. + // The other alternative is to throw exception, which does not sound wise. + return null + } + + return constructor.callBy(args) + } + } + + private inner class BoundedFieldMapper(columnProperties: Map, ColumnProperty>) : + BoundedMapperBase>(clazz, columnProperties) { + + fun map(instance: Any, row: Row, sqlExecutionContext: SqlExecutionContext) { + val (paramValues, _) = mapPropToValue(row, sqlExecutionContext) + + paramValues.forEach { (prop, value) -> + prop.setter.call(instance, value) + } + } + } + + private fun getBoundedConstructorParameterMapper(): BoundedConstructorMapper { + val columnProperties: Map = constructor.parameters.associateWith { parameter -> + ColumnProperty.of(parameter) + } + + return BoundedConstructorMapper(columnProperties) + } + + private fun getBoundedFieldMapper(): BoundedFieldMapper { + val memberProperties: List> = kClass.memberProperties + .mapNotNull { it as? KMutableProperty1<*, *> } + .filter { property -> + !constructor.parameters.any { parameter -> parameter.name() == property.name() } + } + + val columnProperties: Map, ColumnProperty> = memberProperties.associateWith { + ColumnProperty.of(it) + } + + return BoundedFieldMapper(columnProperties) + } +} + +private interface ColumnProperty { + val resolvedName: String + val type: Class<*> + val isOptional: Boolean + val isNullable: Boolean + val isNested: Boolean + + companion object { + fun of(param: KParameter): ColumnProperty { + return object : ColumnProperty { + override val resolvedName = param.getName() + + override val type: Class<*> = param.getType() + + override val isOptional: Boolean = param.isOptional + + override val isNullable: Boolean = param.type.isMarkedNullable + + override val isNested: Boolean = param.findAnnotation() != null + } + } + + fun of(param: KMutableProperty1<*, *>): ColumnProperty { + return object : ColumnProperty { + override val resolvedName: String = param.name() + + override val type: Class<*> = param.getType() + + override val isOptional: Boolean = !param.isLateinit + + override val isNullable: Boolean = param.returnType.isMarkedNullable + + override val isNested: Boolean = ( + param.findAnnotation() + ?: param.javaField?.findAnnotation() + ?: param.setter.findAnnotation() + ) != null + } + } + + } +} + +abstract class PojoMapperFactory : RowMapperFactory { + override fun build(type: Class<*>): RowMapper<*>? { + return if (isSupportedType(type)) { + PojoMapper(type) + } else { + null + } + } + + abstract fun isSupportedType(type: Class<*>): Boolean +} + +private abstract class BoundedMapperBase( + private val mappedType: Class<*>, + private val columnProperties: Map +) { + private val columnPropertiesSeq = columnProperties.asSequence() + private val memberCache = ConcurrentHashMap>() + private val hasMandatoryParams = columnProperties.values.any { !it.isNullable && !it.isOptional } + protected fun hasMandatoryParams() = hasMandatoryParams + protected fun hasParams() = columnProperties.isNotEmpty() + + protected fun mapPropToValue(row: Row, sqlExecutionContext: SqlExecutionContext): BoundedMapResponse { + val columnMetadataList = row.metadata.columnMetadatas + val mandatoryMissingParams = mutableListOf>() + + val values = columnPropertiesSeq + //NOTE: shashwat, the same mapper may be used for different kinds of queries, + // and hence it is required to check if the column is present in the row in every map-attempt + .mapNotNull { (prop, columnProperty) -> + if (columnProperty.isNested) { + val mapper = memberCache.computeIfAbsent(prop) { + getRowMapperFor(sqlExecutionContext, columnProperty) + } + val value = mapper.map(row, sqlExecutionContext) + prop to value + } else if (columnMetadataList.isColumPropertyPresentInMetaData(columnProperty)) { + val mapper = memberCache.computeIfAbsent(prop) { + getColumnMapperFor(sqlExecutionContext, columnProperty) + } + val value = mapper.map(row, sqlExecutionContext) + prop to value + } else if (columnProperty.isNullable) { + prop to null + } else { + if (!columnProperty.isOptional) { + mandatoryMissingParams.add(prop to columnProperty) + } + null + } + } + + return BoundedMapResponse(values, mandatoryMissingParams) + } + + private fun List.isColumPropertyPresentInMetaData( + columnProperty: ColumnProperty + ): Boolean { + val resolvedName = columnProperty.resolvedName + val columnMetadata = this.find { + //NOTE: PostgreSQL driver returns column names in lower case and hence we need to + // ignore case when comparing columnName in columnMetadata with property's resolvedName + it.name.equals(resolvedName, ignoreCase = true) + } + return columnMetadata != null + } + + private fun getColumnMapperFor( + sqlExecutionContext: SqlExecutionContext, + columnProperty: ColumnProperty, + ): NullCheckingRowMapper<*> { + val name = columnProperty.resolvedName + val type = columnProperty.type + val columnMapper = sqlExecutionContext.findColumnMapper(type) + ?: throw IllegalArgumentException("No column mapper found for param: $name, type: ${type.name}, mappedType: ${mappedType.name}") + val mapper = SingleColumnMapper.mapperOf(columnMapper, name) + return NullCheckingRowMapper(mappedType, columnProperty, mapper) + } + + private fun getRowMapperFor( + sqlExecutionContext: SqlExecutionContext, + columnProperty: ColumnProperty, + ): NullCheckingRowMapper<*> { + val name = columnProperty.resolvedName + val type = columnProperty.type + val mapper = sqlExecutionContext.findRowMapper(type) + ?: throw IllegalArgumentException("No row mapper found for param: $name, type: ${type.name}, mappedType: ${mappedType.name}") + return NullCheckingRowMapper(mappedType, columnProperty, mapper) + } +} + +private data class BoundedMapResponse( + val paramValues: Sequence>, + val mandatoryMissingParams: List> +) + +private class NullCheckingRowMapper( + private val mappedType: Class<*>, + private val columnProperty: ColumnProperty, + private val rowMapper: RowMapper +) : RowMapper { + override fun map(row: Row, sqlExecutionContext: SqlExecutionContext): T { + val value = rowMapper.map(row, sqlExecutionContext) + if (value == null && !columnProperty.isNullable) { + throw IllegalStateException("Null value found for non-nullable param: ${columnProperty.resolvedName} in mappedType: ${mappedType.name}") + } + + return value + } +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/R2DbcNativeTypeMapper.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/R2DbcNativeTypeMapper.kt new file mode 100644 index 0000000..26d894e --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/R2DbcNativeTypeMapper.kt @@ -0,0 +1,73 @@ +/* + * 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 com.udaan.r2dbi.mappers + +import com.udaan.r2dbi.SqlExecutionContext +import com.udaan.r2dbi.sql.interfaces.ColumnMapper +import com.udaan.r2dbi.sql.interfaces.ColumnMapperFactory +import com.udaan.r2dbi.utils.wrapPrimitiveType +import io.r2dbc.spi.R2dbcType +import io.r2dbc.spi.Row +import java.util.concurrent.ConcurrentHashMap + +internal class R2DbcNativeTypeMapper(private val clazz: Class<*>) : ColumnMapper { + override fun map(row: Row, columnIndex: Int, sqlExecutionContext: SqlExecutionContext): Any? { + val value = row.get(columnIndex) + return value?.let { + castType(it, clazz) + } + } + + override fun map(row: Row, columnName: String, sqlExecutionContext: SqlExecutionContext): Any? { + val value = row.get(columnName) + return value?.let { + castType(it, clazz) + } + } +} + +internal class R2DbcNativeTypeMapperFactory : ColumnMapperFactory { + private val cache = ConcurrentHashMap, R2DbcNativeTypeMapper>() + override fun build(type: Class<*>): ColumnMapper<*>? { + val boxedType = wrapPrimitiveType(type) + val isR2DbcNativeType = R2dbcType.values().any { + it.javaType.isAssignableFrom(boxedType) + } + return if (isR2DbcNativeType) { + cache.computeIfAbsent(boxedType) { + R2DbcNativeTypeMapper(it) + } + } else { + null + } + } +} + +private fun castType(value: Any, clazz: Class<*>): Any { + return if (value is Number) { + when(clazz) { + java.lang.Long::class.java -> value.toLong() + java.lang.Short::class.java -> value.toShort() + java.lang.Integer::class.java -> value.toInt() + java.lang.Byte::class.java -> value.toByte() + java.lang.Double::class.java -> value.toDouble() + java.lang.Float::class.java -> value.toFloat() + else -> clazz.cast(value) + } + } else { + clazz.cast(value) + } +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/SingleColumnMapper.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/SingleColumnMapper.kt new file mode 100644 index 0000000..f58b195 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/mappers/SingleColumnMapper.kt @@ -0,0 +1,53 @@ +/* + * 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 com.udaan.r2dbi.mappers + +import com.udaan.r2dbi.SqlExecutionContext +import com.udaan.r2dbi.sql.interfaces.ColumnMapper +import com.udaan.r2dbi.sql.interfaces.RowMapper +import io.r2dbc.spi.Row + +class SingleColumnMapperByIndex internal constructor(private val columnIndex: Int, + private val delegate: ColumnMapper) : + RowMapper { + override fun map(row: Row, sqlExecutionContext: SqlExecutionContext): T { + return delegate.map(row, columnIndex, sqlExecutionContext) + } +} + +class SingleColumnMapperByName internal constructor(private val name: String, + private val delegate: ColumnMapper) : + RowMapper { + override fun map(row: Row, sqlExecutionContext: SqlExecutionContext): T { + return delegate.map(row, name, sqlExecutionContext) + } +} + +class SingleColumnMapper private constructor() { + companion object { + fun mapperOf(delegate: ColumnMapper): RowMapper { + return SingleColumnMapperByIndex(0, delegate) + } + + fun mapperOf(delegate: ColumnMapper, atIndex: Int): RowMapper { + return SingleColumnMapperByIndex(atIndex, delegate) + } + + fun mapperOf(delegate: ColumnMapper, name: String): RowMapper { + return SingleColumnMapperByName(name, delegate) + } + } +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/annotations/UseArgumentBinder.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/annotations/UseArgumentBinder.kt new file mode 100644 index 0000000..63c8d07 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/annotations/UseArgumentBinder.kt @@ -0,0 +1,28 @@ +/* + * 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 com.udaan.r2dbi.sql.annotations + +import com.udaan.r2dbi.sql.interfaces.ArgumentBinderFactory +import kotlin.reflect.KClass + +/** + * To be used over Bind + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.ANNOTATION_CLASS) +annotation class UseArgumentBinder( + val value: KClass> +) diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/annotations/UseInvocationDecorator.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/annotations/UseInvocationDecorator.kt new file mode 100644 index 0000000..aa9d26a --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/annotations/UseInvocationDecorator.kt @@ -0,0 +1,35 @@ +/* + * 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 com.udaan.r2dbi.sql.annotations + +import com.udaan.r2dbi.sql.interfaces.SqlInvocationDecorator +import kotlin.reflect.KClass + +/** + * Meta-Annotation used to decorate a SqlInvocationDecorator. Use this to annotate an annotation. + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.ANNOTATION_CLASS) +internal annotation class UseInvocationDecorator( + /** + * [SqlInvocationDecorator] annotation that decorates a SqlInvocationHandler. + *

+ * The SqlInvocationDecorator must have a `(annotation: Annotation)` constructor. + * + * @return the [SqlInvocationDecorator] class + */ + val value: KClass +) diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/annotations/UseRowMapperFactory.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/annotations/UseRowMapperFactory.kt new file mode 100644 index 0000000..1217d77 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/annotations/UseRowMapperFactory.kt @@ -0,0 +1,29 @@ +/* + * 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 com.udaan.r2dbi.sql.annotations + +import com.udaan.r2dbi.sql.interfaces.RowMapperFactory +import kotlin.reflect.KClass + +/** + * Registers a row mapper factory in the scope of a SQL Object type or method. + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +@Repeatable +annotation class UseRowMapperFactory( + val value: KClass +) diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/annotations/UseSqlMethodHandler.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/annotations/UseSqlMethodHandler.kt new file mode 100644 index 0000000..537e639 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/annotations/UseSqlMethodHandler.kt @@ -0,0 +1,35 @@ +/* + * 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 com.udaan.r2dbi.sql.annotations + +import com.udaan.r2dbi.SqlMethodHandler +import kotlin.reflect.KClass + +/** + * Meta-Annotation used to map a method to a SqlMethodHandler. Use this to annotate an annotation. + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.ANNOTATION_CLASS) +internal annotation class UseSqlMethodHandler( + /** + * [SqlMethodHandler] annotation that creates the method handler for the decorated method. + *

+ * The SqlMethodHandler must have a `(Class sqlInterface, Method method)` constructor. + * + * @return the [SqlMethodHandler] class + */ + val value: KClass +) diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/ArgumentBinder.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/ArgumentBinder.kt new file mode 100644 index 0000000..df27088 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/ArgumentBinder.kt @@ -0,0 +1,46 @@ +/* + * 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 com.udaan.r2dbi.sql.interfaces + +import com.udaan.r2dbi.StatementContext +import java.lang.reflect.Method +import java.lang.reflect.Type + +fun interface ArgumentBinder { + fun bindArg(statementContext: StatementContext, value: ArgType) +} + +interface ArgumentBinderFactory { + /** + * Supplies a argumentbinder mapper which will map method arguments to the statement + * + * @param annotation the target Annotation to bind to + * @return an argument mapper instance. + */ + fun buildForParameter( + annotation: T, + sqlInterface: Class<*>, + method: Method, + methodArg: MethodArg + ): ArgumentBinder +} + +data class MethodArg( + val name: String, + val type: Class<*>, + val parameterizedType: Type, + val index: Int +) diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/ColumnMapper.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/ColumnMapper.kt new file mode 100644 index 0000000..ed08793 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/ColumnMapper.kt @@ -0,0 +1,40 @@ +/* + * 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 com.udaan.r2dbi.sql.interfaces + +import com.udaan.r2dbi.SqlExecutionContext +import io.r2dbc.spi.Row + +interface ColumnMapper { + fun map(row: Row, columnIndex: Int, sqlExecutionContext: SqlExecutionContext): T + + fun map(row: Row, columnName: String, sqlExecutionContext: SqlExecutionContext): T +} + +interface ColumnMapperFactory { + /** + * Supplies a column mapper which will map a column in a row to a type if the factory supports it; null otherwise. + * + * @param type the target type to map to + * @return a column mapper for the given type if this factory supports it; null otherwise. + */ + fun build(type: Class<*>): ColumnMapper<*>? +} + + +fun interface ColumnGetter { + fun get(row: Row): T? +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/RowMapper.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/RowMapper.kt new file mode 100644 index 0000000..ca6402a --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/RowMapper.kt @@ -0,0 +1,33 @@ +/* + * 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 com.udaan.r2dbi.sql.interfaces + +import com.udaan.r2dbi.SqlExecutionContext +import io.r2dbc.spi.Row + +fun interface RowMapper { + fun map(row: Row, sqlExecutionContext: SqlExecutionContext): T +} + +interface RowMapperFactory { + /** + * Supplies a row mapper which will map result set rows to type if the factory supports it; null otherwise. + * + * @param type the target type to map to + * @return a row mapper for the given type if this factory supports it; null otherwise. + */ + fun build(type: Class<*>): RowMapper<*>? +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/ScopedExecutor.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/ScopedExecutor.kt new file mode 100644 index 0000000..c6bc2d1 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/ScopedExecutor.kt @@ -0,0 +1,79 @@ +/* + * 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 com.udaan.r2dbi.sql.interfaces + +import com.udaan.r2dbi.SqlExecutionContext +import com.udaan.r2dbi.TransactionIsolationLevel +import org.reactivestreams.Publisher +import java.util.function.Supplier + +internal interface ScopedExecutor { + fun use(fn: SqlExecutionCallback): Publisher + + fun inTransaction(isolationLevel: TransactionIsolationLevel): ScopedExecutor { + return compose { + it.inTransaction(isolationLevel) + } + } + + fun compose(innerScopeProvider: (SqlExecutionContext) -> ScopedExecutor): ScopedExecutor { + return ComposedScopedExecutor(this, innerScopeProvider) + } + + fun thisSupplier(): Supplier = ConstantScopedExecutorSupplier.of(this) +} + +private class ComposedScopedExecutor( + private val outerScopeExecutor: ScopedExecutor, + private val innerScopeProvider: (SqlExecutionContext) -> ScopedExecutor +) : ScopedExecutor { + override fun use(fn: SqlExecutionCallback): Publisher { + return outerScopeExecutor.use { + innerScopeProvider(it).use(fn) + } + } +} + +private class ConstantScopedExecutorSupplier private constructor(private val scopedExecutor: ScopedExecutor) : + Supplier { + override fun get(): ScopedExecutor { + return scopedExecutor + } + + companion object { + fun of(scopedExecutor: ScopedExecutor): ConstantScopedExecutorSupplier { + return ConstantScopedExecutorSupplier(scopedExecutor) + } + } +} + +//private class SuspendableScopedExecutorAdapter private constructor(private val scopedExecutor: ScopedExecutor) : +// SuspendableScopedExecutor { +// override fun use(fn: suspend (SqlExecutionContext) -> T): Flow { +// return scopedExecutor.use { +// flow { +// fn(it) +// }.asPublisher() +// }.asFlow() +// } +// +// companion object { +// fun of(scopedExecutor: ScopedExecutor): SuspendableScopedExecutor { +// return SuspendableScopedExecutorAdapter(scopedExecutor) +// } +// } +//} + diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/SqlExecutionCallback.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/SqlExecutionCallback.kt new file mode 100644 index 0000000..d318070 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/SqlExecutionCallback.kt @@ -0,0 +1,23 @@ +/* + * 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 com.udaan.r2dbi.sql.interfaces + +import com.udaan.r2dbi.SqlExecutionContext +import org.reactivestreams.Publisher + +fun interface SqlExecutionCallback { + fun withContext(sqlExecutionContext: SqlExecutionContext): Publisher +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/SqlInvocationDecorator.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/SqlInvocationDecorator.kt new file mode 100644 index 0000000..af31907 --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/sql/interfaces/SqlInvocationDecorator.kt @@ -0,0 +1,22 @@ +/* + * 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 com.udaan.r2dbi.sql.interfaces + +import com.udaan.r2dbi.SqlInvocationHandler + +internal fun interface SqlInvocationDecorator { + fun decorate(invocationHandler: SqlInvocationHandler): SqlInvocationHandler +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/utils/AnnotationHelpers.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/utils/AnnotationHelpers.kt new file mode 100644 index 0000000..410a66a --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/utils/AnnotationHelpers.kt @@ -0,0 +1,42 @@ +/* + * 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 com.udaan.r2dbi.utils + +import java.lang.reflect.AnnotatedElement +import kotlin.reflect.full.findAnnotation + +inline fun Annotation.findAnnotatedWith(): T? { + return annotationClass.findAnnotation() +} + +inline fun AnnotatedElement.findAnnotation(): T? { + return this.getAnnotation(T::class.java) +} + + +inline fun AnnotatedElement.findAnnotationWhichIsAnnotatedWith(): Pair? { + return findAllAnnotationsWhichIsAnnotatedWith() + .firstOrNull() +} + +inline fun AnnotatedElement.findAllAnnotationsWhichIsAnnotatedWith(): List> { + return annotations.mapNotNull { annotation -> + val annotatedWith = annotation.findAnnotatedWith() + annotatedWith?.let { + annotation to it + } + } +} diff --git a/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/utils/ReflectionUtilities.kt b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/utils/ReflectionUtilities.kt new file mode 100644 index 0000000..844288d --- /dev/null +++ b/r2dbi-core/src/main/kotlin/com/udaan/r2dbi/utils/ReflectionUtilities.kt @@ -0,0 +1,137 @@ +/* + * 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 com.udaan.r2dbi.utils + +import com.udaan.r2dbi.mappers.ColumnName +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import kotlin.reflect.* +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.javaField +import kotlin.reflect.jvm.javaType + +internal fun toClass(javaType: Type?): Class<*>? = when (javaType) { + is ParameterizedType -> javaType.rawType as Class<*> + is Class<*> -> javaType + else -> null +} + +internal fun toClass(kType: KType): Class<*>? = toClass(kType.javaType) + +internal fun isIterableOrArray(clazz: Class<*>) = Iterable::class.java.isAssignableFrom(clazz) + || java.lang.Iterable::class.java.isAssignableFrom(clazz) + || clazz.isArray + +internal fun iteratorFrom(iterable: Any): Iterator? { + val javaClass = iterable.javaClass + return if (Iterable::class.java.isAssignableFrom(javaClass)) { + (iterable as Iterable<*>).iterator() + } else if (java.lang.Iterable::class.java.isAssignableFrom(javaClass)) { + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + (iterable as java.lang.Iterable<*>).iterator() + } else if (javaClass.isArray) { + (iterable as Array<*>).iterator() + } else { + null + } +} + +internal fun Type.unwrapGenericArg(): Class<*>? { + val firstType = (this as? ParameterizedType)?.actualTypeArguments?.first() + return if (firstType != null) { + toClass(firstType) + } else { + null + } +} + + +internal fun KClass<*>.simpleName(): String = this.simpleName ?: this::java.javaClass.simpleName +internal fun KParameter.name(): String? = name //findAnnotation()?.value ?: name +internal fun KParameter.getName(): String { + val annotation = this.findAnnotation() + return annotation?.value + ?: name + ?: throw IllegalArgumentException("No name found for parameter: $this") +} + +internal fun KParameter.type(): Class<*>? = toClass(this.type.javaType) +internal fun KParameter.getType(): Class<*> = toClass(this.type.javaType) + ?: throw IllegalArgumentException("Cannot handle paramType: ${this.type.javaType.typeName} for parameter: $this") + +internal fun KMutableProperty1<*, *>.name(): String { + val annotation = this.findAnnotation() + return annotation?.value + ?: this.javaField?.findAnnotation() + ?.value + ?: name +} + +internal fun KMutableProperty1<*, *>.type(): Class<*>? = + toClass(this.returnType.javaType) + +internal fun KMutableProperty1<*, *>.getType(): Class<*> = + toClass(this.returnType.javaType) + ?: throw IllegalArgumentException("Cannot handle paramType: ${this.returnType.javaType.typeName} for parameter: $this") + +internal fun findConstructor(kClass: KClass): KFunction { + return kClass.primaryConstructor + ?: if (kClass.constructors.size == 1) { + kClass.constructors.first() + } else { + throw IllegalArgumentException("Bean ${kClass.simpleName()} is not instantiable: more than one constructor found") + } +} + + +internal fun findGenericParameter(type: Type, parameterizedSupertype: Class<*>, n: Int): Type? { + if (type is Class<*>) { + val genericSuper = type.getGenericSuperclass() + if (genericSuper is ParameterizedType) { + if (genericSuper.rawType === parameterizedSupertype) { + return genericSuper.actualTypeArguments[n] + } + } + } + + return null +} + +fun wrapPrimitiveType(clazz: Class<*>): Class<*> { + return when (clazz.name) { + "boolean" -> java.lang.Boolean::class.java + "char" -> java.lang.Character::class.java + "byte" -> java.lang.Byte::class.java + "short" -> java.lang.Short::class.java + "int" -> java.lang.Integer::class.java + "float" -> java.lang.Float::class.java + "long" -> java.lang.Long::class.java + "double" -> java.lang.Double::class.java + "void" -> Void::class.java + else -> clazz + } +} + + +private const val METADATA_FQ_NAME = "kotlin.Metadata" + +/** + * Returns true if the [Class][java.lang.Class] object represents a Kotlin class. + * + * @return True if this is a Kotlin class. + */ +fun Class<*>.isKotlinClass() = this.annotations.singleOrNull { it.annotationClass.java.name == METADATA_FQ_NAME } != null diff --git a/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/BaseTestClass.kt b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/BaseTestClass.kt new file mode 100644 index 0000000..9ba78cb --- /dev/null +++ b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/BaseTestClass.kt @@ -0,0 +1,60 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.testDao.TestUpdateDao +import kotlinx.coroutines.flow.count +import kotlinx.coroutines.flow.reduce +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.TestInstance +import reactor.test.StepVerifier +import kotlin.test.assertEquals + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +abstract class BaseTestClass(private val extension: R2DbiTestExtensionBase) { + @BeforeAll + fun setUp() { + extension.beforeAll() + + doInsert() + } + + @AfterAll + fun tearDown() { + extension.afterAll() + } + + private fun doInsert() = runBlocking { + val updateDao = extension.getR2dbi().onDemand(TestUpdateDao::class.java) + + val flow = updateDao.insertSingleValue(config1.name, config1.value) + assertEquals(1, flow.count(), "Expected 1 row to be updated") + + val flow2 = updateDao.insertSingle(config2) + StepVerifier.create(flow2) + .expectNext(1) + .verifyComplete() + + + val flow3 = updateDao.insertMultiple(listOf(config3, config4, config5)) + assertEquals(3, flow3.reduce { acc, value -> acc + value }, "Expected 3 inserts") + + val flow4 = updateDao.insertSingleInTransaction(config6) + assertEquals(1, flow4.count(), "Expected 1 row to be updated") + } +} diff --git a/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/MssqlTests.kt b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/MssqlTests.kt new file mode 100644 index 0000000..16fc758 --- /dev/null +++ b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/MssqlTests.kt @@ -0,0 +1,68 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.binders.Bind +import com.udaan.r2dbi.internal.AzureSqlEdgeContainerProvider +import com.udaan.r2dbi.internal.SqlDatabaseExtension +import com.udaan.r2dbi.testDao.ConfigWithBooleanValue +import com.udaan.r2dbi.testDao.TableName +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.single +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import kotlin.test.assertTrue + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class MssqlTests : BaseTestClass(extension) { + companion object { + private val extension = SqlDatabaseExtension(AzureSqlEdgeContainerProvider()) + } + + @Nested + inner class TestDynamicInterfaces : TestDynamicInterfaceBase(extension.getR2dbi()) + + @Nested + inner class TestMssqlTests: TestMssqlTestCases(extension.getR2dbi()) + + @Nested + inner class TestSqlExecutionContext : TestSqlExecutionContextBase(extension.getR2dbi(), extension.getPool()) + + @Nested + inner class TestFluentR2Dbi : TestFluentR2DbiBase(extension.getR2dbi()) + + @Nested + inner class TestMappers : TestMappersBase(extension.getR2dbi()) + +} + +abstract class TestMssqlTestCases(private val r2dbi: R2Dbi) { + private val dao by lazy { r2dbi.onDemand(MssqlQueryDao::class.java) } + + @Test + fun `test with boolean value`() = runBlocking { + val data = dao.getConfigWithBooleanValue(config1.name) + .single() + assertTrue(data.value) + } +} + +interface MssqlQueryDao { + @SqlQuery("SELECT name, Cast(1 as bit) as value FROM $TableName where name = :name") + fun getConfigWithBooleanValue(@Bind("name") name: String): Flow +} \ No newline at end of file diff --git a/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/PostgreSQLTests.kt b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/PostgreSQLTests.kt new file mode 100644 index 0000000..993aa5b --- /dev/null +++ b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/PostgreSQLTests.kt @@ -0,0 +1,90 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.binders.Bind +import com.udaan.r2dbi.internal.SqlDatabaseContainer +import com.udaan.r2dbi.internal.SqlDatabaseContainerAdapter +import com.udaan.r2dbi.internal.SqlDatabaseContainerProvider +import com.udaan.r2dbi.internal.SqlDatabaseExtension +import com.udaan.r2dbi.testDao.ConfigWithBooleanValue +import com.udaan.r2dbi.testDao.TableName +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.single +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.testcontainers.containers.PostgreSQLContainer +import org.testcontainers.utility.DockerImageName +import kotlin.test.assertTrue + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class PostgreSQLTests : BaseTestClass(extension) { + companion object { + private val extension = SqlDatabaseExtension(PostgresSqlContainerProvider()) + } + + @Nested + inner class TestDynamicInterfaces : TestDynamicInterfaceBase(extension.getR2dbi()) + + @Nested + inner class TestSqlExecutionContext : TestSqlExecutionContextBase(extension.getR2dbi(), extension.getPool()) + + @Nested + inner class TestPostgreSQLTests : TestPostgreSQLTestCases(extension.getR2dbi()) + + @Nested + inner class TestMappers : TestMappersBase(extension.getR2dbi()) +} + +private class PostgresSqlContainerProvider : SqlDatabaseContainerProvider { + override fun newInstance(): SqlDatabaseContainer { + return newInstance("latest") + } + + private fun newInstance(tag: String?): SqlDatabaseContainer { + val taggedImageName: DockerImageName = DockerImageName.parse("postgres") + .withTag(tag ?: "latest") + val sqlContainer = PostgreSQLContainer(taggedImageName) + return object : SqlDatabaseContainerAdapter(sqlContainer) { + override fun getDriverName(): String { + return "postgresql" + } + + override fun getPort(): Int { + return sqlContainer.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT) + } + } + + } +} + +abstract class TestPostgreSQLTestCases(private val r2dbi: R2Dbi) { + private val dao by lazy { r2dbi.onDemand(PostgresDao::class.java) } + + @Test + fun `test with boolean value`() = runBlocking { + val data = dao.getConfigWithBooleanValue(config1.name) + .single() + assertTrue(data.value) + } +} + +interface PostgresDao { + @SqlQuery("SELECT name, true as value FROM $TableName where name = :name") + fun getConfigWithBooleanValue(@Bind("name") name: String): Flow +} \ No newline at end of file diff --git a/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/R2DbiTestExtensionBase.kt b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/R2DbiTestExtensionBase.kt new file mode 100644 index 0000000..0af1812 --- /dev/null +++ b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/R2DbiTestExtensionBase.kt @@ -0,0 +1,103 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.testDao.TableName +import io.r2dbc.mssql.MssqlConnectionFactoryProvider +import io.r2dbc.pool.ConnectionPool +import io.r2dbc.pool.ConnectionPoolConfiguration +import io.r2dbc.spi.ConnectionFactories +import io.r2dbc.spi.ConnectionFactory +import io.r2dbc.spi.ConnectionFactoryOptions +import java.time.Duration + +abstract class R2DbiTestExtensionBase { + private lateinit var pool: ConnectionPool + private lateinit var r2Dbi: R2Dbi + + open fun beforeAll() { + pool = createPool() + r2Dbi = R2Dbi.withPool(pool) + + createTable() + } + + open fun afterAll() { + dropTable() + + pool.close().block() + } + + private fun createTable() { + println("Creating table - $TableName") + getR2dbi().execute { + val stmtContext = + it.createStatementContext("CREATE TABLE $TableName ( name VARCHAR(128), value VARCHAR(256))") + stmtContext.executeUpdate() + .then() + }.blockLast() + } + + private fun dropTable() { + println("Dropping table - $TableName") + getR2dbi().execute { + val stmtContext = it.createStatementContext("DROP TABLE $TableName") + stmtContext.executeUpdate() + .then() + }.blockLast() + } + + private fun createPool(): ConnectionPool { + val options: ConnectionFactoryOptions = ConnectionFactoryOptions.builder() + .option(ConnectionFactoryOptions.DRIVER, getDriver()) + .option(ConnectionFactoryOptions.HOST, getHost()) + .option(ConnectionFactoryOptions.PORT, getPort()) + .option(ConnectionFactoryOptions.USER, getUsername()) + .option(ConnectionFactoryOptions.PASSWORD, getPassword()) + .option(ConnectionFactoryOptions.DATABASE, getDatabaseName()) // optional + .option(ConnectionFactoryOptions.SSL, false) // optional, defaults to false + .option(MssqlConnectionFactoryProvider.TRUST_SERVER_CERTIFICATE, true) + + .build() + + val connectionFactory: ConnectionFactory = ConnectionFactories.get(options) + // Create a ConnectionPool for connectionFactory + val configuration: ConnectionPoolConfiguration = ConnectionPoolConfiguration.builder(connectionFactory) + .maxIdleTime(Duration.ofMillis(1000)) + .maxSize(1) + .build() + + return ConnectionPool(configuration) + } + + fun getR2dbi(): R2Dbi { + return r2Dbi + } + + fun getPool(): ConnectionPool { + return pool + } + + protected abstract fun getHost(): String + protected abstract fun getPassword(): String + protected abstract fun getPort(): Int + protected abstract fun getUsername(): String + + protected abstract fun getDatabaseName(): String + + protected abstract fun getDriver(): String + +} diff --git a/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestDynamicInterfaceBase.kt b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestDynamicInterfaceBase.kt new file mode 100644 index 0000000..97500eb --- /dev/null +++ b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestDynamicInterfaceBase.kt @@ -0,0 +1,332 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.testDao.* +import kotlinx.coroutines.flow.count +import kotlinx.coroutines.flow.single +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.test.StepVerifier +import reactor.test.StepVerifierOptions +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +abstract class TestDynamicInterfaceBase(private val r2dbi: R2Dbi) { + + private val dao: TestQueryDao by lazy { r2dbi.onDemand(TestQueryDao::class.java) } + + @Test + fun `test simple query`() = runBlocking { + val singleValue = dao.getOne().single() + assertEquals(1, singleValue) + } + + @Test + fun `test querying empty results`() = runBlocking { + dao.getConfigByName("some random key name").collect { + Assertions.assertNull(it, "Expected empty response") + } + } + + @Test + fun `test transaction failure - with txn failure - scenario 1`() = runBlocking { + val (beforeTx, inTx, afterTx) = transactionScenario().blockLast()!! + + // Note: DO NOT Change the order; inTx emits error() and will stop further publishes and prevent + // afterTx from publishing any result, causing the insert to never happen + val orderedExecution = beforeTx.concatWith(inTx).concatWith(afterTx) + + StepVerifier.create(orderedExecution) + .expectNext(1) + .expectError(IllegalStateException::class.java) + .verify() + + dao.getConfigByNameAsPublisher(failureKey).let { + StepVerifier.create(it, StepVerifierOptions.create().scenarioName("Transaction that failed")) + }.verifyComplete() + + dao.getConfigByNameAsPublisher(successKeyBeforeTx).let { + StepVerifier.create(it, StepVerifierOptions.create().scenarioName("Before Transaction")) + }.expectNextCount(1) + .verifyComplete() + + dao.getConfigByNameAsPublisher(successKeyAfterTx).let { + StepVerifier.create(it, StepVerifierOptions.create().scenarioName("After Transaction")) + } + .verifyComplete() + + r2dbi.execute { + it.createStatementContext("delete from $TableName where name in ('$successKeyBeforeTx', '$successKeyAfterTx')") + .executeUpdate() + }.blockLast() + + Unit + } + + @Test + fun `test transaction failure - with txn failure - scenario 2`() = runBlocking { + val (beforeTx, inTx, afterTx) = transactionScenario().blockLast()!! + + // Note: DO NOT Change the order; inTx emits error() and will stop further publishes. + // In this scenario, we are testing if we wait for afterTx first and then execute inTx + // The error from inTx MUST NOT prevent beforeTx and afterTx to execute + val orderedExecution = beforeTx + .concatWith(afterTx) + .concatWith(inTx) + + // NOTE that we are expecting beforeTx and afterTx - both to succeed + StepVerifier.create(orderedExecution) + .expectNext(1) + .expectNext(1) + .expectError(IllegalStateException::class.java) + .verify() + + dao.getConfigByNameAsPublisher(failureKey).let { + StepVerifier.create(it, StepVerifierOptions.create().scenarioName("Transaction that failed")) + }.verifyComplete() + + dao.getConfigByNameAsPublisher(successKeyBeforeTx).let { + StepVerifier.create(it, StepVerifierOptions.create().scenarioName("Before Transaction")) + }.expectNextCount(1) + .verifyComplete() + + dao.getConfigByNameAsPublisher(successKeyAfterTx).let { + StepVerifier.create(it, StepVerifierOptions.create().scenarioName("After Transaction")) + }.expectNextCount(1) + .verifyComplete() + + r2dbi.execute { + it.createStatementContext("delete from $TableName where name in ('$successKeyBeforeTx', '$successKeyAfterTx')") + .executeUpdate() + }.blockLast() + + Unit + } + + @Test + fun `test dao`() = runBlocking { + val configData = dao.getConfig() + assertEquals(6, configData.count(), "Expected 6 entries") + + val configByName = dao.getConfigByName(CONFIG_NAME1) + val singleVal = configByName.single() + assertEquals(config1, singleVal) + } + + @Test + fun `test bind pojo`() = runBlocking { + val configData = dao.getConfigByPojo(config1) + val config = configData.single() + assertEquals(config1, config) + } + + @Test + fun `test batch`() = runBlocking { + val configData = dao.getBatch(listOf(CONFIG_NAME1, CONFIG_NAME2, CONFIG_NAME3), config2.value) + val list = configData.toList() + assertEquals(1, list.size) + assertEquals(config2, list.first()) + } + + @Test + fun `test batch as publisher`() = runBlocking { + val listOfNames = listOf(config1, config2, config3).map { it.name } + val configData = dao.getBatchAsPublisher(listOfNames, config2.value) + StepVerifier.create(configData) + .expectNext(config2) + .verifyComplete() + + val long: Publisher = + dao.getBatchAsPublisherAndRowsUpdated(listOfNames, config2.value) + + StepVerifier.create(long) + .expectNext(0) + .expectNext(1) + .expectNext(0) + .verifyComplete() + + Unit + } + + @Test + fun `test single`() = runBlocking { + val configData = dao.getSingleString(CONFIG_NAME1) + val list = configData.toList() + assertEquals(1, list.size) + assertEquals(config1.value, list.first()) + } + + @Test + fun `test enum bindings`() { + val configData = Flux.from(dao.getCountOfValues(TestConfigName.Key1)) + .reduce { a, v -> a + v } + .block() + + assertNotNull(configData) + assertEquals(1, configData) + } + + @Test + fun `test enum row mapping`() = runBlocking { + val configWithEnumData = dao.getConfigWithEnumValue(CONFIG_NAME1) + .single() + assertEquals(ConfigWithEnum(TestConfigName.Key1, config1.value), configWithEnumData) + } + + @Test + fun `test nullable value`() = runBlocking { + val value = dao.getConfigAndForceValueAsNull(config1.name) + .single() + assertEquals(config1.name, value.name) + assertNull(value.value) + } + + @Test + fun `test nullable value but no column`() = runBlocking { + val value = dao.getNoValueColForNonOptionalButNullableField(config1.name) + .single() + assertEquals(config1.name, value.name) + assertNull(value.value) + } + + @Test + fun `test nullable value and expect exception`() { + val exception = assertThrows { + runBlocking { + dao.getConfigAndValueAsNull(config1.name) + .single() + } + } + + assertEquals( + "Null value found for non-nullable param: value in mappedType: com.udaan.r2dbi.testDao.ConfigData", + exception.message + ) + } + + @Test + fun `test optional value`() = runBlocking { + val value = dao.getConfigNameOnlyAssumingValueIsOptional(config1.name) + .single() + assertEquals(config1.name, value.name) + assertEquals("hello", value.value) + } + + @Test + fun `test optional value and expect exception`() { + val exception = assertThrows { + runBlocking { + dao.getConfigNameOnly(config1.name) + .single() + } + } + assertEquals( + "No value found for mandatory param: value in mappedType: com.udaan.r2dbi.testDao.ConfigData", + exception.message + ) + } + + @Test + fun `test with diff property name or column name`() = runBlocking { + val data1 = dao.getConfigByNameWithDifferentFieldNames(config1.name) + .single() + assertEquals(config1.name, data1.configName) + assertEquals(config1.value, data1.configValue) + + val data2 = dao.getConfigByNameWithDifferentColumnNames(config1.name) + .single() + + assertEquals(config1.name, data2.name) + assertEquals(config1.value, data2.value) + } + + @Test + fun `test null return`() { + val e = assertThrows { + runBlocking { + dao.getNullReturn().single() + } + } + + assertEquals( + "Null value found for returnType: com.udaan.r2dbi.testDao.ConfigData when mapping result in method: getNullReturn in clazz: com.udaan.r2dbi.testDao.TestQueryDao", + e.message + ) + } + + @Test + fun `test with multiple placeholders with same name`() = runBlocking { + val config = dao.getConfigWithSameNameAndValue(config6.name).single() + assertEquals(config6.name, config.name) + assertEquals(config6.name, config.value) + } + + private fun transactionScenario() = r2dbi.execute { context -> + val updateCountBeforeTxMono: Mono = + context.createStatementContext("insert into $TableName values ('$successKeyBeforeTx', 'some value')") + .executeUpdate() + .reduce { acc, value -> acc + value } + + val txPublisher: Publisher = context.inTransaction(TransactionIsolationLevel.DEFAULT).use { + // Inside Transaction + val stmt = context.createStatementContext("insert into $TableName values ('$failureKey', 'some value')") + val publisher = stmt.executeUpdate() + .then( + Mono.error(IllegalStateException("Terminating Transaction")) + ) + publisher + } + + val updateCountAfterTxnMono: Mono = + context.createStatementContext("insert into $TableName values ('$successKeyAfterTx', 'some value')") + .executeUpdate() + .reduce { acc, value -> acc + value } + + Mono.just( + TransactionScenarioResult( + updateCountBeforeTxMono, + Mono.from(txPublisher), + updateCountAfterTxnMono + ) + ) + } +} + +internal val config1 = ConfigData(CONFIG_NAME1, "some value1") +internal val config2 = ConfigData(CONFIG_NAME2, "some value2") +internal val config3 = ConfigData(CONFIG_NAME3, "some value3") +internal val config4 = ConfigData(CONFIG_NAME4, "some value4") +internal val config5 = ConfigData(CONFIG_NAME5, "some value5") +internal val config6 = ConfigData(CONFIG_NAME6, CONFIG_NAME6) + +private const val successKeyBeforeTx = "success before Tx Key" +private const val successKeyAfterTx = "success after Tx Key" +private const val failureKey = "failure Key" + +private data class TransactionScenarioResult( + val beforeTx: Mono, + val inTx: Mono, + val afterTx: Mono +) + diff --git a/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestFluentR2DbiBase.kt b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestFluentR2DbiBase.kt new file mode 100644 index 0000000..111aaa6 --- /dev/null +++ b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestFluentR2DbiBase.kt @@ -0,0 +1,59 @@ +/* + * 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 com.udaan.r2dbi + +import kotlinx.coroutines.flow.single +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test + +@OptIn(ExperimentalAPI::class) +abstract class TestFluentR2DbiBase(private val r2dbi: R2Dbi) { + + @Test + fun testFluent() = runBlocking { + r2dbi.open() + .sql("select 1") + .execute() + .subscribeNotNull(Int::class.java) { + println(it) + assertEquals(1, it) + } + } + + @Test + fun testFluentNull() = runBlocking { + r2dbi.open() + .sql("select null") + .execute() + .subscribeRows(Int::class.java) { + println(it) + assertNull(it) + } + } + + @Test + fun testFluentWithFlow() = runBlocking { + val long = r2dbi.open() + .sql("select 1") + .execute() + .mapToNotNull(Long::class.java) + .single() + + assertEquals(1, long) + } +} diff --git a/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestGenerics.kt b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestGenerics.kt new file mode 100644 index 0000000..d96e098 --- /dev/null +++ b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestGenerics.kt @@ -0,0 +1,58 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.utils.unwrapGenericArg +import reactor.core.publisher.Flux +import java.lang.reflect.ParameterizedType + +fun main() { + val a: List = listOf(1) + println(a.javaClass.typeParameters.first()) + + val aParam = A::class.java.methods.first().parameters.first().parameterizedType + println((aParam as ParameterizedType).actualTypeArguments.first()) + println(aParam.javaClass.name) + + val bParam = B::class.java.methods.first().parameters.first().parameterizedType + println(bParam) + println(bParam.javaClass.name) + + + val cReturnType = C::class.java.methods.first().genericReturnType + println(cReturnType) + println(cReturnType.unwrapGenericArg()) + + val dReturnType = D::class.java.methods.first().genericReturnType + println(dReturnType) + println(dReturnType.unwrapGenericArg()) +} + + +interface A { + fun list(list: List) +} +interface B { + fun list(list: Int) +} + +interface C { + fun list(): Flux> +} + +interface D { + fun list(): Flux +} diff --git a/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestMappersBase.kt b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestMappersBase.kt new file mode 100644 index 0000000..3930ba7 --- /dev/null +++ b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestMappersBase.kt @@ -0,0 +1,121 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.mappers.Nested +import com.udaan.r2dbi.testDao.CONFIG_NAME1 +import com.udaan.r2dbi.testDao.TableName +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.single +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertNull + +abstract class TestMappersBase(private val r2dbi: R2Dbi) { + + private val dao by lazy { r2dbi.onDemand(TestNestedObjectDao::class.java) } + +// @Test +// fun `test column mapper ordering`() { +// TODO("Not yet implemented") +// } + + @Test + fun `test pojo mapper nesting`() = runBlocking { + val config = dao.getNestedValueConfig().single() + assertEquals(config1.name, config.name) + assertEquals(config1.value, config.v.value) + } + + @Test + fun `test pojo mapper nesting all fields`() = runBlocking { + val config = dao.getNestedConfig().single() + assertEquals(config1.name, config.n.name) + assertEquals(config1.value, config.v.value) + } + + @Test + fun `test nullable nested config with null value`() = runBlocking { + val config = dao.getNullValueInNullableNestedConfig().single() + assertEquals(config1.name, config.n.name) + assertEquals(config1.name, config.name) + assertNull(config.v) + } + + @Test + fun `test nested config with null value - expect exception`() { + val exception = assertThrows { + runBlocking { + dao.getNullValueInNestedConfig().single() + } + } + assertEquals("Null value found for non-nullable param: v in mappedType: com.udaan.r2dbi.NestedConfig", exception.message) + } +} + + +interface TestNestedObjectDao { + @SqlQuery("select * from $TableName where name = '$CONFIG_NAME1'") + fun getNestedValueConfig(): Flow + + @SqlQuery("select * from $TableName where name = '$CONFIG_NAME1'") + fun getNestedConfig(): Flow + + @SqlQuery("select name from $TableName where name = '$CONFIG_NAME1'") + fun getNullValueInNullableNestedConfig(): Flow + + @SqlQuery("select name from $TableName where name = '$CONFIG_NAME1'") + fun getNullValueInNestedConfig(): Flow +} + +data class NestedValueConfig( + val name: String +) { + @Nested + var v: NestedValue = NestedValue("") +} + +data class NestedConfig( + @Nested val n: NestedName, + + @Nested var v: NestedValue = NestedValue("") +) + +data class NullableNestedConfig( + val name: String, + + @Nested val n: NestedName, + + @Nested + val v: NestedValue? +) + +class NestedName { + var name: String = "" + override fun toString(): String { + return "NestedName(name='$name')" + } +} + +class NestedValue( + val value: String +) { + override fun toString(): String { + return "NestedValue(value='$value')" + } +} diff --git a/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestParametrisedSql.kt b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestParametrisedSql.kt new file mode 100644 index 0000000..f0a0a1e --- /dev/null +++ b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestParametrisedSql.kt @@ -0,0 +1,35 @@ +/* + * 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 com.udaan.r2dbi + +fun main() { + val sql = + ParameterisedSql("/* this is a comment with @param, :param2*/ select a, b, c::string, d * from abcd where name = ':hi' and value = :dede and check = $1 and another_comment = /* this is a comment : it :name */ : it :value ", DefCustomiser) + + println(sql.finalSql) + println(sql.parameters) +} + +private object DefCustomiser : SqlParameterCustomizer { + override fun getParameterName(parameter: SqlParameter): String { + return "@${parameter.name}" + } + + override fun getArgumentName(parameter: SqlParameter): String { + return parameter.name + } + +} \ No newline at end of file diff --git a/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestSqlExecutionContextBase.kt b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestSqlExecutionContextBase.kt new file mode 100644 index 0000000..23b0e9d --- /dev/null +++ b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/TestSqlExecutionContextBase.kt @@ -0,0 +1,54 @@ +/* + * 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 com.udaan.r2dbi + +import com.udaan.r2dbi.testDao.TestQueryDao +import io.r2dbc.pool.ConnectionPool +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.count +import kotlinx.coroutines.flow.single +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +@OptIn(ExperimentalAPI::class) +abstract class TestSqlExecutionContextBase(private val r2dbi: R2Dbi, private val pool: ConnectionPool) { + @Test + fun `test single dao attached to SqlExecutionContext`() = runBlocking { + val scopedExecutor = r2dbi.createScopedExecutor() + val dao = r2dbi.attachTo(TestQueryDao::class.java, scopedExecutor) + + val configData = dao.getConfig() + assertEquals(6, configData.count(), "Expected 6 entries") + + val configByName = dao.getConfigByName(config1.name) + val singleVal = configByName.single() + assertEquals(config1, singleVal) + } + + @Test + fun `test multiple dao attached to SqlExecutionContext`() = runBlocking { + val scopedExecutor = r2dbi.createScopedExecutor() + val dao = r2dbi.attachTo(TestQueryDao::class.java, scopedExecutor) + + val configData = dao.getConfig() + assertEquals(6, configData.count(), "Expected 6 entries") + + val configByName = dao.getConfigByName(config1.name) + val singleVal = configByName.single() + assertEquals(config1, singleVal) + } +} diff --git a/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/internal/AzureSqlEdgeContainerProvider.kt b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/internal/AzureSqlEdgeContainerProvider.kt new file mode 100644 index 0000000..e77dcf1 --- /dev/null +++ b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/internal/AzureSqlEdgeContainerProvider.kt @@ -0,0 +1,183 @@ +/* + * 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 com.udaan.r2dbi.internal + +import com.udaan.r2dbi.internal.AzureSQLEdgeServerContainer.Companion.MS_SQL_SERVER_PORT +import org.testcontainers.containers.JdbcDatabaseContainer +import org.testcontainers.containers.MSSQLServerContainer +import org.testcontainers.utility.DockerImageName +import org.testcontainers.utility.LicenseAcceptance +import java.util.* +import java.util.regex.Pattern +import java.util.stream.Stream + +/** + * Uses a [MSSQLServerContainer] with a `mcr.microsoft.com/azure-sql-edge` image (default: "latest") in place of + * the standard ` mcr.microsoft.com/mssql/server` image + */ +class AzureSqlEdgeContainerProvider : SqlDatabaseContainerProvider { +// override fun supports(databaseType: String): Boolean { +// return databaseType == NAME +// } + + override fun newInstance(): SqlDatabaseContainer { + return newInstance("latest") + } + + private fun newInstance(tag: String): SqlDatabaseContainer { + val taggedImageName: DockerImageName = DockerImageName.parse("mcr.microsoft.com/azure-sql-edge") + .withTag(tag) + .asCompatibleSubstituteFor("mcr.microsoft.com/mssql/server") + val containerInstance = AzureSQLEdgeServerContainer(taggedImageName) + .withUrlParam("trustServerCertificate", "true") + + return object: SqlDatabaseContainerAdapter(containerInstance) { + override fun getDriverName(): String { + return containerInstance.getDriverName() + } + + override fun getPort(): Int { + return containerInstance.getMappedPort(MS_SQL_SERVER_PORT) + } + } + } + + companion object { + private const val NAME = "azuresqledge" + } +} + +/** + * Testcontainers implementation for Microsoft SQL Server. + * + * + * Supported image: `mcr.microsoft.com/mssql/server` + * + * + * Exposed ports: 1433 + */ +open class AzureSQLEdgeServerContainer(dockerImageName: DockerImageName) : JdbcDatabaseContainer(dockerImageName) { + private var password: String = DEFAULT_PASSWORD + + init { + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME) + addExposedPort(MS_SQL_SERVER_PORT) + } + + override fun configure() { + // If license was not accepted programatically, check if it was accepted via resource file + if (!envMap.containsKey("ACCEPT_EULA")) { + LicenseAcceptance.assertLicenseAccepted(this.dockerImageName) + acceptLicense() + } + addEnv("MSSQL_SA_PASSWORD", password) + addEnv("MSSQL_PID", "Developer") + addEnv("MSSQL_USER", username) + } + + /** + * Accepts the license for the SQLServer container by setting the ACCEPT_EULA=Y + * variable as described at [https://hub.docker.com/_/microsoft-mssql-server](https://hub.docker.com/_/microsoft-mssql-server) + */ + open fun acceptLicense(): AzureSQLEdgeServerContainer? { + addEnv("ACCEPT_EULA", "1") + return self() + } + + override fun getDriverClassName(): String { + return "com.microsoft.sqlserver.jdbc.SQLServerDriver" + } + + override fun constructUrlForConnection(queryString: String): String { + // The JDBC driver of MS SQL Server enables encryption by default for versions > 10.1.0. + // We need to disable it by default to be able to use the container without having to pass extra params. + // See https://github.com/microsoft/mssql-jdbc/releases/tag/v10.1.0 + if (urlParameters.keys.stream().map { obj: String -> + obj.lowercase( + Locale.getDefault() + ) + }.noneMatch { anObject: String? -> "encrypt".equals(anObject) }) { + urlParameters["encrypt"] = "false" + } + return super.constructUrlForConnection(queryString) + } + + override fun getJdbcUrl(): String { + val additionalUrlParams = constructUrlParameters(";", ";") + return "jdbc:sqlserver://" + host + ":" + getMappedPort(MS_SQL_SERVER_PORT) + additionalUrlParams + } + + override fun getUsername(): String { + return DEFAULT_USER + } + + override fun getPassword(): String { + return password + } + + override fun getTestQueryString(): String { + return "SELECT 1" + } + + override fun getDatabaseName(): String { + return "master" + } + + fun getDriverName(): String { + return "sqlserver" + } + + override fun withPassword(password: String): AzureSQLEdgeServerContainer? { + checkPasswordStrength(password) + this.password = password + return self() + } + + private fun checkPasswordStrength(password: String?) { + requireNotNull(password) { "Null password is not allowed" } + require(password.length >= 8) { "Password should be at least 8 characters long" } + require(password.length <= 128) { "Password can be up to 128 characters long" } + val satisfiedCategories = Stream + .of(*PASSWORD_CATEGORY_VALIDATION_PATTERNS) + .filter { p: Pattern -> + p.matcher(password).find() + } + .count() + require(satisfiedCategories >= 3) { + """Password must contain characters from three of the following four categories: + - Latin uppercase letters (A through Z) + - Latin lowercase letters (a through z) + - Base 10 digits (0 through 9) + - Non-alphanumeric characters such as: exclamation point (!), dollar sign ($), number sign (#), or percent (%).""" + } + } + + companion object { + private val DEFAULT_IMAGE_NAME = DockerImageName.parse("mcr.microsoft.com/azure-sql-edge") + const val NAME = "sqlserver" + val IMAGE: String = DEFAULT_IMAGE_NAME.getUnversionedPart() + const val MS_SQL_SERVER_PORT = 1433 + const val DEFAULT_USER = "sa" + const val DEFAULT_PASSWORD = "A_Str0ng_Required_Password" + + private val PASSWORD_CATEGORY_VALIDATION_PATTERNS = arrayOf( + Pattern.compile("[A-Z]+"), + Pattern.compile("[a-z]+"), + Pattern.compile("[0-9]+"), + Pattern.compile("[^a-zA-Z0-9]+", Pattern.CASE_INSENSITIVE) + ) + } +} diff --git a/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/internal/LocalSqlServerExtension.kt b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/internal/LocalSqlServerExtension.kt new file mode 100644 index 0000000..14513d7 --- /dev/null +++ b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/internal/LocalSqlServerExtension.kt @@ -0,0 +1,33 @@ +/* + * 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 com.udaan.r2dbi.internal + +import com.udaan.r2dbi.R2DbiTestExtensionBase + +class LocalSqlServerExtension : R2DbiTestExtensionBase() { + override fun getHost(): String = "localhost" + + override fun getPassword(): String = "MyPass@word" + + override fun getPort(): Int = 1433 + + override fun getUsername(): String = "SA" + + override fun getDatabaseName(): String = "db-test" + + override fun getDriver(): String = "sqlserver" + +} diff --git a/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/internal/SqlDatabaseContainerProvider.kt b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/internal/SqlDatabaseContainerProvider.kt new file mode 100644 index 0000000..d19dd5d --- /dev/null +++ b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/internal/SqlDatabaseContainerProvider.kt @@ -0,0 +1,78 @@ +/* + * 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 com.udaan.r2dbi.internal + +import org.testcontainers.containers.JdbcDatabaseContainer + +interface SqlDatabaseContainerProvider { + fun newInstance(): SqlDatabaseContainer +} + +interface SqlDatabaseContainer { + + fun start() + + fun stop() + + fun getHost(): String + fun getUserName(): String + fun getPassword(): String + + fun getPort(): Int + fun getDatabaseName(): String + + fun getDriverName(): String +} + +abstract class SqlDatabaseContainerAdapter(private val jdbcDatabaseContainer: JdbcDatabaseContainer<*>): SqlDatabaseContainer { + + init { + jdbcDatabaseContainer.withStartupTimeoutSeconds(DEFAULT_STARTUP_TIMEOUT_SECONDS) + jdbcDatabaseContainer.withConnectTimeoutSeconds(DEFAULT_CONNECT_TIMEOUT_SECONDS) + } + + override fun start() { + jdbcDatabaseContainer.start() + } + + override fun stop() { + jdbcDatabaseContainer.stop() + } + + override fun getHost(): String { + return jdbcDatabaseContainer.getHost() + } + + override fun getPort(): Int { + return jdbcDatabaseContainer.getFirstMappedPort() + } + + override fun getDatabaseName(): String { + return jdbcDatabaseContainer.getDatabaseName() + } + + override fun getUserName(): String { + return jdbcDatabaseContainer.getUsername() + } + + override fun getPassword(): String { + return jdbcDatabaseContainer.getPassword() + } + +} + +const val DEFAULT_STARTUP_TIMEOUT_SECONDS = 240 +const val DEFAULT_CONNECT_TIMEOUT_SECONDS = 240 diff --git a/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/internal/SqlDatabaseExtension.kt b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/internal/SqlDatabaseExtension.kt new file mode 100644 index 0000000..32b2f85 --- /dev/null +++ b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/internal/SqlDatabaseExtension.kt @@ -0,0 +1,40 @@ +/* + * 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 com.udaan.r2dbi.internal + +import com.udaan.r2dbi.R2DbiTestExtensionBase + +class SqlDatabaseExtension(provider: SqlDatabaseContainerProvider) : R2DbiTestExtensionBase() { + private val container = provider.newInstance() + + override fun beforeAll() { + container.start() + super.beforeAll() + } + + override fun afterAll() { + super.afterAll() + container.stop() + } + + override fun getHost(): String = container.getHost() + override fun getPassword(): String = container.getPassword() + override fun getPort(): Int = container.getPort() //getMappedPort(MSSQLServerContainer.MS_SQL_SERVER_PORT) + override fun getUsername(): String = container.getUserName() + + override fun getDriver(): String = container.getDriverName() + override fun getDatabaseName(): String = container.getDatabaseName() +} diff --git a/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/testDao/ConfigData.kt b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/testDao/ConfigData.kt new file mode 100644 index 0000000..dcb12a2 --- /dev/null +++ b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/testDao/ConfigData.kt @@ -0,0 +1,61 @@ +/* + * 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 com.udaan.r2dbi.testDao + +import com.udaan.r2dbi.mappers.ColumnName +import com.udaan.r2dbi.mappers.PojoMapperFactory + +data class ConfigData(val name: String, val value: String) +data class ConfigDataWithNullableField(val name: String, val value: String?) + +data class ConfigDataWithOptionalField(val name: String, val value: String = "hello") + +data class ConfigWithEnum(val name: TestConfigName, val value: String) + +class ConfigDataMapperFactory : PojoMapperFactory() { + override fun isSupportedType(type: Class<*>): Boolean { + return type.kotlin.isData + } +} + +data class ConfigDataWithDiffFieldNames( + @ColumnName("name") + val configName: String, +) { + @ColumnName("value") + var configValue: String = "" + +} + +data class ConfigDataWithDiffColumnNames( + @ColumnName("configName") + val name: String, + + @ColumnName("configValue") + val value: String +) + +data class ConfigWithBooleanValue( + val name: String, + val value: Boolean +) + +internal const val CONFIG_NAME1 = "Key1" +internal const val CONFIG_NAME2 = "Key2" +internal const val CONFIG_NAME3 = "Key3" +internal const val CONFIG_NAME4 = "Key4" +internal const val CONFIG_NAME5 = "Key5" +internal const val CONFIG_NAME6 = "Key6" diff --git a/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/testDao/TestQueryDao.kt b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/testDao/TestQueryDao.kt new file mode 100644 index 0000000..560e41b --- /dev/null +++ b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/testDao/TestQueryDao.kt @@ -0,0 +1,102 @@ +/* + * 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 com.udaan.r2dbi.testDao + +import com.udaan.r2dbi.SqlBatch +import com.udaan.r2dbi.SqlQuery +import com.udaan.r2dbi.binders.Bind +import com.udaan.r2dbi.binders.BindPojo +import com.udaan.r2dbi.sql.annotations.UseRowMapperFactory +import kotlinx.coroutines.flow.Flow +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux + +@UseRowMapperFactory(ConfigDataMapperFactory::class) +interface TestQueryDao { + + @SqlQuery("SELECT 1") + fun getOne(): Flow + + @SqlQuery("SELECT * FROM $TableName") + fun getConfig(): Flow + + @SqlQuery("SELECT * FROM $TableName where name = :name") + fun getConfigByName(@Bind("name") name: String): Flow + + @SqlQuery("SELECT * FROM $TableName where name = :name") + fun getConfigByNameWithDifferentFieldNames(@Bind("name") name: String): Flow + + @SqlQuery("SELECT name as configName, value as configValue FROM $TableName where name = :name") + fun getConfigByNameWithDifferentColumnNames(@Bind("name") name: String): Flow + + @SqlQuery("SELECT * FROM $TableName where name = :name") + fun getConfigByNameAsPublisher(@Bind("name") name: String): Publisher + + @SqlQuery("SELECT * FROM $TableName where name = :name") + fun getConfigByPojo(@BindPojo config: ConfigData): Flow + + @SqlBatch("SELECT * FROM $TableName where name = :name and value = :value") + fun getBatch(@Bind("name") list: List, @Bind("value") value: String): Flow + + @SqlBatch("SELECT * FROM $TableName where name = :name and value = :value") + fun getBatchAsPublisher(@Bind("name") list: List, @Bind("value") value: String): Flux + + @SqlBatch("SELECT * FROM $TableName where name = :name and value = :value", returnUpdatedRows = true) + fun getBatchAsPublisherAndRowsUpdated( + @Bind("name") list: List, + @Bind("value") value: String + ): Publisher + + @SqlQuery("SELECT value FROM $TableName where name = :name") + fun getSingleString(@Bind("name") name: String): Flow + + @SqlQuery("SELECT count(*) FROM $TableName where name = :name") + fun getCountOfValues(@Bind("name") name: String): Publisher + + @SqlQuery("SELECT count(*) FROM $TableName where name = :name") + fun getCountOfValues(@Bind("name") name: TestConfigName): Publisher + + @SqlQuery("SELECT * FROM $TableName where name = :name") + fun getConfigWithEnumValue(@Bind("name") name: String): Flow + + @SqlQuery("SELECT name FROM $TableName where name = :name") + fun getConfigNameOnlyAssumingValueIsOptional(@Bind("name") name: String): Flow + + @SqlQuery("SELECT name FROM $TableName where name = :name") + fun getConfigNameOnly(@Bind("name") name: String): Flow + + @SqlQuery("SELECT name, null as value FROM $TableName where name = :name") + fun getConfigAndForceValueAsNull(@Bind("name") name: String): Flow + + @SqlQuery("SELECT name, null as value FROM $TableName where name = :name") + fun getConfigAndValueAsNull(@Bind("name") name: String): Flow + + @SqlQuery("SELECT name FROM $TableName where name = :name") + fun getNoValueColForNonOptionalButNullableField(@Bind("name") name: String): Flow + + @SqlQuery("Select name as colName from $TableName where name = '$CONFIG_NAME1'") + fun getNullReturn(): Flow + + @SqlQuery("SELECT name, value FROM $TableName where name = :name and value = :name") + fun getConfigWithSameNameAndValue(@Bind("name") name: String): Flow +} + +internal const val TableName = "test" +enum class TestConfigName { + Key1, + Key2 +} + diff --git a/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/testDao/TestUpdateDao.kt b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/testDao/TestUpdateDao.kt new file mode 100644 index 0000000..0f94538 --- /dev/null +++ b/r2dbi-core/src/test/kotlin/com/udaan/r2dbi/testDao/TestUpdateDao.kt @@ -0,0 +1,36 @@ +/* + * 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 com.udaan.r2dbi.testDao + +import com.udaan.r2dbi.* +import com.udaan.r2dbi.binders.Bind +import com.udaan.r2dbi.binders.BindPojo +import kotlinx.coroutines.flow.Flow +import reactor.core.publisher.Flux + +interface TestUpdateDao { + @SqlUpdate("insert into $TableName values (:name, :value)") + fun insertSingleValue(@Bind("name") name: String, @Bind("value") value: String): Flow + @SqlUpdate("insert into $TableName values (:name, :value)") + fun insertSingle(@BindPojo config: ConfigData): Flux + + @SqlBatch("insert into $TableName values (:name, :value)", returnUpdatedRows = true) + fun insertMultiple(@BindPojo configs: List): Flow + + @Transaction + @SqlBatch("insert into $TableName values (:name, :value)", returnUpdatedRows = true) + fun insertSingleInTransaction(@BindPojo config: ConfigData): Flow +} diff --git a/r2dbi-core/src/test/resources/container-license-acceptance.txt b/r2dbi-core/src/test/resources/container-license-acceptance.txt new file mode 100644 index 0000000..53168fe --- /dev/null +++ b/r2dbi-core/src/test/resources/container-license-acceptance.txt @@ -0,0 +1,2 @@ +mcr.microsoft.com/mssql/server:2017-CU12 +mcr.microsoft.com/azure-sql-edge:latest diff --git a/r2dbi-core/src/test/resources/logback-test.xml b/r2dbi-core/src/test/resources/logback-test.xml new file mode 100644 index 0000000..4b0d434 --- /dev/null +++ b/r2dbi-core/src/test/resources/logback-test.xml @@ -0,0 +1,39 @@ + + + + + + + + %date{HH:mm:ss.SSS} %-18thread %-55logger %msg%n + + + + + + + + + + + + + + + + +