├── .gitignore ├── LICENSE ├── README.md ├── Snowflake.Client.Benchmarks ├── HexUtilsBenchmarks.cs ├── Program.cs ├── Snowflake.Client.Benchmarks.csproj └── SnowflakeDataMapperBenchmarks.cs ├── Snowflake.Client.Tests ├── IntegrationTests │ ├── IntegrationTestBase.cs │ ├── SnowflakeChunksDownloaderTest.cs │ ├── SnowflakeQueriesTest.cs │ ├── SnowflakeQueryAndMapTest_SimpleTypes.cs │ └── SnowflakeSessionTest.cs ├── Models │ ├── SnowflakeConnectionInfo.cs │ └── TestConfiguration.cs ├── Snowflake.Client.Tests.csproj ├── UnitTests │ ├── HexUtilsTests.cs │ ├── ParameterBindingsComplexTypesTest.cs │ ├── ParameterBindingsMultipleValuesTest.cs │ ├── ParameterBindingsSingleValuesTest.cs │ ├── SnowflakeClientSettingsTest.cs │ ├── SnowflakeDataMapperTests.cs │ └── SnowflakeTypesConverterTest.cs └── testconfig.json ├── Snowflake.Client ├── ChunksDownloader.cs ├── ConcatenatedStream.cs ├── DownloadedChunkRowSet.cs ├── Extensions │ └── EnumerableExtensions.cs ├── Helpers │ ├── HexUtils.cs │ ├── QuotedNumbersToIntConverter.cs │ └── SnowflakeUtils.cs ├── ISnowflakeClient.cs ├── Json │ ├── BaseRequest.cs │ ├── BaseResponse.cs │ ├── CloseResponse.cs │ ├── ColumnDescription.cs │ ├── ExecResponseChunk.cs │ ├── LoginRequest.cs │ ├── LoginRequestClientEnv.cs │ ├── LoginRequestData.cs │ ├── LoginResponse.cs │ ├── LoginResponseData.cs │ ├── NameValueParameter.cs │ ├── NullDataResponse.cs │ ├── ParamBinding.cs │ ├── QueryCancelRequest.cs │ ├── QueryExecResponse.cs │ ├── QueryExecResponseData.cs │ ├── QueryRequest.cs │ ├── RenewSessionRequest.cs │ ├── RenewSessionResponse.cs │ ├── RenewSessionResponseData.cs │ └── SessionInfoRaw.cs ├── Media │ └── snowflake_icon.png ├── Model │ ├── AuthInfo.cs │ ├── ChunksDownloadInfo.cs │ ├── ClientAppInfo.cs │ ├── SessionInfo.cs │ ├── SnowflakeClientSettings.cs │ ├── SnowflakeConst.cs │ ├── SnowflakeException.cs │ ├── SnowflakeQueryRawResponse.cs │ ├── SnowflakeRawData.cs │ ├── SnowflakeSession.cs │ ├── SnowflakeStatementType.cs │ └── UrlInfo.cs ├── ParameterBinder.cs ├── RequestBuilder.cs ├── RestClient.cs ├── Snowflake.Client.csproj ├── SnowflakeClient.cs ├── SnowflakeDataMapper.cs └── SnowflakeTypesConverter.cs └── SnowflakeClient.sln /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore project files 2 | Snowflake.Client.Tests/testconfig.json 3 | .idea/* 4 | 5 | ## Ignore Visual Studio temporary files, build results, and 6 | ## files generated by popular Visual Studio add-ons. 7 | ## 8 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 9 | 10 | # Visual Studio Code files 11 | .vscode/* 12 | !.vscode/settings.json 13 | !.vscode/tasks.json 14 | !.vscode/launch.json 15 | !.vscode/extensions.json 16 | *.code-workspace 17 | 18 | # Local History for Visual Studio Code 19 | .history/ 20 | 21 | # User-specific files 22 | *.rsuser 23 | *.suo 24 | *.user 25 | *.userosscache 26 | *.sln.docstates 27 | 28 | # User-specific files (MonoDevelop/Xamarin Studio) 29 | *.userprefs 30 | 31 | # Mono auto generated files 32 | mono_crash.* 33 | 34 | # Build results 35 | [Dd]ebug/ 36 | [Dd]ebugPublic/ 37 | [Rr]elease/ 38 | [Rr]eleases/ 39 | x64/ 40 | x86/ 41 | [Aa][Rr][Mm]/ 42 | [Aa][Rr][Mm]64/ 43 | bld/ 44 | [Bb]in/ 45 | [Oo]bj/ 46 | [Ll]og/ 47 | [Ll]ogs/ 48 | 49 | # Visual Studio 2015/2017 cache/options directory 50 | .vs/ 51 | # Uncomment if you have tasks that create the project's static files in wwwroot 52 | #wwwroot/ 53 | 54 | # Visual Studio 2017 auto generated files 55 | Generated\ Files/ 56 | 57 | # MSTest test Results 58 | [Tt]est[Rr]esult*/ 59 | [Bb]uild[Ll]og.* 60 | 61 | # NUnit 62 | *.VisualState.xml 63 | TestResult.xml 64 | nunit-*.xml 65 | 66 | # Build Results of an ATL Project 67 | [Dd]ebugPS/ 68 | [Rr]eleasePS/ 69 | dlldata.c 70 | 71 | # Benchmark Results 72 | BenchmarkDotNet.Artifacts/ 73 | 74 | # .NET Core 75 | project.lock.json 76 | project.fragment.lock.json 77 | artifacts/ 78 | 79 | # StyleCop 80 | StyleCopReport.xml 81 | 82 | # Files built by Visual Studio 83 | *_i.c 84 | *_p.c 85 | *_h.h 86 | *.ilk 87 | *.meta 88 | *.obj 89 | *.iobj 90 | *.pch 91 | *.pdb 92 | *.ipdb 93 | *.pgc 94 | *.pgd 95 | *.rsp 96 | *.sbr 97 | *.tlb 98 | *.tli 99 | *.tlh 100 | *.tmp 101 | *.tmp_proj 102 | *_wpftmp.csproj 103 | *.log 104 | *.vspscc 105 | *.vssscc 106 | .builds 107 | *.pidb 108 | *.svclog 109 | *.scc 110 | 111 | # Chutzpah Test files 112 | _Chutzpah* 113 | 114 | # Visual C++ cache files 115 | ipch/ 116 | *.aps 117 | *.ncb 118 | *.opendb 119 | *.opensdf 120 | *.sdf 121 | *.cachefile 122 | *.VC.db 123 | *.VC.VC.opendb 124 | 125 | # Visual Studio profiler 126 | *.psess 127 | *.vsp 128 | *.vspx 129 | *.sap 130 | 131 | # Visual Studio Trace Files 132 | *.e2e 133 | 134 | # TFS 2012 Local Workspace 135 | $tf/ 136 | 137 | # Guidance Automation Toolkit 138 | *.gpState 139 | 140 | # ReSharper is a .NET coding add-in 141 | _ReSharper*/ 142 | *.[Rr]e[Ss]harper 143 | *.DotSettings.user 144 | 145 | # TeamCity is a build add-in 146 | _TeamCity* 147 | 148 | # DotCover is a Code Coverage Tool 149 | *.dotCover 150 | 151 | # AxoCover is a Code Coverage Tool 152 | .axoCover/* 153 | !.axoCover/settings.json 154 | 155 | # Visual Studio code coverage results 156 | *.coverage 157 | *.coveragexml 158 | 159 | # NCrunch 160 | _NCrunch_* 161 | .*crunch*.local.xml 162 | nCrunchTemp_* 163 | 164 | # MightyMoose 165 | *.mm.* 166 | AutoTest.Net/ 167 | 168 | # Web workbench (sass) 169 | .sass-cache/ 170 | 171 | # Installshield output folder 172 | [Ee]xpress/ 173 | 174 | # DocProject is a documentation generator add-in 175 | DocProject/buildhelp/ 176 | DocProject/Help/*.HxT 177 | DocProject/Help/*.HxC 178 | DocProject/Help/*.hhc 179 | DocProject/Help/*.hhk 180 | DocProject/Help/*.hhp 181 | DocProject/Help/Html2 182 | DocProject/Help/html 183 | 184 | # Click-Once directory 185 | publish/ 186 | 187 | # Publish Web Output 188 | *.[Pp]ublish.xml 189 | *.azurePubxml 190 | # Note: Comment the next line if you want to checkin your web deploy settings, 191 | # but database connection strings (with potential passwords) will be unencrypted 192 | *.pubxml 193 | *.publishproj 194 | 195 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 196 | # checkin your Azure Web App publish settings, but sensitive information contained 197 | # in these scripts will be unencrypted 198 | PublishScripts/ 199 | 200 | # NuGet Packages 201 | *.nupkg 202 | # NuGet Symbol Packages 203 | *.snupkg 204 | # The packages folder can be ignored because of Package Restore 205 | **/[Pp]ackages/* 206 | # except build/, which is used as an MSBuild target. 207 | !**/[Pp]ackages/build/ 208 | # Uncomment if necessary however generally it will be regenerated when needed 209 | #!**/[Pp]ackages/repositories.config 210 | # NuGet v3's project.json files produces more ignorable files 211 | *.nuget.props 212 | *.nuget.targets 213 | 214 | # Microsoft Azure Build Output 215 | csx/ 216 | *.build.csdef 217 | 218 | # Microsoft Azure Emulator 219 | ecf/ 220 | rcf/ 221 | 222 | # Windows Store app package directories and files 223 | AppPackages/ 224 | BundleArtifacts/ 225 | Package.StoreAssociation.xml 226 | _pkginfo.txt 227 | *.appx 228 | *.appxbundle 229 | *.appxupload 230 | 231 | # Visual Studio cache files 232 | # files ending in .cache can be ignored 233 | *.[Cc]ache 234 | # but keep track of directories ending in .cache 235 | !?*.[Cc]ache/ 236 | 237 | # Others 238 | ClientBin/ 239 | ~$* 240 | *~ 241 | *.dbmdl 242 | *.dbproj.schemaview 243 | *.jfm 244 | *.pfx 245 | *.publishsettings 246 | orleans.codegen.cs 247 | 248 | # Including strong name files can present a security risk 249 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 250 | #*.snk 251 | 252 | # Since there are multiple workflows, uncomment next line to ignore bower_components 253 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 254 | #bower_components/ 255 | 256 | # RIA/Silverlight projects 257 | Generated_Code/ 258 | 259 | # Backup & report files from converting an old project file 260 | # to a newer Visual Studio version. Backup files are not needed, 261 | # because we have git ;-) 262 | _UpgradeReport_Files/ 263 | Backup*/ 264 | UpgradeLog*.XML 265 | UpgradeLog*.htm 266 | ServiceFabricBackup/ 267 | *.rptproj.bak 268 | 269 | # SQL Server files 270 | *.mdf 271 | *.ldf 272 | *.ndf 273 | 274 | # Business Intelligence projects 275 | *.rdl.data 276 | *.bim.layout 277 | *.bim_*.settings 278 | *.rptproj.rsuser 279 | *- [Bb]ackup.rdl 280 | *- [Bb]ackup ([0-9]).rdl 281 | *- [Bb]ackup ([0-9][0-9]).rdl 282 | 283 | # Microsoft Fakes 284 | FakesAssemblies/ 285 | 286 | # GhostDoc plugin setting file 287 | *.GhostDoc.xml 288 | 289 | # Node.js Tools for Visual Studio 290 | .ntvs_analysis.dat 291 | node_modules/ 292 | 293 | # Visual Studio 6 build log 294 | *.plg 295 | 296 | # Visual Studio 6 workspace options file 297 | *.opt 298 | 299 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 300 | *.vbw 301 | 302 | # Visual Studio LightSwitch build output 303 | **/*.HTMLClient/GeneratedArtifacts 304 | **/*.DesktopClient/GeneratedArtifacts 305 | **/*.DesktopClient/ModelManifest.xml 306 | **/*.Server/GeneratedArtifacts 307 | **/*.Server/ModelManifest.xml 308 | _Pvt_Extensions 309 | 310 | # Paket dependency manager 311 | .paket/paket.exe 312 | paket-files/ 313 | 314 | # FAKE - F# Make 315 | .fake/ 316 | 317 | # CodeRush personal settings 318 | .cr/personal 319 | 320 | # Python Tools for Visual Studio (PTVS) 321 | __pycache__/ 322 | *.pyc 323 | 324 | # Cake - Uncomment if you are using it 325 | # tools/** 326 | # !tools/packages.config 327 | 328 | # Tabs Studio 329 | *.tss 330 | 331 | # Telerik's JustMock configuration file 332 | *.jmconfig 333 | 334 | # BizTalk build output 335 | *.btp.cs 336 | *.btm.cs 337 | *.odx.cs 338 | *.xsd.cs 339 | 340 | # OpenCover UI analysis results 341 | OpenCover/ 342 | 343 | # Azure Stream Analytics local run output 344 | ASALocalRun/ 345 | 346 | # MSBuild Binary and Structured Log 347 | *.binlog 348 | 349 | # NVidia Nsight GPU debugger configuration file 350 | *.nvuser 351 | 352 | # MFractors (Xamarin productivity tool) working folder 353 | .mfractor/ 354 | 355 | # Local History for Visual Studio 356 | .localhistory/ 357 | 358 | # BeatPulse healthcheck temp database 359 | healthchecksdb 360 | 361 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 362 | MigrationBackup/ 363 | 364 | # Ionide (cross platform F# VS Code tools) working folder 365 | .ionide/ 366 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NuGet](https://img.shields.io/badge/nuget-v0.4.6-green.svg)](https://www.nuget.org/packages/Snowflake.Client/) 2 | [![](https://img.shields.io/nuget/dt/Snowflake.Client.svg)](https://www.nuget.org/packages/Snowflake.Client/) 3 | [![Targets](https://img.shields.io/badge/.NET%20Standard-2.0-green.svg)](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) 4 | [![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)](https://opensource.org/licenses/Apache-2.0) 5 | 6 | ## Snowflake.Client 7 | .NET client for [Snowflake](https://www.snowflake.com) REST API. 8 | Provides API to execute SQL queries and map response to your models. 9 | Read my [blog post](https://medium.com/@fixer_m/better-net-client-for-snowflake-db-ecb48c48c872) about the ideas behind it. 10 | 11 | ### Installation 12 | Add nuget package [Snowflake.Client](https://www.nuget.org/packages/Snowflake.Client) to your project: 13 | ```{r, engine='bash', code_block_name} 14 | PM> Install-Package Snowflake.Client 15 | ``` 16 | 17 | ### Main features 18 | - User/Password authentication 19 | - Execute any SQL queries with parameters 20 | - Map response data to your models 21 | 22 | ### Basic usage 23 | ```csharp 24 | // Creates new client 25 | var snowflakeClient = new SnowflakeClient("user", "password", "account", "region"); 26 | 27 | // Executes query and maps response data to "Employee" class 28 | var employees = await snowflakeClient.QueryAsync("SELECT * FROM MASTER.PUBLIC.EMPLOYEES;"); 29 | 30 | // Executes query and returns raw response from Snowflake (rows, columns and query information) 31 | var queryRawResponse = await snowflakeClient.QueryRawResponseAsync("SELECT * FROM MASTER.PUBLIC.EMPLOYEES;"); 32 | 33 | // Executes query and returns value of first cell as string result 34 | string useRoleResult = await snowflakeClient.ExecuteScalarAsync("USE ROLE ACCOUNTADMIN;"); 35 | 36 | // Executes query and returns affected rows count 37 | int affectedRows = await snowflakeClient.ExecuteAsync("INSERT INTO EMPLOYEES Title VALUES (?);", "Dev"); 38 | ``` 39 | 40 | ### Comparison with Snowflake.Data 41 | Official [Snowflake.Data](https://github.com/snowflakedb/snowflake-connector-net) connector implements ADO.NET interfaces (IDbConnection, IDataReader etc), so you have to work with it as with usual database, however under the hood it actually uses Snowflake REST API. In contrast Snowflake.Client is designed as REST API client wrapper with convenient API. [Read more](https://medium.com/@fixer_m/better-net-client-for-snowflake-db-ecb48c48c872) about it. 42 | 43 | Improvements in Snowflake.Client vs Snowflake.Data: 44 | - Performance: Re-uses Snowflake session, i.e. **~3x less roundtrips to SF** 45 | - Performance: Doesn't have additional intermediate mapping from SF to DB types 46 | - Better API: Clean and simple API vs verbose ADO.NET 47 | - No third party dependencies vs 9 external packages in Snowflake.Data 48 | 49 | New features in Snowflake.Client: 50 | - Map response data to entities 51 | - Supports `describeOnly` flag 52 | - Has option to return raw data from Snowflake response (including QueryID and more) 53 | - Exposes Snowflake session info 54 | - New SQL parameter binding API with a few options (inspired by Dapper) 55 | 56 | Missing features in Snowflake.Client: 57 | - Authentication options other than basic user/password 58 | - GET/PUT command (was recenlty implemented in Snowflake.Data) 59 | 60 | ### Parameter binding 61 | Snowflake supports two placeholder formats for [parameter binding](https://docs.snowflake.com/en/user-guide/python-connector-example.html#qmark-or-numeric-binding): 62 | - Positional — with a "?" placeholders 63 | - Named — parameter name prefixed with a ":" 64 | 65 | Both formats are supported. You can use positional placeholders to bind values of "simple" types (like `Int`, `String` or `DateTime`). To bind named parameters you can use classes, structs, anonymous types or dictionary. See examples below. 66 | ```csharp 67 | // Positional placeholder, any "simple" type 68 | var result1 = await snowflakeClient.QueryAsync 69 | ("SELECT * FROM EMPLOYEES WHERE TITLE = ?", "Programmer"); 70 | 71 | // Positional placeholders, any IEnumerable 72 | var result2 = await snowflakeClient.QueryAsync 73 | ("SELECT * FROM EMPLOYEES WHERE ID IN (?, ?, ?)", new int[] { 1, 2, 3 }); 74 | 75 | // Named placeholders, any custom class or struct 76 | var result3 = await snowflakeClient.QueryAsync 77 | ("SELECT * FROM EMPLOYEES WHERE TITLE = :Title", new Employee() { Title = "Programmer" }); 78 | 79 | // Named placeholders, any anonymous class 80 | var result4 = await snowflakeClient.QueryAsync 81 | ("SELECT * FROM EMPLOYEES WHERE TITLE = :Title", new { Title = "Junior" }); 82 | 83 | // Named placeholders, any IDictionary 84 | var result5 = await snowflakeClient.QueryAsync 85 | ("SELECT * FROM EMPLOYEES WHERE TITLE = :Title", new Dictionary {{ "Title", "Programmer" }}); 86 | ``` 87 | 88 | ### Mapping basics 89 | Use `QueryAsync` method to get response data automatically mapped to your model (`T`): 90 | ```csharp 91 | // Executes query and maps response data to "Employee" class 92 | IEnumerable employees = await snowflakeClient.QueryAsync("SELECT * FROM MASTER.PUBLIC.EMPLOYEES;"); 93 | 94 | // Your model 95 | public class Employee 96 | { 97 | public int Id { get; set; } 98 | public float? Rating { get; set; } 99 | public bool? IsFired { get; set; } 100 | public string FirstName { get; set; } 101 | public string[] ContactLinks { get; set; } // supports arrays and lists 102 | public EmplyeeInfo Info { get; set; } // supports custom json ojects ("object" and "variant") 103 | public DateTimeOffset HiredAt { get; set; } // DateTimeOffset for "timestamp_ltz" and "timestamp_tz" 104 | public DateTime FiredAt { get; set; } // DateTime for "date", "time" and "timestamp_ntz" 105 | public byte[] Image { get; set; } // bytes array/list for "binary" 106 | } 107 | ``` 108 | 109 | Internally it uses [System.Text.Json](https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-apis/) to deserialize Snowflake data to your model. It uses [default deserialize behavior](https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-how-to?pivots=dotnet-5-0#deserialization-behavior), except `PropertyNameCaseInsensitive` is set to **true**, so your properties names don't have to be in the exact same case as column names in your tables. 110 | You can override this behavior by providing custom `JsonSerializerOptions`. You can pass it in `SnowflakeClient` constructor or you can set it directly via `SnowflakeDataMapper.SetJsonMapperOptions(jsonSerializerOptions)`. 111 | 112 | ### Advanced usage 113 | You may want to get raw response from Snowflake, for example, to get **QueryID** or some other information. 114 | In this case you can use mapper explicitly: 115 | ```csharp 116 | // Executes query and returns raw response from Snowflake (rows, columns and query information) 117 | var queryDataResponse = await snowflakeClient.QueryRawResponseAsync("SELECT * FROM MASTER.PUBLIC.EMPLOYEES;"); 118 | 119 | // Maps Snowflake rows and columns to your model (internally uses System.Text.Json) 120 | var employees = SnowflakeDataMapper.MapTo(queryDataResponse.Columns, queryDataResponse.Rows); 121 | ``` 122 | You can override internal http client. For example, this can be used to bypass SSL check: 123 | ```csharp 124 | var handler = new HttpClientHandler 125 | { 126 | SslProtocols = SslProtocols.Tls12, 127 | CheckCertificateRevocationList = false, 128 | ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true // i.e. bypass cert validation 129 | }; 130 | 131 | var httpClient = new HttpClient(handler); 132 | var snowflakeClient = new SnowflakeClient("user", "password", "account", "region"); 133 | snowflakeClient.SetHttpClient(httpClient); 134 | ``` 135 | 136 | ### Release notes 137 | 138 | 0.4.4 139 | - Now client has exposed `Settings` property, so you can configure client settings after it's creation 140 | - Implemented concurrent `ChunksDownloader`, prefetch threads count is configurable https://github.com/fixer-m/snowflake-db-net-client/issues/26 141 | - Added client option to prefetch chunks for `QueryRawResponseAsync` 142 | - Added new public method `ExecuteScalarAsync` https://github.com/fixer-m/snowflake-db-net-client/issues/32 143 | - Fixed issue with renewing session not working https://github.com/fixer-m/snowflake-db-net-client/issues/31 144 | 145 | 0.4.3 146 | - Move to .NET 6 147 | 148 | 0.4.2 149 | - Add new regions cloud tags 150 | 151 | 0.4.1 152 | - Fixed issue with rowset data not being combined with chunks https://github.com/fixer-m/snowflake-db-net-client/pull/27 153 | 154 | 0.4.0 155 | - Increased http client tiemout to 1 hour for a long running queries 156 | - Added missing cancellation token for a few methods 157 | 158 | 0.3.9 159 | - Now can handle [long-running queries](https://github.com/fixer-m/snowflake-db-net-client/issues/15) (> 45 seconds) 160 | - Now returns date-time values as `DateTimeKind.Unspecified` 161 | 162 | 0.3.8 163 | - Implemented [downloading big data reponses](https://github.com/fixer-m/snowflake-db-net-client/issues/13) (> 1000 rows) from chunks (`ChunksDownloader`) 164 | - Now returns affected rows count for `COPY UNLOAD` command 165 | 166 | 0.3.7 167 | - Added cancellation token for public async methods 168 | - Improved mapping tests 169 | 170 | 0.3.6 171 | - Set `Expect100Continue` and `UseNagleAlgorithm` to false for better HTTP performance 172 | - Allow streaming for http responses with `ResponseHeadersRead` option 173 | - Improved bool mapping 174 | - Adding `IDictionary<>` support for binding parameters 175 | 176 | 0.3.5 177 | - Added response auto-decompression 178 | - Added cloud tag auto-detection to finally fix [SSL cert issue](https://github.com/fixer-m/snowflake-db-net-client/issues/7) 179 | - Fix: explicit URL host now actually have higher priority than auto-constructed 180 | - Now it automatically replaces underscore in account name 181 | - [More info on this release](https://github.com/fixer-m/snowflake-db-net-client/issues/7#issuecomment-812715944) 182 | 183 | 0.3.4 184 | - Forced TLS 1.2 and revocation check as in official connector 185 | 186 | 0.3.3 187 | - Added `SetHttpClient()` as workaround for [SSL cert issue](https://github.com/fixer-m/snowflake-db-net-client/issues/7) 188 | 189 | 0.3.2 190 | - Added support for binding from class fields and structs 191 | - Added a lot of unit tests 192 | - Started working on integration tests 193 | - Now uses it's own driver name _.NET_Snowflake.Client_ 194 | 195 | 0.3.1 196 | - Implemented query cancellation with `CancelQueryAsync()` 197 | - Fixed [issue with mapping](https://github.com/fixer-m/snowflake-db-net-client/issues/4#issue-795843806) for semi-structured SF types (object, variant, array) 198 | - Implemented auto-renewing SF session, if its expired 199 | - Initializes SF session automatically with first query 200 | - `QueryRawAsync()` now returns response with all metadata 201 | - Extracted interfaces for public classes 202 | 203 | 0.3.0 204 | - Changed all API methods to be async 205 | - Added a lot of documentation in readme file 206 | - Implemented first unit tests 207 | - Target frameworks changed to NET Standard 2.0 and .NET Core 3.1 208 | 209 | 0.2.4 210 | - Fix: now actually uses passed `JsonMapperOptions` 211 | - New `Execute()` method which returns affected rows count 212 | 213 | 0.2.3 214 | - Changed return type of `ExecuteScalar()` to string 215 | - Added comments and documentation for public methods 216 | 217 | 0.2.2 218 | - First public release 219 | - Implemented all basic features 220 | -------------------------------------------------------------------------------- /Snowflake.Client.Benchmarks/HexUtilsBenchmarks.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using BenchmarkDotNet.Attributes; 3 | using Snowflake.Client.Helpers; 4 | 5 | namespace Snowflake.Client.Benchmarks; 6 | 7 | [MemoryDiagnoser] 8 | public class HexUtilsBenchmarks 9 | { 10 | public static readonly string __hexCharsLong = GenerateRandomHex(2_000_000); 11 | 12 | private MemoryStream _memoryStream; 13 | private StreamWriter _streamWriter; 14 | 15 | [GlobalSetup] 16 | public void IterationSetup() 17 | { 18 | _memoryStream = new MemoryStream(1_000_000); 19 | _streamWriter = new StreamWriter(_memoryStream); 20 | } 21 | 22 | [Benchmark] 23 | public void HexToBase64_Short() 24 | { 25 | HexUtils.HexToBase64("0a0b0c", _streamWriter); 26 | _streamWriter.Flush(); 27 | _memoryStream.SetLength(0); 28 | } 29 | 30 | [Benchmark] 31 | public void HexToBase64_Long() 32 | { 33 | HexUtils.HexToBase64(__hexCharsLong, _streamWriter); 34 | _streamWriter.Flush(); 35 | _memoryStream.SetLength(0); 36 | } 37 | 38 | private static string GenerateRandomHex(int length) 39 | { 40 | Random rng = new(123); 41 | 42 | StringBuilder sb = new(length); 43 | for(int i = 0; i < length; i++) 44 | { 45 | sb.Append(rng.Next(0, 16).ToString("X")); 46 | } 47 | 48 | return sb.ToString(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Snowflake.Client.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | using Snowflake.Client.Benchmarks; 3 | 4 | var switcher = new BenchmarkSwitcher(new[] { 5 | typeof(SnowflakeDataMapperBenchmarks), 6 | typeof(HexUtilsBenchmarks) 7 | }); 8 | 9 | switcher.Run(args); -------------------------------------------------------------------------------- /Snowflake.Client.Benchmarks/Snowflake.Client.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Snowflake.Client.Benchmarks/SnowflakeDataMapperBenchmarks.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using Snowflake.Client.Json; 3 | 4 | namespace Snowflake.Client.Benchmarks; 5 | 6 | [MemoryDiagnoser] 7 | public class SnowflakeDataMapperBenchmarks 8 | { 9 | readonly QueryExecResponseData _responseSample; 10 | 11 | public SnowflakeDataMapperBenchmarks() 12 | { 13 | _responseSample = GetFakeResponse(); 14 | } 15 | 16 | [Benchmark] 17 | public int ResponseWithValues_MapTo_CustomClass() 18 | { 19 | var enumerable = SnowflakeDataMapper.MapTo(_responseSample.RowType, _responseSample.RowSet); 20 | 21 | // Enumerate the result to actually execute the code inside the iterator method. 22 | int totalLen = 0; 23 | foreach(var result in enumerable) 24 | { 25 | totalLen += result.StringProperty?.Length ?? 0; 26 | } 27 | 28 | return totalLen; 29 | } 30 | 31 | private static QueryExecResponseData GetFakeResponse() 32 | { 33 | var response = new QueryExecResponseData() { RowType = new List(), RowSet = new List>() }; 34 | 35 | response.RowType.Add(new ColumnDescription() { Name = "StringProperty", Type = "text" }); 36 | response.RowType.Add(new ColumnDescription() { Name = "BoolProperty", Type = "boolean" }); 37 | response.RowType.Add(new ColumnDescription() { Name = "IntProperty", Type = "fixed" }); 38 | response.RowType.Add(new ColumnDescription() { Name = "FloatProperty", Type = "real" }); 39 | response.RowType.Add(new ColumnDescription() { Name = "DecimalProperty", Type = "real" }); 40 | response.RowType.Add(new ColumnDescription() { Name = "DateTimeProperty", Type = "timestamp_ntz" }); 41 | response.RowType.Add(new ColumnDescription() { Name = "DateTimeOffsetProperty", Type = "timestamp_ltz" }); 42 | response.RowType.Add(new ColumnDescription() { Name = "GuidProperty", Type = "text" }); 43 | response.RowType.Add(new ColumnDescription() { Name = "ByteArrayProperty", Type = "binary" }); 44 | 45 | for(int i=0; i < 100; i++) 46 | { 47 | response.RowSet.Add( 48 | new List() 49 | { 50 | "Sometext", 51 | "true", 52 | "7", 53 | "27.6", 54 | "19.239834", 55 | "1600000000.000000000", 56 | "1600000000.000000000", 57 | "e7412bbf-88ee-4149-b341-101e0f72ec7c", 58 | "0080ff0a0b0c0d0e0f" 59 | }); 60 | } 61 | 62 | return response; 63 | } 64 | 65 | public class CustomClass 66 | { 67 | public string StringProperty { get; set; } 68 | 69 | public bool BoolProperty { get; set; } 70 | 71 | public int IntProperty { get; set; } 72 | 73 | public float FloatProperty { get; set; } 74 | 75 | public decimal DecimalProperty { get; set; } 76 | 77 | public DateTime DateTimeProperty { get; set; } 78 | 79 | public DateTimeOffset DateTimeOffsetProperty { get; set; } 80 | 81 | public Guid GuidProperty { get; set; } 82 | 83 | public byte[] ByteArrayProperty { get; set; } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Snowflake.Client.Tests/IntegrationTests/IntegrationTestBase.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text.Json; 3 | using Snowflake.Client.Tests.Models; 4 | 5 | namespace Snowflake.Client.Tests.IntegrationTests 6 | { 7 | public class IntegrationTestBase 8 | { 9 | protected readonly SnowflakeClient _snowflakeClient; 10 | 11 | public IntegrationTestBase() 12 | { 13 | var configJson = File.ReadAllText("testconfig.json"); 14 | var testParameters = JsonSerializer.Deserialize(configJson, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); 15 | var connectionInfo = testParameters.Connection; 16 | 17 | _snowflakeClient = new SnowflakeClient(new Model.AuthInfo 18 | { 19 | User = connectionInfo.User, 20 | Password = connectionInfo.Password, 21 | Account = connectionInfo.Account, 22 | Region = connectionInfo.Region 23 | }, 24 | new Model.SessionInfo 25 | { 26 | Warehouse = connectionInfo.Warehouse 27 | }); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Snowflake.Client.Tests/IntegrationTests/SnowflakeChunksDownloaderTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Snowflake.Client.Model; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Snowflake.Client.Tests.IntegrationTests 7 | { 8 | [TestFixture] 9 | public class SnowflakeChunksDownloaderTest : IntegrationTestBase 10 | { 11 | [Test] 12 | public async Task DownloadAndParseChunks_MultipleThreads() 13 | { 14 | // Will produce 6 chunks 15 | var query = "select top 40000 * from SNOWFLAKE_SAMPLE_DATA.TPCH_SF1000.SUPPLIER;"; 16 | var result = await _snowflakeClient.QueryRawResponseAsync(query); 17 | 18 | var chunksDownloadInfo = new ChunksDownloadInfo() { ChunkHeaders = result.ChunkHeaders, Chunks = result.Chunks, Qrmk = result.Qrmk }; 19 | var parsed = await ChunksDownloader.DownloadAndParseChunksAsync(chunksDownloadInfo); 20 | var totalRowCountInChunks = result.Chunks.Sum(c => c.RowCount); 21 | 22 | Assert.AreEqual(totalRowCountInChunks, parsed.Count); 23 | } 24 | 25 | [Test] 26 | public async Task DownloadAndParseChunks_SingleThread() 27 | { 28 | // Will produce 6 chunks 29 | var query = "select top 40000 * from SNOWFLAKE_SAMPLE_DATA.TPCH_SF1000.SUPPLIER;"; 30 | var result = await _snowflakeClient.QueryRawResponseAsync(query); 31 | 32 | var chunksDownloadInfo = new ChunksDownloadInfo() { ChunkHeaders = result.ChunkHeaders, Chunks = result.Chunks, Qrmk = result.Qrmk }; 33 | var parsed = await ChunksDownloader.DownloadAndParseChunksSingleThreadAsync(chunksDownloadInfo); 34 | var totalRowCountInChunks = result.Chunks.Sum(c => c.RowCount); 35 | 36 | Assert.AreEqual(totalRowCountInChunks, parsed.Count); 37 | } 38 | 39 | [Test] 40 | public async Task QueryAndMap_ResponseWithChunks() 41 | { 42 | var selectCount = 10000; 43 | var query = $"select top {selectCount} * from SNOWFLAKE_SAMPLE_DATA.TPCH_SF1000.SUPPLIER;"; 44 | var result = await _snowflakeClient.QueryAsync(query); 45 | var records = result.ToList(); 46 | 47 | Assert.AreEqual(selectCount, records.Count); 48 | } 49 | 50 | [Test] 51 | public async Task QueryAndMap_ResponseWithChunksAndRowset() 52 | { 53 | var selectCount = 1370; 54 | var query = $"select top {selectCount} * from SNOWFLAKE_SAMPLE_DATA.TPCH_SF1000.SUPPLIER;"; 55 | var result = await _snowflakeClient.QueryAsync(query); 56 | var records = result.ToList(); 57 | 58 | Assert.AreEqual(selectCount, records.Count); 59 | } 60 | } 61 | 62 | internal class Supplier 63 | { 64 | public long? S_Suppkey { get; set; } 65 | public string S_Name { get; set; } 66 | public string S_Address { get; set; } 67 | public int? S_Nationkey { get; set; } 68 | public string S_Phone { get; set; } 69 | public float? S_Acctbal { get; set; } 70 | public string S_Comment { get; set; } 71 | } 72 | } -------------------------------------------------------------------------------- /Snowflake.Client.Tests/IntegrationTests/SnowflakeQueriesTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System.Threading.Tasks; 3 | 4 | namespace Snowflake.Client.Tests.IntegrationTests 5 | { 6 | [TestFixture] 7 | public class SnowflakeQueriesTest : IntegrationTestBase 8 | { 9 | [Test] 10 | public async Task ExecuteScalar_String() 11 | { 12 | var result = await _snowflakeClient.ExecuteScalarAsync("SELECT CURRENT_USER();"); 13 | Assert.IsTrue(!string.IsNullOrWhiteSpace(result)); 14 | } 15 | 16 | [Test] 17 | public async Task ExecuteScalar_Null() 18 | { 19 | var result = await _snowflakeClient.ExecuteScalarAsync("SELECT 1 WHERE 2 > 3;"); 20 | Assert.IsNull(result); 21 | } 22 | 23 | [Test] 24 | public async Task ExecuteScalar_Number() 25 | { 26 | var result = await _snowflakeClient.ExecuteScalarAsync("SELECT 1;"); 27 | Assert.AreEqual("1", result); 28 | } 29 | 30 | [Test] 31 | public async Task ExecuteScalar_Typed_String() 32 | { 33 | var result = await _snowflakeClient.ExecuteScalarAsync("SELECT CURRENT_USER();"); 34 | Assert.IsTrue(!string.IsNullOrWhiteSpace(result)); 35 | } 36 | 37 | [Test] 38 | public async Task ExecuteScalar_Typed_Null() 39 | { 40 | var result = await _snowflakeClient.ExecuteScalarAsync("SELECT 1 WHERE 2 > 3;"); 41 | Assert.IsNull(result); 42 | } 43 | 44 | [Test] 45 | public async Task ExecuteScalar_Typed_Number() 46 | { 47 | var result = await _snowflakeClient.ExecuteScalarAsync("SELECT 1;"); 48 | Assert.AreEqual(1, result); 49 | } 50 | 51 | [Test] 52 | public async Task Execute() 53 | { 54 | // todo: do temporary insert to get affected rows > 0 55 | 56 | var affectedRows = await _snowflakeClient.ExecuteAsync("SELECT 1;"); 57 | Assert.AreEqual(-1, affectedRows); 58 | } 59 | 60 | [Test] 61 | public async Task QueryRawResponse() 62 | { 63 | var result = await _snowflakeClient.QueryRawResponseAsync("SELECT CURRENT_USER();"); 64 | Assert.IsNotNull(result); 65 | } 66 | 67 | [Test] 68 | public async Task QueryRawResponse_WithDownloadChunks() 69 | { 70 | _snowflakeClient.Settings.DownloadChunksForQueryRawResponses = true; 71 | var selectCount = 1370; 72 | var query = $"select top {selectCount} * from SNOWFLAKE_SAMPLE_DATA.TPCH_SF1000.SUPPLIER;"; 73 | 74 | var result = await _snowflakeClient.QueryRawResponseAsync(query); 75 | 76 | _snowflakeClient.Settings.DownloadChunksForQueryRawResponses = false; 77 | Assert.AreEqual(result.Total, result.Rows.Count); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Snowflake.Client.Tests/IntegrationTests/SnowflakeQueryAndMapTest_SimpleTypes.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Snowflake.Client.Tests.IntegrationTests 7 | { 8 | [TestFixture] 9 | public class SnowflakeQueryAndMapTest_SimpleTypes : IntegrationTestBase 10 | { 11 | [Test] 12 | public async Task QueryAndMap_SimpleTypes() 13 | { 14 | await CreateAndPopulateTable(); 15 | 16 | var result = await _snowflakeClient.QueryAsync("SELECT * FROM DEMO_DB.PUBLIC.DATATYPES_SIMPLE;"); 17 | var records = result.ToList(); 18 | 19 | ValidateRecords(records); 20 | 21 | await RemoveTable(); 22 | } 23 | 24 | private async Task CreateAndPopulateTable() 25 | { 26 | var query = "CREATE OR REPLACE TABLE DEMO_DB.PUBLIC.DATATYPES_SIMPLE " + 27 | "(ID INT, SomeInt INT, SomeFloat FLOAT, SomeVarchar VARCHAR, SomeBoolean BOOLEAN, SomeBinary BINARY);"; 28 | 29 | var result = await _snowflakeClient.ExecuteScalarAsync(query); 30 | 31 | var insertQuery1 = "INSERT INTO DEMO_DB.PUBLIC.DATATYPES_SIMPLE (ID, SomeInt, SomeFloat, SomeVarchar, SomeBoolean, SomeBinary) " + 32 | "SELECT 1, 1, 2.5, 'some-text', true, to_binary(hex_encode('wow'));"; 33 | 34 | var insertQuery2 = "INSERT INTO DEMO_DB.PUBLIC.DATATYPES_SIMPLE (ID, SomeInt, SomeFloat, SomeVarchar, SomeBoolean, SomeBinary) " + 35 | "SELECT 2, 0, 777.0, '', false, null;"; 36 | 37 | var insertQuery3 = "INSERT INTO DEMO_DB.PUBLIC.DATATYPES_SIMPLE (ID, SomeInt, SomeFloat, SomeVarchar, SomeBoolean, SomeBinary) " + 38 | "SELECT 3, -1, -2.5, 'some-text\r\n with rn', null, to_binary(hex_encode('wow'), 'UTF-8');"; 39 | 40 | var insertQuery4 = "INSERT INTO DEMO_DB.PUBLIC.DATATYPES_SIMPLE (ID, SomeInt, SomeFloat, SomeVarchar, SomeBoolean, SomeBinary) " + 41 | "SELECT 4, null, null, null, null, null;"; 42 | 43 | var insertion1 = await _snowflakeClient.ExecuteAsync(insertQuery1); 44 | var insertion2 = await _snowflakeClient.ExecuteAsync(insertQuery2); 45 | var insertion3 = await _snowflakeClient.ExecuteAsync(insertQuery3); 46 | var insertion4 = await _snowflakeClient.ExecuteAsync(insertQuery4); 47 | } 48 | 49 | private void ValidateRecords(List records) 50 | { 51 | Assert.AreEqual(1, records[0].Id); 52 | Assert.AreEqual(1, records[0].SomeInt); 53 | Assert.AreEqual(2.5F, records[0].SomeFloat); 54 | Assert.AreEqual("some-text", records[0].SomeVarchar); 55 | Assert.AreEqual(true, records[0].SomeBoolean); 56 | Assert.AreEqual(new byte[] { 119, 111, 119 }, records[0].SomeBinary); 57 | 58 | Assert.AreEqual(2, records[1].Id); 59 | Assert.AreEqual(0, records[1].SomeInt); 60 | Assert.AreEqual(777.0F, records[1].SomeFloat); 61 | Assert.AreEqual("", records[1].SomeVarchar); 62 | Assert.AreEqual(false, records[1].SomeBoolean); 63 | Assert.AreEqual(null, records[1].SomeBinary); 64 | 65 | Assert.AreEqual(3, records[2].Id); 66 | Assert.AreEqual(-1, records[2].SomeInt); 67 | Assert.AreEqual(-2.5F, records[2].SomeFloat); 68 | Assert.AreEqual("some-text\r\n with rn", records[2].SomeVarchar); 69 | Assert.AreEqual(null, records[2].SomeBoolean); 70 | Assert.AreEqual(new byte[] { 55, 55, 54, 70, 55, 55 }, records[2].SomeBinary); 71 | 72 | Assert.AreEqual(4, records[3].Id); 73 | Assert.AreEqual(null, records[3].SomeInt); 74 | Assert.AreEqual(null, records[3].SomeFloat); 75 | Assert.AreEqual(null, records[3].SomeVarchar); 76 | Assert.AreEqual(null, records[3].SomeBoolean); 77 | Assert.AreEqual(null, records[3].SomeBinary); 78 | } 79 | 80 | private async Task RemoveTable() 81 | { 82 | var query = "DROP TABLE IF EXISTS DEMO_DB.PUBLIC.DATATYPES_SIMPLE;"; 83 | var result = await _snowflakeClient.ExecuteScalarAsync(query); 84 | 85 | return result; 86 | } 87 | } 88 | 89 | public class SimpleDataTypes 90 | { 91 | public int Id { get; set; } 92 | public int? SomeInt { get; set; } 93 | public float? SomeFloat { get; set; } 94 | public string SomeVarchar { get; set; } 95 | public bool? SomeBoolean { get; set; } 96 | public byte[] SomeBinary { get; set; } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Snowflake.Client.Tests/IntegrationTests/SnowflakeSessionTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System.Threading.Tasks; 3 | 4 | namespace Snowflake.Client.Tests.IntegrationTests 5 | { 6 | [TestFixture] 7 | public class SnowflakeSessionTest : IntegrationTestBase 8 | { 9 | [Test] 10 | public async Task InitNewSession() 11 | { 12 | var sessionInitialized = await _snowflakeClient.InitNewSessionAsync(); 13 | 14 | Assert.IsTrue(sessionInitialized); 15 | Assert.IsNotNull(_snowflakeClient.SnowflakeSession); 16 | } 17 | 18 | [Test] 19 | public async Task RenewSession() 20 | { 21 | var sessionInitialized = await _snowflakeClient.InitNewSessionAsync(); 22 | var firstSessionToken = _snowflakeClient.SnowflakeSession.SessionToken; 23 | 24 | var sessionRenewed = await _snowflakeClient.RenewSessionAsync(); 25 | var secondSessionToken = _snowflakeClient.SnowflakeSession.SessionToken; 26 | 27 | Assert.IsTrue(sessionInitialized); 28 | Assert.IsTrue(sessionRenewed); 29 | Assert.IsTrue(firstSessionToken != secondSessionToken); 30 | } 31 | 32 | [Test] 33 | public async Task CloseSession() 34 | { 35 | var sessionInitialized = await _snowflakeClient.InitNewSessionAsync(); 36 | var sessionClosed = await _snowflakeClient.CloseSessionAsync(); 37 | 38 | Assert.IsTrue(sessionInitialized); 39 | Assert.IsTrue(sessionClosed); 40 | Assert.IsNull(_snowflakeClient.SnowflakeSession); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Snowflake.Client.Tests/Models/SnowflakeConnectionInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Snowflake.Client.Tests.Models 2 | { 3 | public class SnowflakeConnectionInfo 4 | { 5 | public string User { get; set; } 6 | public string Password { get; set; } 7 | public string Account { get; set; } 8 | public string Region { get; set; } 9 | public string Warehouse { get; set; } 10 | public string Database { get; set; } 11 | public string Schema { get; set; } 12 | public string Role { get; set; } 13 | public string Host { get; set; } 14 | public string Protocol { get; set; } 15 | public int Port { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Snowflake.Client.Tests/Models/TestConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Snowflake.Client.Tests.Models 2 | { 3 | public class TestConfiguration 4 | { 5 | public SnowflakeConnectionInfo Connection { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Snowflake.Client.Tests/Snowflake.Client.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | false 6 | Copyright (c) 2020-2021 Ilya Bystrov 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | PreserveNewest 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Snowflake.Client.Tests/UnitTests/HexUtilsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.IO; 4 | using System.Text; 5 | using NUnit.Framework; 6 | using Snowflake.Client.Helpers; 7 | 8 | namespace Snowflake.Client.Tests.UnitTests 9 | { 10 | [TestFixture] 11 | public class HexUtilsTests 12 | { 13 | [Test] 14 | [TestCase("0080ff", 0, 128, 255)] 15 | [TestCase("0a0b0c", 10, 11, 12)] 16 | public void ConvertHexToBytes(string hex, byte b1, byte b2, byte b3) 17 | { 18 | var expectedBytes = new byte[] { b1, b2, b3 }; 19 | string expectedBase64 = Convert.ToBase64String(expectedBytes); 20 | byte[] expectedUtf8Bytes = Encoding.UTF8.GetBytes(expectedBase64); 21 | 22 | byte[] actualUtf8Bytes; 23 | 24 | using (var ms = new MemoryStream(32)) 25 | using (var sw = new StreamWriter(ms)) 26 | { 27 | HexUtils.HexToBase64(hex, sw); 28 | sw.Flush(); 29 | actualUtf8Bytes = ms.ToArray(); 30 | } 31 | 32 | Assert.AreEqual(expectedUtf8Bytes, actualUtf8Bytes); 33 | } 34 | 35 | [Test] 36 | [TestCaseSource("TestCases")] 37 | public void ConvertHexToBytes_VaryingLengths(string hex, byte[] expectedBytes) 38 | { 39 | string expectedBase64 = Convert.ToBase64String(expectedBytes); 40 | byte[] expectedUtf8Bytes = Encoding.UTF8.GetBytes(expectedBase64); 41 | 42 | byte[] actualUtf8Bytes; 43 | 44 | using(var ms = new MemoryStream(32)) 45 | using(var sw = new StreamWriter(ms)) 46 | { 47 | HexUtils.HexToBase64(hex, sw); 48 | sw.Flush(); 49 | actualUtf8Bytes = ms.ToArray(); 50 | } 51 | 52 | Assert.AreEqual(expectedUtf8Bytes, actualUtf8Bytes); 53 | } 54 | 55 | public static IEnumerable TestCases 56 | { 57 | get 58 | { 59 | yield return new TestCaseData("00", new byte[] { 0x00 }); 60 | yield return new TestCaseData("0080", new byte[] { 0x00, 0x80 }); 61 | yield return new TestCaseData("0080ff", new byte[] { 0x00, 0x80, 0xff }); 62 | yield return new TestCaseData("0a0b0c0d", new byte[] { 0x0a, 0x0b, 0x0c, 0x0d }); 63 | yield return new TestCaseData("0a0b0c0d0e", new byte[] { 0x0a, 0x0b, 0x0c, 0x0d, 0x0e }); 64 | yield return new TestCaseData("0a0b0c0d0e0f", new byte[] { 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }); 65 | } 66 | } 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Snowflake.Client.Tests/UnitTests/ParameterBindingsComplexTypesTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace Snowflake.Client.Tests.UnitTests 4 | { 5 | [TestFixture] 6 | public class ParameterBindingsComplexTypesTest 7 | { 8 | [Test] 9 | public void BuildParameters_FromClass_Properties() 10 | { 11 | var value = new CustomClassWithProperties() { IntProperty = 2, StringProperty = "test" }; 12 | 13 | var bindings = ParameterBinder.BuildParameterBindings(value); 14 | 15 | Assert.IsTrue(bindings.ContainsKey(nameof(value.IntProperty))); 16 | Assert.IsTrue(bindings[nameof(value.IntProperty)].Type == "FIXED"); 17 | Assert.IsTrue(bindings[nameof(value.IntProperty)].Value == value.IntProperty.ToString()); 18 | 19 | Assert.IsTrue(bindings.ContainsKey(nameof(value.StringProperty))); 20 | Assert.IsTrue(bindings[nameof(value.StringProperty)].Type == "TEXT"); 21 | Assert.IsTrue(bindings[nameof(value.StringProperty)].Value == value.StringProperty); 22 | } 23 | 24 | [Test] 25 | public void BuildParameters_FromStruct_Properties() 26 | { 27 | var value = new CustomStructWithProperties() { IntProperty = 2, StringProperty = "test" }; 28 | 29 | var bindings = ParameterBinder.BuildParameterBindings(value); 30 | 31 | Assert.IsTrue(bindings.ContainsKey(nameof(value.IntProperty))); 32 | Assert.IsTrue(bindings[nameof(value.IntProperty)].Type == "FIXED"); 33 | Assert.IsTrue(bindings[nameof(value.IntProperty)].Value == value.IntProperty.ToString()); 34 | 35 | Assert.IsTrue(bindings.ContainsKey(nameof(value.StringProperty))); 36 | Assert.IsTrue(bindings[nameof(value.StringProperty)].Type == "TEXT"); 37 | Assert.IsTrue(bindings[nameof(value.StringProperty)].Value == value.StringProperty); 38 | } 39 | 40 | [Test] 41 | public void BuildParameters_FromClass_Fields() 42 | { 43 | var value = new CustomClassWithFields() { IntField = 2, StringField = "test" }; 44 | 45 | var bindings = ParameterBinder.BuildParameterBindings(value); 46 | 47 | Assert.IsTrue(bindings.ContainsKey(nameof(value.IntField))); 48 | Assert.IsTrue(bindings[nameof(value.IntField)].Type == "FIXED"); 49 | Assert.IsTrue(bindings[nameof(value.IntField)].Value == value.IntField.ToString()); 50 | 51 | Assert.IsTrue(bindings.ContainsKey(nameof(value.StringField))); 52 | Assert.IsTrue(bindings[nameof(value.StringField)].Type == "TEXT"); 53 | Assert.IsTrue(bindings[nameof(value.StringField)].Value == value.StringField); 54 | } 55 | 56 | [Test] 57 | public void BuildParameters_FromStruct_Fields() 58 | { 59 | var value = new CustomStructWithFields() { IntField = 2, StringField = "test" }; 60 | 61 | var bindings = ParameterBinder.BuildParameterBindings(value); 62 | 63 | Assert.IsTrue(bindings.ContainsKey(nameof(value.IntField))); 64 | Assert.IsTrue(bindings[nameof(value.IntField)].Type == "FIXED"); 65 | Assert.IsTrue(bindings[nameof(value.IntField)].Value == value.IntField.ToString()); 66 | 67 | Assert.IsTrue(bindings.ContainsKey(nameof(value.StringField))); 68 | Assert.IsTrue(bindings[nameof(value.StringField)].Type == "TEXT"); 69 | Assert.IsTrue(bindings[nameof(value.StringField)].Value == value.StringField); 70 | } 71 | 72 | [Test] 73 | public void BuildParameters_FromAnonymousType() 74 | { 75 | var value = new { IntProperty = 2, StringProperty = "test" }; 76 | 77 | var bindings = ParameterBinder.BuildParameterBindings(value); 78 | 79 | Assert.IsTrue(bindings.ContainsKey(nameof(value.IntProperty))); 80 | Assert.IsTrue(bindings[nameof(value.IntProperty)].Type == "FIXED"); 81 | Assert.IsTrue(bindings[nameof(value.IntProperty)].Value == value.IntProperty.ToString()); 82 | 83 | Assert.IsTrue(bindings.ContainsKey(nameof(value.StringProperty))); 84 | Assert.IsTrue(bindings[nameof(value.StringProperty)].Type == "TEXT"); 85 | Assert.IsTrue(bindings[nameof(value.StringProperty)].Value == value.StringProperty); 86 | } 87 | private class CustomClassWithProperties 88 | { 89 | public string StringProperty { get; set; } 90 | 91 | public int IntProperty { get; set; } 92 | } 93 | 94 | private class CustomClassWithFields 95 | { 96 | public string StringField; 97 | 98 | public int IntField; 99 | } 100 | 101 | private struct CustomStructWithProperties 102 | { 103 | public string StringProperty { get; set; } 104 | 105 | public int IntProperty { get; set; } 106 | } 107 | 108 | private struct CustomStructWithFields 109 | { 110 | public string StringField; 111 | 112 | public int IntField; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Snowflake.Client.Tests/UnitTests/ParameterBindingsMultipleValuesTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace Snowflake.Client.Tests.UnitTests 7 | { 8 | [TestFixture] 9 | public class ParameterBindingsMultipleValuesTest 10 | { 11 | private static IEnumerable GetStringValues() 12 | { 13 | yield return "one"; 14 | yield return "two"; 15 | yield return "three"; 16 | } 17 | 18 | [Test] 19 | public void BuildParameters_List() 20 | { 21 | var values = GetStringValues().ToList(); 22 | 23 | var bindings = ParameterBinder.BuildParameterBindings(values); 24 | 25 | Assert.AreEqual(values.Count, bindings.Count); 26 | 27 | var i = 1; 28 | foreach (var binding in bindings) 29 | { 30 | Assert.IsTrue(binding.Key == i.ToString()); 31 | Assert.IsTrue(binding.Value.Type == "TEXT"); 32 | Assert.IsTrue(binding.Value.Value == values[i - 1]); 33 | 34 | i++; 35 | } 36 | } 37 | 38 | [Test] 39 | public void BuildParameters_Array() 40 | { 41 | var values = GetStringValues().ToArray(); 42 | 43 | var bindings = ParameterBinder.BuildParameterBindings(values); 44 | 45 | Assert.AreEqual(values.Length, bindings.Count); 46 | 47 | var i = 1; 48 | foreach (var binding in bindings) 49 | { 50 | Assert.IsTrue(binding.Key == i.ToString()); 51 | Assert.IsTrue(binding.Value.Type == "TEXT"); 52 | Assert.IsTrue(binding.Value.Value == values[i - 1]); 53 | 54 | i++; 55 | } 56 | } 57 | 58 | [Test] 59 | public void BuildParameters_Enumerable() 60 | { 61 | var values = GetStringValues(); 62 | var valuesList = values.ToList(); 63 | 64 | var bindings = ParameterBinder.BuildParameterBindings(values); 65 | 66 | Assert.AreEqual(values.Count(), bindings.Count); 67 | 68 | var i = 1; 69 | foreach (var binding in bindings) 70 | { 71 | Assert.IsTrue(binding.Key == i.ToString()); 72 | Assert.IsTrue(binding.Value.Type == "TEXT"); 73 | Assert.IsTrue(binding.Value.Value == valuesList[i - 1]); 74 | 75 | i++; 76 | } 77 | } 78 | 79 | [Test] 80 | public void BuildParameters_Dictionary_Ints() 81 | { 82 | var values = new Dictionary 83 | { 84 | { "First", 1 }, 85 | { "Second", 2 } 86 | }; 87 | 88 | var bindings = ParameterBinder.BuildParameterBindings(values); 89 | 90 | Assert.AreEqual(values.Count(), bindings.Count); 91 | 92 | var i = 0; 93 | foreach (var binding in bindings) 94 | { 95 | Assert.IsTrue(binding.Key == values.Keys.ElementAt(i)); 96 | Assert.IsTrue(binding.Value.Type == "FIXED"); 97 | Assert.IsTrue(binding.Value.Value == values.Values.ElementAt(i).ToString()); 98 | 99 | i++; 100 | } 101 | } 102 | 103 | [Test] 104 | public void BuildParameters_Dictionary_DifferentTypes() 105 | { 106 | var values = new Dictionary 107 | { 108 | { "1", "Sometext" }, 109 | { "2", true }, 110 | { "3", 777 }, 111 | { "4", 26.5F }, 112 | { "5", 19.239834M}, 113 | { "6", Guid.Parse("e7412bbf-88ee-4149-b341-101e0f72ec7c") }, 114 | { "7", new byte[] { 0, 128, 255 } } 115 | }; 116 | 117 | var bindings = ParameterBinder.BuildParameterBindings(values); 118 | 119 | Assert.AreEqual(values.Count(), bindings.Count); 120 | 121 | 122 | for (var i = 0; i < bindings.Count; i++) 123 | { 124 | Assert.IsTrue(bindings.Keys.ElementAt(i) == values.Keys.ElementAt(i)); 125 | } 126 | } 127 | 128 | [Test] 129 | public void BuildParameters_Dictionary_ComplexType() 130 | { 131 | var values = new Dictionary 132 | { 133 | { "1", new CustomClass() { Property = "Str" } } 134 | }; 135 | 136 | Assert.Throws(() => ParameterBinder.BuildParameterBindings(values)); 137 | } 138 | 139 | [Test] 140 | public void BuildParameters_Dictionary_CustomClass() 141 | { 142 | var values = new Dictionary 143 | { 144 | { "1", new CustomClass() { Property = "Str" } } 145 | }; 146 | 147 | Assert.Throws(() => ParameterBinder.BuildParameterBindings(values)); 148 | } 149 | 150 | [Test] 151 | public void BuildParameters_ListOfComplexTypes() 152 | { 153 | var values = GetCustomClassCollection().ToList(); 154 | 155 | Assert.Throws(() => ParameterBinder.BuildParameterBindings(values)); 156 | } 157 | 158 | [Test] 159 | public void BuildParameters_IEnumerableOfComplexTypes() 160 | { 161 | var values = GetCustomClassCollection(); 162 | 163 | Assert.Throws(() => ParameterBinder.BuildParameterBindings(values)); 164 | } 165 | 166 | private static IEnumerable GetCustomClassCollection() 167 | { 168 | yield return new CustomClass() { Property = "one" }; 169 | yield return new CustomClass() { Property = "two" }; 170 | } 171 | 172 | private class CustomClass 173 | { 174 | public string Property { get; set; } 175 | } 176 | } 177 | 178 | 179 | } 180 | -------------------------------------------------------------------------------- /Snowflake.Client.Tests/UnitTests/ParameterBindingsSingleValuesTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Globalization; 4 | 5 | namespace Snowflake.Client.Tests.UnitTests 6 | { 7 | [TestFixture] 8 | public class ParameterBindingsSingleValuesTest 9 | { 10 | [Test] 11 | [TestCase(-42, typeof(int), "FIXED")] 12 | [TestCase(42, typeof(uint), "FIXED")] 13 | [TestCase(-42, typeof(sbyte), "FIXED")] 14 | [TestCase(42, typeof(byte), "FIXED")] 15 | [TestCase(-42, typeof(short), "FIXED")] 16 | [TestCase(42, typeof(ushort), "FIXED")] 17 | [TestCase(-42, typeof(long), "FIXED")] 18 | [TestCase(42, typeof(ulong), "FIXED")] 19 | [TestCase(42.5, typeof(float), "REAL")] 20 | [TestCase(-42.5, typeof(double), "REAL")] 21 | [TestCase(42.5, typeof(decimal), "REAL")] 22 | public void BuildParameters_Numeric(object objectValue, Type type, string bindingType) 23 | { 24 | var value = Convert.ChangeType(objectValue, type); 25 | var binding = ParameterBinder.BuildParameterBindings(value); 26 | 27 | Assert.AreEqual(1, binding.Count); 28 | Assert.IsTrue(binding.ContainsKey("1")); 29 | Assert.AreEqual(bindingType, binding["1"].Type); 30 | 31 | var stringValue = string.Format(CultureInfo.InvariantCulture, "{0}", objectValue); 32 | Assert.AreEqual(stringValue, binding["1"].Value); 33 | } 34 | 35 | [Test] 36 | [TestCase(true, "BOOLEAN")] 37 | [TestCase(false, "BOOLEAN")] 38 | public void BuildParameters_Bool(bool value, string bindingType) 39 | { 40 | var binding = ParameterBinder.BuildParameterBindings(value); 41 | 42 | Assert.AreEqual(1, binding.Count); 43 | Assert.IsTrue(binding.ContainsKey("1")); 44 | Assert.AreEqual(bindingType, binding["1"].Type); 45 | Assert.AreEqual(value.ToString(), binding["1"].Value); 46 | } 47 | 48 | [Test] 49 | [TestCase("sometext", "TEXT")] 50 | [TestCase("", "TEXT")] 51 | public void BuildParameters_String(string value, string bindingType) 52 | { 53 | var binding = ParameterBinder.BuildParameterBindings(value); 54 | 55 | Assert.AreEqual(1, binding.Count); 56 | Assert.IsTrue(binding.ContainsKey("1")); 57 | Assert.AreEqual(bindingType, binding["1"].Type); 58 | Assert.AreEqual(value, binding["1"].Value); 59 | } 60 | 61 | [Test] 62 | public void BuildParameters_Guid() 63 | { 64 | var guid = Guid.NewGuid(); 65 | 66 | var binding = ParameterBinder.BuildParameterBindings(guid); 67 | 68 | var stringValue = string.Format(CultureInfo.InvariantCulture, "{0}", guid); 69 | 70 | Assert.AreEqual(1, binding.Count); 71 | Assert.IsTrue(binding.ContainsKey("1")); 72 | Assert.AreEqual("TEXT", binding["1"].Type); 73 | Assert.AreEqual(stringValue, binding["1"].Value); 74 | } 75 | 76 | [Test] 77 | [TestCase(new byte[] { 200, 201, 202 }, "c8c9ca", "BINARY")] 78 | [TestCase(new byte[] { 0 }, "00", "BINARY")] 79 | public void BuildParameters_BytesArray(byte[] value, string expectedString, string bindingType) 80 | { 81 | var binding = ParameterBinder.BuildParameterBindings(value); 82 | 83 | Assert.AreEqual(1, binding.Count); 84 | Assert.IsTrue(binding.ContainsKey("1")); 85 | Assert.AreEqual(bindingType, binding["1"].Type); 86 | Assert.AreEqual(expectedString, binding["1"].Value); 87 | } 88 | 89 | [Test] 90 | [TestCase("2021-06-10 16:17:18.0000000", "1623341838000000000", "TIMESTAMP_NTZ")] 91 | public void BuildParameters_DateTime(string stringValue, string expectedString, string bindingType) 92 | { 93 | var value = DateTime.ParseExact(stringValue, "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture); 94 | 95 | var binding = ParameterBinder.BuildParameterBindings(value); 96 | 97 | Assert.AreEqual(1, binding.Count); 98 | Assert.IsTrue(binding.ContainsKey("1")); 99 | Assert.AreEqual(bindingType, binding["1"].Type); 100 | Assert.AreEqual(expectedString, binding["1"].Value); 101 | } 102 | 103 | [Test] 104 | [TestCase("2021-06-10 16:17:18.0000000", "1623341838000000000 1440", "TIMESTAMP_TZ")] 105 | public void BuildParameters_DateTimeOffset(string stringValue, string expectedString, string bindingType) 106 | { 107 | var value = DateTimeOffset.ParseExact(stringValue, "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); 108 | 109 | var binding = ParameterBinder.BuildParameterBindings(value); 110 | 111 | Assert.AreEqual(1, binding.Count); 112 | Assert.IsTrue(binding.ContainsKey("1")); 113 | Assert.AreEqual(bindingType, binding["1"].Type); 114 | Assert.AreEqual(expectedString, binding["1"].Value); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Snowflake.Client.Tests/UnitTests/SnowflakeClientSettingsTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Snowflake.Client.Model; 3 | using System; 4 | 5 | namespace Snowflake.Client.Tests.UnitTests 6 | { 7 | [TestFixture] 8 | public class SnowflakeClientSettingsTest 9 | { 10 | [Test] 11 | [TestCase("user", "pw", "account")] 12 | public void AuthInfo_Ctor(string user, string password, string account) 13 | { 14 | var authInfo = new AuthInfo(user, password, account); 15 | var settings = new SnowflakeClientSettings(authInfo); 16 | 17 | Assert.AreEqual($"{account}.snowflakecomputing.com", settings.UrlInfo.Host); 18 | Assert.AreEqual("https", settings.UrlInfo.Protocol); 19 | Assert.AreEqual(443, settings.UrlInfo.Port); 20 | 21 | Assert.AreEqual(user, settings.AuthInfo.User); 22 | Assert.AreEqual(password, settings.AuthInfo.Password); 23 | Assert.AreEqual(account, settings.AuthInfo.Account); 24 | } 25 | 26 | [Test] 27 | [TestCase("user", "pw", "account")] 28 | public void AuthInfo_Props(string user, string password, string account) 29 | { 30 | var authInfo = new AuthInfo() { User = user, Password = password, Account = account }; 31 | var settings = new SnowflakeClientSettings(authInfo); 32 | 33 | Assert.AreEqual($"{account}.snowflakecomputing.com", settings.UrlInfo.Host); 34 | Assert.AreEqual("https", settings.UrlInfo.Protocol); 35 | Assert.AreEqual(443, settings.UrlInfo.Port); 36 | 37 | Assert.AreEqual(user, settings.AuthInfo.User); 38 | Assert.AreEqual(password, settings.AuthInfo.Password); 39 | Assert.AreEqual(account, settings.AuthInfo.Account); 40 | } 41 | 42 | [Test] 43 | public void AuthInfo_Ctor_AccountWithUnderscore() 44 | { 45 | var authInfo = new AuthInfo("user", "pw", "account_with_underscore"); 46 | var settings = new SnowflakeClientSettings(authInfo); 47 | 48 | Assert.AreEqual("account-with-underscore.snowflakecomputing.com", settings.UrlInfo.Host); 49 | } 50 | 51 | [Test] 52 | [TestCase("")] 53 | [TestCase(null)] 54 | public void AuthInfo_Ctor_EmptyAccount(string account) 55 | { 56 | var authInfo = new AuthInfo("user", "pw", account); 57 | 58 | Assert.Throws(() => 59 | { 60 | var snowflakeClientSettings = new SnowflakeClientSettings(authInfo); 61 | }); 62 | } 63 | 64 | [Test] 65 | [TestCase("west-us-2", "azure")] 66 | [TestCase("east-us-2", "azure")] 67 | [TestCase("us-gov-virginia", "azure")] 68 | [TestCase("canada-central", "azure")] 69 | [TestCase("west-europe", "azure")] 70 | [TestCase("switzerland-north", "azure")] 71 | [TestCase("southeast-asia", "azure")] 72 | [TestCase("australia-east", "azure")] 73 | [TestCase("us-east-2", "aws")] 74 | [TestCase("us-east-1-gov", "aws")] 75 | [TestCase("ca-central-1", "aws")] 76 | [TestCase("eu-west-2", "aws")] 77 | [TestCase("ap-northeast-1", "aws")] 78 | [TestCase("ap-south-1", "aws")] 79 | [TestCase("us-central1", "gcp")] 80 | [TestCase("europe-west2", "gcp")] 81 | [TestCase("europe-west4", "gcp")] 82 | public void AuthInfo_Ctor_Regions(string region, string expectedCloud) 83 | { 84 | var authInfo = new AuthInfo("user", "pw", "account", region); 85 | var settings = new SnowflakeClientSettings(authInfo); 86 | 87 | Assert.AreEqual($"account.{region}.{expectedCloud}.snowflakecomputing.com", settings.UrlInfo.Host); 88 | } 89 | 90 | [Test] 91 | [TestCase("us-east-1")] 92 | [TestCase("eu-west-1")] 93 | [TestCase("eu-central-1")] 94 | [TestCase("ap-southeast-1")] 95 | [TestCase("ap-southeast-2")] 96 | public void AuthInfo_Ctor_Unique_Regions(string region) 97 | { 98 | var authInfo = new AuthInfo("user", "pw", "account", region); 99 | var settings = new SnowflakeClientSettings(authInfo); 100 | 101 | Assert.AreEqual($"account.{region}.snowflakecomputing.com", settings.UrlInfo.Host); 102 | } 103 | 104 | [Test] 105 | [TestCase(null)] 106 | [TestCase("")] 107 | [TestCase("us-west-2")] 108 | public void AuthInfo_Ctor_Default_Region(string region) 109 | { 110 | var authInfo = new AuthInfo("user", "pw", "account", region); 111 | var settings = new SnowflakeClientSettings(authInfo); 112 | 113 | Assert.AreEqual($"account.snowflakecomputing.com", settings.UrlInfo.Host); 114 | } 115 | 116 | [Test] 117 | [TestCase("ca-central-1.aws")] 118 | [TestCase("europe-west4.gcp")] 119 | public void AuthInfo_Ctor_RegionWithCloud(string region) 120 | { 121 | var authInfo = new AuthInfo("user", "pw", "account", region); 122 | var settings = new SnowflakeClientSettings(authInfo); 123 | 124 | Assert.AreEqual($"account.{region}.snowflakecomputing.com", settings.UrlInfo.Host); 125 | } 126 | 127 | [Test] 128 | public void UrlInfo_Ctor() 129 | { 130 | var urlInfo = new UrlInfo("account-url.snowflakecomputing.com"); 131 | var settings = new SnowflakeClientSettings(new AuthInfo("user", "pw", "account"), null, urlInfo); 132 | 133 | Assert.AreEqual("account-url.snowflakecomputing.com", settings.UrlInfo.Host); 134 | Assert.AreEqual("https", settings.UrlInfo.Protocol); 135 | Assert.AreEqual(443, settings.UrlInfo.Port); 136 | } 137 | 138 | [Test] 139 | public void UrlInfo_Props() 140 | { 141 | var urlInfo = new UrlInfo() { Host = "account-url.snowflakecomputing.com" }; 142 | var settings = new SnowflakeClientSettings(new AuthInfo("user", "pw", "account"), null, urlInfo); 143 | 144 | Assert.AreEqual("account-url.snowflakecomputing.com", settings.UrlInfo.Host); 145 | Assert.AreEqual("https", settings.UrlInfo.Protocol); 146 | Assert.AreEqual(443, settings.UrlInfo.Port); 147 | } 148 | 149 | [Test] 150 | public void UrlInfo_Explicit_Host() 151 | { 152 | var authInfo = new AuthInfo("user", "pw", "account-auth"); 153 | var urlInfo = new UrlInfo("account-url.snowflakecomputing.com"); 154 | var settings = new SnowflakeClientSettings(authInfo, null, urlInfo); 155 | 156 | Assert.AreEqual("account-url.snowflakecomputing.com", settings.UrlInfo.Host); 157 | } 158 | 159 | [Test] 160 | public void UrlInfo_Explicit_Host_Account_WithUnderscore() 161 | { 162 | var authInfo = new AuthInfo("user", "pw", "account"); 163 | var urlInfo = new UrlInfo("account_with_underscore.snowflakecomputing.com"); 164 | var settings = new SnowflakeClientSettings(authInfo, null, urlInfo); 165 | 166 | Assert.AreEqual($"account-with-underscore.snowflakecomputing.com", settings.UrlInfo.Host); 167 | } 168 | 169 | [Test] 170 | [TestCase("https://account-1.snowflakecomputing.com", "https", 443)] 171 | [TestCase("http://account-2.snowflakecomputing.com", "http", 80)] 172 | public void UrlInfo_Uri(string url, string expectedProtocol, int expectedPort) 173 | { 174 | var authInfo = new AuthInfo("user", "pw", "account"); 175 | var urlInfo = new UrlInfo(new Uri(url)); 176 | var settings = new SnowflakeClientSettings(authInfo, null, urlInfo); 177 | 178 | Assert.AreEqual(url.Replace(expectedProtocol + "://", ""), settings.UrlInfo.Host); 179 | Assert.AreEqual(expectedPort, settings.UrlInfo.Port); 180 | Assert.AreEqual(expectedProtocol, settings.UrlInfo.Protocol); 181 | } 182 | } 183 | } -------------------------------------------------------------------------------- /Snowflake.Client.Tests/UnitTests/SnowflakeDataMapperTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Snowflake.Client.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Globalization; 6 | using System.Linq; 7 | 8 | namespace Snowflake.Client.Tests.UnitTests 9 | { 10 | [TestFixture] 11 | public class SnowflakeDataMapperTests 12 | { 13 | [Test] 14 | public void ResponseWithValues_MapTo_CustomClass() 15 | { 16 | var responseSample = GetFakeResponse(); 17 | var firstObjectRowset = new List>() { responseSample.RowSet[0] }; 18 | var mappedObjects = SnowflakeDataMapper.MapTo(responseSample.RowType, firstObjectRowset); 19 | var mappedObject = mappedObjects.FirstOrDefault(); 20 | 21 | Assert.IsNotNull(mappedObject); 22 | Assert.AreEqual("Sometext", mappedObject.StringProperty); 23 | Assert.AreEqual(true, mappedObject.BoolProperty); 24 | Assert.AreEqual(7, mappedObject.IntProperty); 25 | Assert.AreEqual(27.6F, mappedObject.FloatProperty); 26 | Assert.AreEqual(19.239834M, mappedObject.DecimalProperty); 27 | Assert.AreEqual(Guid.Parse("e7412bbf-88ee-4149-b341-101e0f72ec7c"), mappedObject.GuidProperty); 28 | Assert.AreEqual(new byte[] { 0, 128, 255 }, mappedObject.ByteArrayProperty); 29 | 30 | var dateTime = DateTime.ParseExact("2020-09-13 12:26:40.0000000", "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture); 31 | Assert.AreEqual(dateTime, mappedObject.DateTimeProperty); 32 | 33 | var dateTimeOffset = DateTimeOffset.ParseExact("2020-09-13 12:26:40.0000000", "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); 34 | Assert.AreEqual(dateTimeOffset, mappedObject.DateTimeOffsetProperty); 35 | } 36 | 37 | [Test] 38 | public void ResponseWithValues_MapTo_CustomClassWithNullableProps() 39 | { 40 | var responseSample = GetFakeResponse(); 41 | var firstObjectRowset = new List>() { responseSample.RowSet[0] }; 42 | var mappedObjects = SnowflakeDataMapper.MapTo(responseSample.RowType, firstObjectRowset); 43 | var mappedObject = mappedObjects.FirstOrDefault(); 44 | 45 | Assert.IsNotNull(mappedObject); 46 | Assert.AreEqual(true, mappedObject.BoolProperty); 47 | Assert.AreEqual(7, mappedObject.IntProperty); 48 | Assert.AreEqual(27.6F, mappedObject.FloatProperty); 49 | Assert.AreEqual(19.239834M, mappedObject.DecimalProperty); 50 | Assert.AreEqual(Guid.Parse("e7412bbf-88ee-4149-b341-101e0f72ec7c"), mappedObject.GuidProperty); 51 | 52 | var dateTime = DateTime.ParseExact("2020-09-13 12:26:40.0000000", "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture); 53 | Assert.AreEqual(dateTime, mappedObject.DateTimeProperty); 54 | 55 | var dateTimeOffset = DateTimeOffset.ParseExact("2020-09-13 12:26:40.0000000", "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); 56 | Assert.AreEqual(dateTimeOffset, mappedObject.DateTimeOffsetProperty); 57 | } 58 | 59 | [Test] 60 | public void ResponseWithStringNull_MapTo_CustomClassWithNullables() 61 | { 62 | var responseSample = GetFakeResponse(); 63 | 64 | var firstObjectRowset = new List>() { responseSample.RowSet[1] }; 65 | var mappedObjects = SnowflakeDataMapper.MapTo(responseSample.RowType, firstObjectRowset); 66 | var mappedObject = mappedObjects.FirstOrDefault(); 67 | 68 | Assert.IsNotNull(mappedObject); 69 | Assert.AreEqual(null, mappedObject.StringProperty); 70 | Assert.AreEqual(null, mappedObject.BoolProperty); 71 | Assert.AreEqual(null, mappedObject.IntProperty); 72 | Assert.AreEqual(null, mappedObject.FloatProperty); 73 | Assert.AreEqual(null, mappedObject.DecimalProperty); 74 | Assert.AreEqual(null, mappedObject.GuidProperty); 75 | Assert.AreEqual(null, mappedObject.DateTimeProperty); 76 | Assert.AreEqual(null, mappedObject.DateTimeOffsetProperty); 77 | Assert.AreEqual(null, mappedObject.ByteArrayProperty); 78 | } 79 | 80 | [Test] 81 | public void ResponseWithNull_MapTo_CustomClassWithNullables() 82 | { 83 | var responseSample = GetFakeResponse(); 84 | 85 | var firstObjectRowset = new List>() { responseSample.RowSet[2] }; 86 | var mappedObjects = SnowflakeDataMapper.MapTo(responseSample.RowType, firstObjectRowset); 87 | var mappedObject = mappedObjects.FirstOrDefault(); 88 | 89 | Assert.IsNotNull(mappedObject); 90 | Assert.AreEqual(null, mappedObject.StringProperty); 91 | Assert.AreEqual(null, mappedObject.BoolProperty); 92 | Assert.AreEqual(null, mappedObject.IntProperty); 93 | Assert.AreEqual(null, mappedObject.FloatProperty); 94 | Assert.AreEqual(null, mappedObject.DecimalProperty); 95 | Assert.AreEqual(null, mappedObject.GuidProperty); 96 | Assert.AreEqual(null, mappedObject.DateTimeProperty); 97 | Assert.AreEqual(null, mappedObject.DateTimeOffsetProperty); 98 | Assert.AreEqual(null, mappedObject.ByteArrayProperty); 99 | } 100 | 101 | [Test] 102 | public void ResponseWithValues_MapTo_SingleValue() 103 | { 104 | var responseSample = GetFakeResponse(); 105 | var rowSet = responseSample.RowSet[0]; 106 | var rowType = responseSample.RowType; 107 | 108 | var stringValue = SnowflakeDataMapper.MapTo(rowType[0], rowSet[0]); 109 | Assert.AreEqual("Sometext", stringValue); 110 | 111 | var boolValue = SnowflakeDataMapper.MapTo(rowType[1], rowSet[1]); 112 | Assert.AreEqual(true, boolValue); 113 | 114 | var intValue = SnowflakeDataMapper.MapTo(rowType[2], rowSet[2]); 115 | Assert.AreEqual(7, intValue); 116 | 117 | var floatValue = SnowflakeDataMapper.MapTo(rowType[3], rowSet[3]); 118 | Assert.AreEqual(27.6F, floatValue); 119 | 120 | var decimalValue = SnowflakeDataMapper.MapTo(rowType[4], rowSet[4]); 121 | Assert.AreEqual(19.239834M, decimalValue); 122 | 123 | var dateTimeExpected = DateTime.ParseExact("2020-09-13 12:26:40.0000000", "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture); 124 | var dateTimeValue = SnowflakeDataMapper.MapTo(rowType[5], rowSet[5]); 125 | Assert.AreEqual(dateTimeExpected, dateTimeValue); 126 | 127 | var dateTimeOffsetExpected = DateTimeOffset.ParseExact("2020-09-13 12:26:40.0000000", "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); 128 | var dateTimeOffsetValue = SnowflakeDataMapper.MapTo(rowType[6], rowSet[6]); 129 | Assert.AreEqual(dateTimeOffsetExpected, dateTimeOffsetValue); 130 | 131 | var guidValue = SnowflakeDataMapper.MapTo(rowType[7], rowSet[7]); 132 | Assert.AreEqual(Guid.Parse("e7412bbf-88ee-4149-b341-101e0f72ec7c"), guidValue); 133 | 134 | var bytesValues = SnowflakeDataMapper.MapTo(rowType[8], rowSet[8]); 135 | Assert.AreEqual(new byte[] { 0, 128, 255 }, bytesValues); 136 | } 137 | 138 | [Test] 139 | public void ResponseWithStringNull_MapTo_SingleValueNullable() 140 | { 141 | var responseSample = GetFakeResponse(); 142 | var rowSet = responseSample.RowSet[1]; 143 | var rowType = responseSample.RowType; 144 | 145 | var boolValue = SnowflakeDataMapper.MapTo(rowType[1], rowSet[1]); 146 | Assert.AreEqual(null, boolValue); 147 | 148 | var intValue = SnowflakeDataMapper.MapTo(rowType[2], rowSet[2]); 149 | Assert.AreEqual(null, intValue); 150 | 151 | var floatValue = SnowflakeDataMapper.MapTo(rowType[3], rowSet[3]); 152 | Assert.AreEqual(null, floatValue); 153 | 154 | var decimalValue = SnowflakeDataMapper.MapTo(rowType[4], rowSet[4]); 155 | Assert.AreEqual(null, decimalValue); 156 | 157 | var dateTimeValue = SnowflakeDataMapper.MapTo(rowType[5], rowSet[5]); 158 | Assert.AreEqual(null, dateTimeValue); 159 | 160 | var dateTimeOffsetValue = SnowflakeDataMapper.MapTo(rowType[6], rowSet[6]); 161 | Assert.AreEqual(null, dateTimeOffsetValue); 162 | 163 | var guidValue = SnowflakeDataMapper.MapTo(rowType[7], rowSet[7]); 164 | Assert.AreEqual(null, guidValue); 165 | } 166 | 167 | [Test] 168 | public void ResponseWithNull_MapTo_SingleValueNullable() 169 | { 170 | var responseSample = GetFakeResponse(); 171 | var rowSet = responseSample.RowSet[2]; 172 | var rowType = responseSample.RowType; 173 | 174 | var boolValue = SnowflakeDataMapper.MapTo(rowType[1], rowSet[1]); 175 | Assert.AreEqual(null, boolValue); 176 | 177 | var intValue = SnowflakeDataMapper.MapTo(rowType[2], rowSet[2]); 178 | Assert.AreEqual(null, intValue); 179 | 180 | var floatValue = SnowflakeDataMapper.MapTo(rowType[3], rowSet[3]); 181 | Assert.AreEqual(null, floatValue); 182 | 183 | var decimalValue = SnowflakeDataMapper.MapTo(rowType[4], rowSet[4]); 184 | Assert.AreEqual(null, decimalValue); 185 | 186 | var dateTimeValue = SnowflakeDataMapper.MapTo(rowType[5], rowSet[5]); 187 | Assert.AreEqual(null, dateTimeValue); 188 | 189 | var dateTimeOffsetValue = SnowflakeDataMapper.MapTo(rowType[6], rowSet[6]); 190 | Assert.AreEqual(null, dateTimeOffsetValue); 191 | 192 | var guidValue = SnowflakeDataMapper.MapTo(rowType[7], rowSet[7]); 193 | Assert.AreEqual(null, guidValue); 194 | } 195 | 196 | private QueryExecResponseData GetFakeResponse() 197 | { 198 | var response = new QueryExecResponseData() { RowType = new List(), RowSet = new List>() }; 199 | 200 | response.RowType.Add(new ColumnDescription() { Name = "StringProperty", Type = "text" }); 201 | response.RowType.Add(new ColumnDescription() { Name = "BoolProperty", Type = "boolean" }); 202 | response.RowType.Add(new ColumnDescription() { Name = "IntProperty", Type = "fixed" }); 203 | response.RowType.Add(new ColumnDescription() { Name = "FloatProperty", Type = "real" }); 204 | response.RowType.Add(new ColumnDescription() { Name = "DecimalProperty", Type = "real" }); 205 | response.RowType.Add(new ColumnDescription() { Name = "DateTimeProperty", Type = "timestamp_ntz" }); 206 | response.RowType.Add(new ColumnDescription() { Name = "DateTimeOffsetProperty", Type = "timestamp_ltz" }); 207 | response.RowType.Add(new ColumnDescription() { Name = "GuidProperty", Type = "text" }); 208 | response.RowType.Add(new ColumnDescription() { Name = "ByteArrayProperty", Type = "binary" }); 209 | 210 | response.RowSet.Add(new List() 211 | { 212 | "Sometext", 213 | "true", 214 | "7", 215 | "27.6", 216 | "19.239834", 217 | "1600000000.000000000", 218 | "1600000000.000000000", 219 | "e7412bbf-88ee-4149-b341-101e0f72ec7c", 220 | "0080ff" 221 | }); 222 | 223 | response.RowSet.Add(new List() 224 | { 225 | "null", 226 | "null", 227 | "null", 228 | "null", 229 | "null", 230 | "null", 231 | "null", 232 | "null", 233 | "null" 234 | }); 235 | 236 | 237 | response.RowSet.Add(new List() 238 | { 239 | null, 240 | null, 241 | null, 242 | null, 243 | null, 244 | null, 245 | null, 246 | null, 247 | null 248 | }); 249 | 250 | return response; 251 | } 252 | 253 | private class CustomClass 254 | { 255 | public string StringProperty { get; set; } 256 | 257 | public bool BoolProperty { get; set; } 258 | 259 | public int IntProperty { get; set; } 260 | 261 | public float FloatProperty { get; set; } 262 | 263 | public decimal DecimalProperty { get; set; } 264 | 265 | public DateTime DateTimeProperty { get; set; } 266 | 267 | public DateTimeOffset DateTimeOffsetProperty { get; set; } 268 | 269 | public Guid GuidProperty { get; set; } 270 | 271 | public byte[] ByteArrayProperty { get; set; } 272 | } 273 | 274 | private class CustomClassNullables 275 | { 276 | public string StringProperty { get; set; } 277 | 278 | public bool? BoolProperty { get; set; } 279 | 280 | public int? IntProperty { get; set; } 281 | 282 | public float? FloatProperty { get; set; } 283 | 284 | public decimal? DecimalProperty { get; set; } 285 | 286 | public DateTime? DateTimeProperty { get; set; } 287 | 288 | public DateTimeOffset? DateTimeOffsetProperty { get; set; } 289 | 290 | public Guid? GuidProperty { get; set; } 291 | 292 | public byte[] ByteArrayProperty { get; set; } 293 | } 294 | } 295 | } -------------------------------------------------------------------------------- /Snowflake.Client.Tests/UnitTests/SnowflakeTypesConverterTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Globalization; 4 | 5 | namespace Snowflake.Client.Tests.UnitTests 6 | { 7 | [TestFixture] 8 | public class SnowflakeTypesConverterTest 9 | { 10 | [Test] 11 | [TestCase("4133980799.999999900", "2100-12-31 23:59:59.9999999")] 12 | [TestCase("7258159353.445566700", "2200-01-01 11:22:33.4455667")] 13 | [TestCase("253402300799.999999900", "9999-12-31 23:59:59.9999999")] 14 | [TestCase("380218800.666666600", "1982-01-18 16:20:00.6666666")] 15 | public void SnowflakeTimestampNtz_ToDateTime(string sfTimestampNtz, string expectedDateTimeString) 16 | { 17 | var expectedDateTime = DateTime.ParseExact(expectedDateTimeString, "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture); 18 | var convertedResult = SnowflakeTypesConverter.ConvertToDateTime(sfTimestampNtz, "timestamp_ntz"); 19 | 20 | Assert.AreEqual(expectedDateTime, convertedResult); 21 | } 22 | 23 | [Test] 24 | [TestCase("4133980799.999999900", "2100-12-31 23:59:59.9999999")] 25 | [TestCase("7258159353.445566700", "2200-01-01 11:22:33.4455667")] 26 | [TestCase("253402300799.999999900", "9999-12-31 23:59:59.9999999")] 27 | [TestCase("380218800.666666600", "1982-01-18 16:20:00.6666666")] 28 | public void SnowflakeTime_ToDateTime(string sfTime, string expectedDateTimeString) 29 | { 30 | var expectedDateTime = DateTime.ParseExact(expectedDateTimeString, "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture); 31 | var convertedResult = SnowflakeTypesConverter.ConvertToDateTime(sfTime, "time"); // same conversion as for "timestamp_ntz" 32 | 33 | Assert.AreEqual(expectedDateTime, convertedResult); 34 | } 35 | 36 | [Test] 37 | [TestCase("47846", "2100-12-31")] 38 | [TestCase("84006", "2200-01-01")] 39 | [TestCase("2932896", "9999-12-31")] 40 | [TestCase("4400", "1982-01-18")] 41 | public void SnowflakeDate_ToDateTime(string sfDate, string expectedDateTimeString) 42 | { 43 | var expectedDateTime = DateTime.ParseExact(expectedDateTimeString, "yyyy-MM-dd", CultureInfo.InvariantCulture); 44 | var convertedResult = SnowflakeTypesConverter.ConvertToDateTime(sfDate, "date"); 45 | 46 | Assert.AreEqual(expectedDateTime, convertedResult); 47 | } 48 | 49 | [Test] 50 | [TestCase("2100-12-31 23:59:59.9999999", "4133980799999999900")] 51 | [TestCase("2200-01-01 11:22:33.4455667", "7258159353445566700")] 52 | [TestCase("9999-12-31 23:59:59.9999999", "253402300799999999900")] 53 | [TestCase("1982-01-18 16:20:00.6666666", "380218800666666600")] 54 | public void DateTime_ToSnowflakeTimestampNtz(string dateTimeString, string expectedSnowflakeTimestampNtz) 55 | { 56 | var dateTime = DateTime.ParseExact(dateTimeString, "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture); 57 | var convertedResult = SnowflakeTypesConverter.ConvertToTimestampNtz(dateTime); 58 | 59 | Assert.AreEqual(expectedSnowflakeTimestampNtz, convertedResult); 60 | } 61 | 62 | [Test] 63 | [TestCase("2100-12-31 23:59:59.9999999", "4133980799999999900 1440")] 64 | [TestCase("2200-01-01 11:22:33.4455667", "7258159353445566700 1440")] 65 | [TestCase("9999-12-31 23:59:59.9999999", "253402300799999999900 1440")] 66 | [TestCase("1982-01-18 16:20:00.6666666", "380218800666666600 1440")] 67 | public void DateTimeOffset_ToSnowflakeTimestampTz(string dateTimeString, string expectedSnowflakeTimestampTz) 68 | { 69 | var dateTimeOffsetUtc = DateTimeOffset.ParseExact(dateTimeString, "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); 70 | var convertedResult = SnowflakeTypesConverter.ConvertToTimestampTz(dateTimeOffsetUtc); 71 | 72 | Assert.AreEqual(expectedSnowflakeTimestampTz, convertedResult); 73 | } 74 | 75 | [Test] 76 | [TestCase(0, 128, 255, "0080ff")] 77 | [TestCase(10, 11, 12, "0a0b0c")] 78 | public void ConvertBytesToHex(byte b1, byte b2, byte b3, string hexExpected) 79 | { 80 | var hexResult = SnowflakeTypesConverter.BytesToHex(new byte[] { b1, b2, b3 }); 81 | Assert.AreEqual(hexExpected, hexResult); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Snowflake.Client.Tests/testconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "connection": { 3 | "user": "username", 4 | "password": "password", 5 | "account": "account", 6 | "region": "eu-central-1", 7 | "warehouse": "COMPUTE_WH", 8 | "database": "TPCH_SF1", 9 | "schema": "SNOWFLAKE_SAMPLE_DATA", 10 | "role": "SYSADMIN" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Snowflake.Client/ChunksDownloader.cs: -------------------------------------------------------------------------------- 1 | using Snowflake.Client.Json; 2 | using System; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Text; 10 | using System.Text.Json; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | using Snowflake.Client.Extensions; 14 | using Snowflake.Client.Model; 15 | 16 | namespace Snowflake.Client 17 | { 18 | public static class ChunksDownloader 19 | { 20 | private const string SSE_C_ALGORITHM = "x-amz-server-side-encryption-customer-algorithm"; 21 | private const string SSE_C_KEY = "x-amz-server-side-encryption-customer-key"; 22 | private const string SSE_C_AES = "AES256"; 23 | 24 | private static int _prefetchThreadsCount = 4; 25 | private static readonly HttpClient Client; 26 | 27 | static ChunksDownloader() 28 | { 29 | var httpClientHandler = new HttpClientHandler 30 | { 31 | AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate 32 | }; 33 | 34 | Client = new HttpClient(httpClientHandler) 35 | { 36 | Timeout = TimeSpan.FromHours(1) 37 | }; 38 | } 39 | 40 | public static void Configure(ChunksDownloaderOptions options) 41 | { 42 | if (options.PrefetchThreadsCount >= 1 && options.PrefetchThreadsCount <= 10) 43 | { 44 | _prefetchThreadsCount = options.PrefetchThreadsCount; 45 | } 46 | } 47 | 48 | public static async Task>> DownloadAndParseChunksAsync(ChunksDownloadInfo chunksDownloadInfo, CancellationToken ct = default) 49 | { 50 | var chunkHeaders = chunksDownloadInfo.ChunkHeaders; 51 | var chunksQrmk = chunksDownloadInfo.Qrmk; 52 | var downloadRequests = chunksDownloadInfo.Chunks.Select(c => BuildChunkDownloadRequest(c, chunkHeaders, chunksQrmk)).ToArray(); 53 | 54 | var downloadedChunks = new ConcurrentBag(); 55 | await downloadRequests.ForEachWithThrottleAsync(async request => 56 | { 57 | var chunkRowSet = await GetChunkContentAsync(request, ct).ConfigureAwait(false); 58 | var chunkIndex = Array.IndexOf(downloadRequests, request); 59 | downloadedChunks.Add(new DownloadedChunkRowSet(request.RequestUri, chunkIndex, chunkRowSet)); 60 | }, _prefetchThreadsCount) 61 | .ConfigureAwait(false); 62 | 63 | var totalRowSet = downloadedChunks.OrderBy(c => c.ChunkIndex).SelectMany(c => c.ChunkRowSet).ToList(); 64 | return totalRowSet; 65 | } 66 | 67 | [Obsolete("Use DownloadAndParseChunksAsync instead")] 68 | public static async Task>> DownloadAndParseChunksSingleThreadAsync(ChunksDownloadInfo chunksDownloadInfo, CancellationToken ct = default) 69 | { 70 | var rowSet = new List>(); 71 | 72 | foreach (var chunk in chunksDownloadInfo.Chunks) 73 | { 74 | var downloadRequest = BuildChunkDownloadRequest(chunk, chunksDownloadInfo.ChunkHeaders, chunksDownloadInfo.Qrmk); 75 | var chunkRowSet = await GetChunkContentAsync(downloadRequest, ct).ConfigureAwait(false); 76 | 77 | rowSet.AddRange(chunkRowSet); 78 | } 79 | 80 | return rowSet; 81 | } 82 | 83 | private static HttpRequestMessage BuildChunkDownloadRequest(ExecResponseChunk chunk, Dictionary chunkHeaders, string qrmk) 84 | { 85 | var request = new HttpRequestMessage 86 | { 87 | Method = HttpMethod.Get, 88 | RequestUri = new Uri(chunk.Url) 89 | }; 90 | 91 | if (chunkHeaders != null) 92 | { 93 | foreach (var header in chunkHeaders) 94 | { 95 | request.Headers.Add(header.Key, header.Value); 96 | } 97 | } 98 | else 99 | { 100 | request.Headers.Add(SSE_C_ALGORITHM, SSE_C_AES); 101 | request.Headers.Add(SSE_C_KEY, qrmk); 102 | } 103 | 104 | return request; 105 | } 106 | 107 | private static async Task>> GetChunkContentAsync(HttpRequestMessage request, CancellationToken ct = default) 108 | { 109 | using (var response = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false)) 110 | { 111 | response.EnsureSuccessStatusCode(); 112 | 113 | using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) 114 | { 115 | var concatStream = BuildDeserializableStream(stream); 116 | return await JsonSerializer.DeserializeAsync>>(concatStream, cancellationToken: ct).ConfigureAwait(false); 117 | } 118 | } 119 | } 120 | 121 | /// 122 | /// Content from AWS S3 in format of 123 | /// ["val1", "val2", null, ...], 124 | /// ["val3", "val4", null, ...], 125 | /// ... 126 | /// To parse it as a json (array of strings), we need to pre-append '[' and append ']' to the stream 127 | /// 128 | private static Stream BuildDeserializableStream(Stream content) 129 | { 130 | Stream openBracket = new MemoryStream(Encoding.UTF8.GetBytes("[")); 131 | Stream closeBracket = new MemoryStream(Encoding.UTF8.GetBytes("]")); 132 | 133 | return new ConcatenatedStream(new Stream[] { openBracket, content, closeBracket }); 134 | } 135 | } 136 | 137 | public class ChunksDownloaderOptions 138 | { 139 | /// 140 | /// Sets threads count which will be used to download response data chunks. 141 | /// See PREFETCH_THREADS_COUNT client variable in SF documentation. 142 | /// Valid values are: 1 - 10. 143 | /// Default value: 4. 144 | /// 145 | public int PrefetchThreadsCount { get; set; } 146 | } 147 | } -------------------------------------------------------------------------------- /Snowflake.Client/ConcatenatedStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace Snowflake.Client 6 | { 7 | /// 8 | /// Used to merge multiple streams into one 9 | /// 10 | internal class ConcatenatedStream : Stream 11 | { 12 | private readonly Queue _streams; 13 | 14 | public ConcatenatedStream(IEnumerable streams) 15 | { 16 | _streams = new Queue(streams); 17 | } 18 | 19 | public override bool CanRead => true; 20 | 21 | public override bool CanSeek => false; 22 | 23 | public override bool CanWrite => false; 24 | 25 | public override int Read(byte[] buffer, int offset, int count) 26 | { 27 | if (_streams.Count == 0) 28 | return 0; 29 | 30 | var bytesRead = _streams.Peek().Read(buffer, offset, count); 31 | if (bytesRead == 0) 32 | { 33 | _streams.Dequeue().Dispose(); 34 | bytesRead += Read(buffer, offset, count); 35 | } 36 | return bytesRead; 37 | } 38 | 39 | public override void Flush() 40 | { 41 | throw new NotImplementedException(); 42 | } 43 | 44 | public override long Length => throw new NotImplementedException(); 45 | 46 | public override long Position 47 | { 48 | get => throw new NotImplementedException(); 49 | set => throw new NotImplementedException(); 50 | } 51 | 52 | public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); 53 | 54 | public override void SetLength(long value) => throw new NotImplementedException(); 55 | 56 | public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Snowflake.Client/DownloadedChunkRowSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Snowflake.Client 5 | { 6 | public class DownloadedChunkRowSet 7 | { 8 | public Uri ChunkUrl { get; } 9 | public int ChunkIndex { get; } 10 | public List> ChunkRowSet { get; } 11 | public string ChunkName { get; } 12 | 13 | public DownloadedChunkRowSet(Uri chunkUrl, int chunkIndex, List> chunkRowSet) 14 | { 15 | ChunkUrl = chunkUrl; 16 | ChunkIndex = chunkIndex; 17 | ChunkRowSet = chunkRowSet; 18 | ChunkName = chunkUrl.Segments.Length >= 5 ? chunkUrl.Segments[5] : "Unknown"; 19 | } 20 | 21 | public override string ToString() 22 | { 23 | return $"Index: {ChunkIndex}, Name: {ChunkName}, Rows Count: {ChunkRowSet?.Count}"; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /Snowflake.Client/Extensions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Snowflake.Client.Extensions 7 | { 8 | public static class EnumerableExtensions 9 | { 10 | public static async Task ForEachWithThrottleAsync(this IEnumerable items, Func function, int degreeOfParallelism) 11 | { 12 | var tasks = new List(); 13 | using (var semaphoreSlim = new SemaphoreSlim(degreeOfParallelism)) 14 | { 15 | foreach (var item in items) 16 | { 17 | await semaphoreSlim.WaitAsync().ConfigureAwait(false); 18 | var task = Task.Run(async () => 19 | { 20 | try 21 | { 22 | await function(item).ConfigureAwait(false); 23 | } 24 | finally 25 | { 26 | semaphoreSlim.Release(); 27 | } 28 | }); 29 | tasks.Add(task); 30 | } 31 | 32 | await Task.WhenAll(tasks).ConfigureAwait(false); 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /Snowflake.Client/Helpers/HexUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Snowflake.Client.Helpers 5 | { 6 | internal class HexUtils 7 | { 8 | const char __base64PaddingChar = '='; 9 | 10 | // The base64 encoding table. 11 | private static readonly char[] __base64Table = 12 | { 13 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 14 | 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 15 | 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 16 | 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' 17 | }; 18 | 19 | private static readonly int[] __hexValueTable = new int[256]; 20 | 21 | static HexUtils() 22 | { 23 | // Build hex character to value lookup table. 24 | // Init all elements to -1 to begin with. 25 | for(int i = 0; i < __hexValueTable.Length; i++) 26 | __hexValueTable[i] = -1; 27 | 28 | // Set the mappings for the hex characters (upper and lower case). 29 | for(char c = '0'; c <= '9'; c++) __hexValueTable[c] = c - '0'; 30 | for(char c = 'A'; c <= 'F'; c++) __hexValueTable[c] = c - 'A' + 10; 31 | for(char c = 'a'; c <= 'f'; c++) __hexValueTable[c] = c - 'a' + 10; 32 | } 33 | 34 | public static void HexToBase64(string hex, TextWriter tw) 35 | { 36 | if(hex.Length % 2 != 0) 37 | throw new ArgumentException("The hexadecimal string cannot have an odd length."); 38 | 39 | // Get a span over the input string, for fast/efficient processing of the characters. 40 | ReadOnlySpan hexSpan = hex.AsSpan(); 41 | 42 | // Allocate temp storage for decoded hex chars, and encoded base64 characters. 43 | Span inBytes = stackalloc byte[3]; 44 | char[] outChars = new char[4]; 45 | 46 | // Loop over hexSpan in six character chunks (6 hex chars represents 3 bytes). 47 | while(hexSpan.Length >= 6) 48 | { 49 | // Decode 6 hex chars to 3 bytes. 50 | inBytes[0] = HexToByte(hexSpan); 51 | inBytes[1] = HexToByte(hexSpan.Slice(2)); 52 | inBytes[2] = HexToByte(hexSpan.Slice(4)); 53 | 54 | // Encode the three bytes as a base64 block (3 bytes becomes 4 base64 characters). 55 | EncodeBase64Block(inBytes, outChars); 56 | 57 | // Write the base64 chars to the string builder. 58 | tw.Write(outChars); 59 | 60 | // Move hexSpan forward six chars. 61 | hexSpan = hexSpan.Slice(6); 62 | } 63 | 64 | // Handle any remaining hex chars / bytes. I.e., when hex.Length is not a multiple of 6. 65 | switch(hexSpan.Length) 66 | { 67 | case 0: 68 | // No more hex chars; exit. 69 | break; 70 | 71 | case 2: 72 | // Two hex chars remaining (i.e., one byte). 73 | inBytes[0] = HexToByte(hexSpan); 74 | inBytes[1] = 0; 75 | inBytes[2] = 0; 76 | EncodeBase64Block(inBytes, outChars); 77 | outChars[2] = __base64PaddingChar; 78 | outChars[3] = __base64PaddingChar; 79 | tw.Write(outChars); 80 | break; 81 | 82 | case 4: 83 | // Four hex chars remaining (i.e., two bytes). 84 | inBytes[0] = HexToByte(hexSpan); 85 | inBytes[1] = HexToByte(hexSpan.Slice(2)); 86 | inBytes[2] = 0; 87 | EncodeBase64Block(inBytes, outChars); 88 | outChars[3] = __base64PaddingChar; 89 | tw.Write(outChars); 90 | break; 91 | } 92 | } 93 | 94 | private static byte HexToByte(ReadOnlySpan hexSpan) 95 | { 96 | return (byte)(GetHexValue(hexSpan[0]) << 4 | GetHexValue(hexSpan[1])); 97 | } 98 | 99 | private static int GetHexValue(char hexCharacter) 100 | { 101 | if(hexCharacter > 255) 102 | throw new ArgumentException("Invalid hexadecimal character."); 103 | 104 | // Lookup the character's value. 105 | int value = __hexValueTable[hexCharacter]; 106 | 107 | // If the value is -1 then the character is not a hexadecimal character. 108 | if(value >= 0) 109 | return value; 110 | 111 | throw new ArgumentException("Invalid hexadecimal character."); 112 | } 113 | 114 | private static void EncodeBase64Block(ReadOnlySpan inBytes, Span outChars) 115 | { 116 | // Encode the three bytes as a base64 block (3 bytes becomes 4 base64 characters). 117 | outChars[0] = __base64Table[(inBytes[0] & 0xfc) >> 2]; 118 | outChars[1] = __base64Table[((inBytes[0] & 0x03) << 4) | ((inBytes[1] & 0xf0) >> 4)]; 119 | outChars[2] = __base64Table[((inBytes[1] & 0x0f) << 2) | ((inBytes[2] & 0xc0) >> 6)]; 120 | outChars[3] = __base64Table[inBytes[2] & 0x3f]; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Snowflake.Client/Helpers/QuotedNumbersToIntConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Buffers.Text; 4 | using System.Text.Json; 5 | using System.Text.Json.Serialization; 6 | 7 | namespace Snowflake.Client.Helpers 8 | { 9 | /// 10 | /// Allows to deserialize quoted numbers (i.e. strings) into int values 11 | /// 12 | internal class QuotedNumbersToIntConverter : JsonConverter 13 | { 14 | public override int? Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) 15 | { 16 | if (reader.TokenType != JsonTokenType.String) 17 | return reader.GetInt32(); 18 | 19 | var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; 20 | if (Utf8Parser.TryParse(span, out int number, out var bytesConsumed) && span.Length == bytesConsumed) 21 | return number; 22 | 23 | return reader.GetInt32(); 24 | } 25 | 26 | public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options) 27 | { 28 | writer.WriteStringValue(value.ToString()); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Snowflake.Client/Helpers/SnowflakeUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Snowflake.Client.Json; 5 | using Snowflake.Client.Model; 6 | 7 | namespace Snowflake.Client.Helpers 8 | { 9 | internal static class SnowflakeUtils 10 | { 11 | // Based on https://github.com/snowflakedb/snowflake-connector-net/blob/master/Snowflake.Data/Core/ResultSetUtil.cs 12 | internal static long GetAffectedRowsCount(QueryExecResponse response) 13 | { 14 | var statementTypeId = response.Data.StatementTypeId; 15 | 16 | if (!Enum.IsDefined(typeof(SnowflakeStatementType), statementTypeId)) 17 | return 0; 18 | 19 | long updateCount = 0; 20 | var statementType = (SnowflakeStatementType)statementTypeId; 21 | switch (statementType) 22 | { 23 | case SnowflakeStatementType.INSERT: 24 | case SnowflakeStatementType.UPDATE: 25 | case SnowflakeStatementType.DELETE: 26 | case SnowflakeStatementType.MERGE: 27 | case SnowflakeStatementType.MULTI_INSERT: 28 | updateCount = response.Data.RowSet[0].Sum(cell => long.Parse(cell)); 29 | break; 30 | 31 | case SnowflakeStatementType.COPY: 32 | var rowsLoadedColumn = response.Data.RowType.FirstOrDefault(c => c.Name == "rows_loaded"); 33 | if (rowsLoadedColumn != null) 34 | { 35 | var rowsLoadedColumnIndex = response.Data.RowType.IndexOf(rowsLoadedColumn); 36 | updateCount = long.Parse(response.Data.RowSet[0][rowsLoadedColumnIndex]); 37 | } 38 | 39 | break; 40 | 41 | case SnowflakeStatementType.COPY_UNLOAD: 42 | var rowsUnloadedColumn = response.Data.RowType.FirstOrDefault(c => c.Name == "rows_unloaded"); 43 | if (rowsUnloadedColumn != null) 44 | { 45 | var rowsUnloadedColumnIndex = response.Data.RowType.IndexOf(rowsUnloadedColumn); 46 | updateCount = long.Parse(response.Data.RowSet[0][rowsUnloadedColumnIndex]); 47 | } 48 | 49 | break; 50 | 51 | case SnowflakeStatementType.SELECT: 52 | updateCount = -1; 53 | break; 54 | 55 | default: 56 | updateCount = 0; 57 | break; 58 | } 59 | 60 | return updateCount; 61 | } 62 | 63 | // Based on: https://docs.snowflake.com/en/user-guide/admin-account-identifier.html 64 | // Seacrh for table "Non-VPS Account Locator Formats by Cloud Platform and Region" 65 | internal static string GetCloudTagByRegion(string region) 66 | { 67 | if (string.IsNullOrEmpty(region)) 68 | return ""; 69 | 70 | // User can pass "us-east-2.aws" 71 | if (region.Contains(".")) 72 | return ""; 73 | 74 | var regionTags = new Dictionary 75 | { 76 | { "us-west-2", "" }, // "default" one - US West (Oregon) 77 | { "us-gov-west-1", "aws" }, 78 | { "us-east-2", "aws" }, 79 | { "us-east-1", "" }, 80 | { "us-east-1-gov", "aws" }, 81 | { "ca-central-1", "aws" }, 82 | { "sa-east-1", "aws" }, 83 | { "eu-west-1", "" }, 84 | { "eu-west-2", "aws" }, 85 | { "eu-west-3", "aws" }, 86 | { "eu-central-1", "" }, 87 | { "eu-north-1", "aws" }, 88 | { "ap-northeast-1", "aws" }, 89 | { "ap-northeast-3", "aws" }, 90 | { "ap-northeast-2", "aws" }, 91 | { "ap-south-1", "aws" }, 92 | { "ap-southeast-1", "" }, 93 | { "ap-southeast-2", "" }, 94 | { "us-central1", "gcp" }, 95 | { "us-east4", "gcp" }, 96 | { "europe-west2", "gcp" }, 97 | { "europe-west4", "gcp" }, 98 | { "west-us-2", "azure" }, 99 | { "central-us", "azure" }, 100 | { "south-central-us", "azure" }, 101 | { "east-us-2", "azure" }, 102 | { "us-gov-virginia", "azure" }, 103 | { "canada-central", "azure" }, 104 | { "uk-south", "azure" }, 105 | { "north-europe", "azure" }, 106 | { "west-europe", "azure" }, 107 | { "switzerland-north", "azure" }, 108 | { "uae-north", "azure" }, 109 | { "central-india", "azure" }, 110 | { "japan-east", "azure" }, 111 | { "southeast-asia", "azure" }, 112 | { "australia-east", "azure" } 113 | }; 114 | 115 | regionTags.TryGetValue(region, out var cloudTag); 116 | return cloudTag ?? ""; 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /Snowflake.Client/ISnowflakeClient.cs: -------------------------------------------------------------------------------- 1 | using Snowflake.Client.Model; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Snowflake.Client 8 | { 9 | public interface ISnowflakeClient 10 | { 11 | Task CancelQueryAsync(string requestId, CancellationToken ct = default); 12 | Task CloseSessionAsync(CancellationToken ct = default); 13 | Task ExecuteAsync(string sql, object sqlParams = null, CancellationToken ct = default); 14 | Task ExecuteScalarAsync(string sql, object sqlParams = null, CancellationToken ct = default); 15 | Task InitNewSessionAsync(CancellationToken ct = default); 16 | Task> QueryAsync(string sql, object sqlParams = null, CancellationToken ct = default); 17 | Task QueryRawResponseAsync(string sql, object sqlParams = null, bool describeOnly = false, CancellationToken ct = default); 18 | Task RenewSessionAsync(CancellationToken ct = default); 19 | void SetHttpClient(HttpClient httpClient); 20 | } 21 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/BaseRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Snowflake.Client.Json 2 | { 3 | public abstract class BaseRequest 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Snowflake.Client/Json/BaseResponse.cs: -------------------------------------------------------------------------------- 1 | using Snowflake.Client.Helpers; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Snowflake.Client.Json 5 | { 6 | public abstract class BaseResponse 7 | { 8 | public string Message { get; set; } 9 | public bool Success { get; set; } 10 | public string Headers { get; set; } 11 | 12 | [JsonConverter(typeof(QuotedNumbersToIntConverter))] 13 | public int? Code { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/CloseResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Snowflake.Client.Json 2 | { 3 | public class CloseResponse : BaseResponse 4 | { 5 | public object Data { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Snowflake.Client/Json/ColumnDescription.cs: -------------------------------------------------------------------------------- 1 | namespace Snowflake.Client.Json 2 | { 3 | public class ColumnDescription 4 | { 5 | public string Name { get; set; } 6 | public long? ByteLength { get; set; } 7 | public long? Length { get; set; } 8 | public string Type { get; set; } 9 | public long? Scale { get; set; } 10 | public long? Precision { get; set; } 11 | public bool Nullable { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/ExecResponseChunk.cs: -------------------------------------------------------------------------------- 1 | namespace Snowflake.Client.Json 2 | { 3 | public class ExecResponseChunk 4 | { 5 | public string Url { get; set; } 6 | public int RowCount { get; set; } 7 | public long UncompressedSize { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/LoginRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Snowflake.Client.Json 4 | { 5 | public class LoginRequest : BaseRequest 6 | { 7 | [JsonPropertyName("data")] 8 | public LoginRequestData Data { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/LoginRequestClientEnv.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Snowflake.Client.Json 4 | { 5 | public class LoginRequestClientEnv 6 | { 7 | [JsonPropertyName("APPLICATION")] 8 | public string Application { get; set; } 9 | 10 | [JsonPropertyName("OS_VERSION")] 11 | public string OSVersion { get; set; } 12 | 13 | [JsonPropertyName("NET_RUNTIME")] 14 | public string NETRuntime { get; set; } 15 | 16 | [JsonPropertyName("NET_VERSION")] 17 | public string NETVersion { get; set; } 18 | 19 | public override string ToString() 20 | { 21 | return $"Application: {Application}; OS_Version: {OSVersion}; NET_Runtime: {NETRuntime}; NET_Version: {NETVersion}"; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/LoginRequestData.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Snowflake.Client.Json 4 | { 5 | public class LoginRequestData 6 | { 7 | [JsonPropertyName("CLIENT_APP_ID")] 8 | public string ClientAppId { get; set; } 9 | 10 | [JsonPropertyName("CLIENT_APP_VERSION")] 11 | public string ClientAppVersion { get; set; } 12 | 13 | [JsonPropertyName("ACCOUNT_NAME")] 14 | public string AccountName { get; set; } 15 | 16 | [JsonPropertyName("LOGIN_NAME")] 17 | public string LoginName { get; set; } 18 | 19 | [JsonPropertyName("PASSWORD")] 20 | public string Password { get; set; } 21 | 22 | [JsonPropertyName("AUTHENTICATOR")] 23 | public string Authenticator { get; set; } 24 | 25 | [JsonPropertyName("CLIENT_ENVIRONMENT")] 26 | public LoginRequestClientEnv ClientEnvironment { get; set; } 27 | 28 | [JsonPropertyName("RAW_SAML_RESPONSE")] 29 | public string RawSamlResponse { get; set; } 30 | 31 | [JsonPropertyName("TOKEN")] 32 | public string Token { get; set; } 33 | 34 | [JsonPropertyName("PROOF_KEY")] 35 | public string ProofKey { get; set; } 36 | 37 | // [JsonPropertyName("SESSION_PARAMETERS")] 38 | // public Dictionary Session_Parameters { get; set; } 39 | 40 | public override string ToString() 41 | { 42 | return $"Login: {LoginName}; Account_Name: {AccountName};"; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Snowflake.Client/Json/LoginResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Snowflake.Client.Json 2 | { 3 | public class LoginResponse : BaseResponse 4 | { 5 | public LoginResponseData Data { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/LoginResponseData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Snowflake.Client.Json 4 | { 5 | public class LoginResponseData 6 | { 7 | public string MasterToken { get; set; } 8 | public string Token { get; set; } 9 | public int ValidityInSeconds { get; set; } 10 | public int MasterValidityInSeconds { get; set; } 11 | public string DisplayUserName { get; set; } 12 | public string ServerVersion { get; set; } 13 | public bool FirstLogin { get; set; } 14 | public string RemMeToken { get; set; } 15 | public int RemMeValidityInSeconds { get; set; } 16 | public int HealthCheckInterval { get; set; } 17 | public string NewClientForUpgrade { get; set; } 18 | public long SessionId { get; set; } 19 | public List Parameters { get; set; } 20 | public SessionInfoRaw SessionInfo { get; set; } 21 | public string IdToken { get; set; } 22 | public int IdTokenValidityInSeconds { get; set; } 23 | } 24 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/NameValueParameter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Snowflake.Client.Json 4 | { 5 | public class NameValueParameter 6 | { 7 | [JsonPropertyName("name")] 8 | public string Name { get; set; } 9 | 10 | [JsonPropertyName("value")] 11 | public object Value { get; set; } 12 | 13 | public override string ToString() 14 | { 15 | return $"{Name}: {Value}"; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/NullDataResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Snowflake.Client.Json 4 | { 5 | public class NullDataResponse : BaseResponse 6 | { 7 | [JsonPropertyName("data")] 8 | public object data { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/ParamBinding.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Snowflake.Client.Json 4 | { 5 | public class ParamBinding 6 | { 7 | [JsonPropertyName("type")] 8 | public string Type { get; set; } 9 | 10 | [JsonPropertyName("value")] 11 | public string Value { get; set; } 12 | 13 | public override string ToString() 14 | { 15 | return $"Type: {Type}; Value: {Value}"; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/QueryCancelRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Snowflake.Client.Json 4 | { 5 | public class CancelQueryRequest : BaseRequest 6 | { 7 | [JsonPropertyName("requestId")] 8 | public string RequestId { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/QueryExecResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Snowflake.Client.Json 2 | { 3 | public class QueryExecResponse : BaseResponse 4 | { 5 | public QueryExecResponseData Data { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/QueryExecResponseData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Snowflake.Client.Json 4 | { 5 | public class QueryExecResponseData 6 | { 7 | public List Parameters { get; set; } 8 | public List RowType { get; set; } 9 | public List> RowSet { get; set; } 10 | public long Total { get; set; } 11 | public long Returned { get; set; } 12 | public string QueryId { get; set; } 13 | public string SqlState { get; set; } 14 | public string DatabaseProvider { get; set; } 15 | public string FinalDatabaseName { get; set; } 16 | public string FinalSchemaName { get; set; } 17 | public string FinalWarehouseName { get; set; } 18 | public string FinalRoleName { get; set; } 19 | public int NumberOfBinds { get; set; } 20 | public int StatementTypeId { get; set; } 21 | public int Version { get; set; } 22 | public List Chunks { get; set; } 23 | public string Qrmk { get; set; } 24 | public Dictionary ChunkHeaders { get; set; } 25 | public string GetResultUrl { get; set; } 26 | public string ProgressDesc { get; set; } 27 | public long QueryAbortAfterSecs { get; set; } 28 | } 29 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/QueryRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Snowflake.Client.Json 5 | { 6 | public class QueryRequest : BaseRequest 7 | { 8 | [JsonPropertyName("sqlText")] 9 | public string SqlText { get; set; } 10 | 11 | [JsonPropertyName("describeOnly")] 12 | public bool DescribeOnly { get; set; } 13 | 14 | [JsonPropertyName("bindings")] 15 | public Dictionary Bindings { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/RenewSessionRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Snowflake.Client.Json 4 | { 5 | public class RenewSessionRequest : BaseRequest 6 | { 7 | [JsonPropertyName("oldSessionToken")] 8 | public string OldSessionToken { get; set; } 9 | 10 | [JsonPropertyName("requestType")] 11 | public string RequestType { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/RenewSessionResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Snowflake.Client.Json 2 | { 3 | public class RenewSessionResponse : BaseResponse 4 | { 5 | public RenewSessionResponseData Data { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/RenewSessionResponseData.cs: -------------------------------------------------------------------------------- 1 | namespace Snowflake.Client.Json 2 | { 3 | public class RenewSessionResponseData 4 | { 5 | public string SessionToken { get; set; } 6 | public int ValidityInSecondsST { get; set; } 7 | public string MasterToken { get; set; } 8 | public int ValidityInSecondsMT { get; set; } 9 | public long SessionId { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /Snowflake.Client/Json/SessionInfoRaw.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Snowflake.Client.Json 4 | { 5 | public class SessionInfoRaw 6 | { 7 | [JsonPropertyName("databaseName")] 8 | public string DatabaseName { get; set; } 9 | 10 | [JsonPropertyName("schemaName")] 11 | public string SchemaName { get; set; } 12 | 13 | [JsonPropertyName("warehouseName")] 14 | public string WarehouseName { get; set; } 15 | 16 | [JsonPropertyName("roleName")] 17 | public string RoleName { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /Snowflake.Client/Media/snowflake_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fixer-m/snowflake-db-net-client/b4e7bd5d3abd94f0c801e9599ed4e0950955cd0f/Snowflake.Client/Media/snowflake_icon.png -------------------------------------------------------------------------------- /Snowflake.Client/Model/AuthInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Snowflake.Client.Model 2 | { 3 | /// 4 | /// Snowflake Authentication information. 5 | /// 6 | public class AuthInfo 7 | { 8 | /// 9 | /// Your Snowflake account name 10 | /// 11 | public string Account { get; set; } 12 | 13 | /// 14 | /// Your Snowflake user password 15 | /// 16 | public string Password { get; set; } 17 | 18 | /// 19 | /// Your Snowflake username 20 | /// 21 | public string User { get; set; } 22 | 23 | /// 24 | /// Region: "us-east-1", etc. 25 | /// Required for all, except for US West Oregon (us-west-2). 26 | /// Used to build Snowflake hostname: Account.Region.Cloud.snowflakecomputing.com. 27 | /// 28 | public string Region { get; set; } 29 | 30 | public AuthInfo() 31 | { 32 | } 33 | 34 | public AuthInfo(string user, string password, string account, string region = null) 35 | { 36 | User = user; 37 | Password = password; 38 | Account = account; 39 | Region = region; 40 | } 41 | 42 | public override string ToString() 43 | { 44 | return $"Account: {Account}; User: {User}; Region: {Region};"; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /Snowflake.Client/Model/ChunksDownloadInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Snowflake.Client.Json; 3 | 4 | namespace Snowflake.Client.Model 5 | { 6 | public class ChunksDownloadInfo 7 | { 8 | public List Chunks { get; set; } 9 | public string Qrmk { get; set; } 10 | public Dictionary ChunkHeaders { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Snowflake.Client/Model/ClientAppInfo.cs: -------------------------------------------------------------------------------- 1 | using Snowflake.Client.Json; 2 | 3 | namespace Snowflake.Client.Model 4 | { 5 | public class ClientAppInfo 6 | { 7 | public string DriverName { get; } 8 | public string DriverVersion { get; } 9 | public LoginRequestClientEnv Environment { get; } 10 | 11 | public ClientAppInfo() 12 | { 13 | Environment = new LoginRequestClientEnv() 14 | { 15 | Application = System.Diagnostics.Process.GetCurrentProcess().ProcessName, 16 | OSVersion = $"({System.Environment.OSVersion.VersionString})", 17 | #if NETFRAMEWORK 18 | NETRuntime = "NETFramework", 19 | NETVersion = "4.6", 20 | #else 21 | NETRuntime = "NETCore", 22 | NETVersion = "2.0", 23 | #endif 24 | }; 25 | 26 | DriverVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? ""; 27 | DriverName = ".NET_Snowflake.Client"; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Snowflake.Client/Model/SessionInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Snowflake.Client.Model 2 | { 3 | /// 4 | /// Represents current or desired Snowflake session objects 5 | /// 6 | public class SessionInfo 7 | { 8 | public string Role { get; set; } 9 | public string Schema { get; set; } 10 | public string Database { get; set; } 11 | public string Warehouse { get; set; } 12 | 13 | public override string ToString() 14 | { 15 | return $"Role: {Role}; WH: {Schema}; DB: {Database}; Schema: {Schema}"; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Snowflake.Client/Model/SnowflakeClientSettings.cs: -------------------------------------------------------------------------------- 1 | using Snowflake.Client.Helpers; 2 | using System; 3 | using System.Text.Json; 4 | 5 | namespace Snowflake.Client.Model 6 | { 7 | /// 8 | /// Configuration for SnowflakeClient 9 | /// 10 | public class SnowflakeClientSettings 11 | { 12 | /// 13 | /// Data used to authenticate in Snowflake: user, password, account and region 14 | /// 15 | public AuthInfo AuthInfo { get; } 16 | 17 | /// 18 | /// Snowflake URL: host, protocol and port 19 | /// 20 | public UrlInfo UrlInfo { get; } 21 | 22 | /// 23 | /// Snowflake session objects to set: role, schema, database and warehouse 24 | /// 25 | public SessionInfo SessionInfo { get; } 26 | 27 | /// 28 | /// Serializer options used to map data response to your model 29 | /// 30 | public JsonSerializerOptions JsonMapperOptions { get; } 31 | 32 | /// 33 | /// Options used in ChunksDownloader 34 | /// 35 | public ChunksDownloaderOptions ChunksDownloaderOptions { get; } 36 | 37 | /// 38 | /// Snowflake can return response data in a table form ("rowset") or in chunks or both. 39 | /// Set this parameter to true to fetch chunks, so the whole data set will be in a rowset. 40 | /// Default value: False 41 | /// 42 | public bool DownloadChunksForQueryRawResponses { get; set; } 43 | 44 | public SnowflakeClientSettings(AuthInfo authInfo, SessionInfo sessionInfo = null, UrlInfo urlInfo = null, 45 | JsonSerializerOptions jsonMapperOptions = null, ChunksDownloaderOptions chunksDownloaderOptions = null, 46 | bool downloadChunksForQueryRawResponses = false) 47 | { 48 | AuthInfo = authInfo ?? new AuthInfo(); 49 | SessionInfo = sessionInfo ?? new SessionInfo(); 50 | UrlInfo = urlInfo ?? new UrlInfo(); 51 | JsonMapperOptions = jsonMapperOptions ?? new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }; 52 | ChunksDownloaderOptions = chunksDownloaderOptions ?? new ChunksDownloaderOptions() { PrefetchThreadsCount = 4 }; 53 | DownloadChunksForQueryRawResponses = downloadChunksForQueryRawResponses; 54 | 55 | UrlInfo.Host = string.IsNullOrEmpty(UrlInfo.Host) 56 | ? BuildHostName(AuthInfo.Account, AuthInfo.Region) 57 | : ReplaceUnderscores(UrlInfo.Host); 58 | } 59 | 60 | private static string BuildHostName(string account, string region) 61 | { 62 | if (string.IsNullOrEmpty(account)) 63 | throw new ArgumentException("Account name cannot be empty."); 64 | 65 | var hostname = $"{ReplaceUnderscores(account)}."; 66 | 67 | if (!string.IsNullOrEmpty(region) && region.ToLower() != "us-west-2") 68 | hostname += $"{region}."; 69 | 70 | var cloudTag = SnowflakeUtils.GetCloudTagByRegion(region); 71 | 72 | if (!string.IsNullOrEmpty(cloudTag)) 73 | hostname += $"{cloudTag}."; 74 | 75 | hostname += "snowflakecomputing.com"; 76 | 77 | return hostname.ToLower(); 78 | } 79 | 80 | // Underscores in hostname will lead to SSL cert verification issue. 81 | // See https://github.com/snowflakedb/snowflake-connector-net/issues/160#issuecomment-692883663 82 | private static string ReplaceUnderscores(string account) 83 | { 84 | return account.Replace("_", "-"); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /Snowflake.Client/Model/SnowflakeConst.cs: -------------------------------------------------------------------------------- 1 | namespace Snowflake.Client.Model 2 | { 3 | public static class SnowflakeConst 4 | { 5 | public const string SF_SESSION_PATH = "/session"; 6 | 7 | public const string SF_LOGIN_PATH = SF_SESSION_PATH + "/v1/login-request"; 8 | 9 | public const string SF_TOKEN_REQUEST_PATH = SF_SESSION_PATH + "/token-request"; 10 | 11 | public const string SF_AUTHENTICATOR_REQUEST_PATH = SF_SESSION_PATH + "/authenticator-request"; 12 | 13 | public const string SF_QUERY_PATH = "/queries/v1/query-request"; 14 | 15 | public const string SF_QUERY_CANCEL_PATH = "/queries/v1/abort-request"; 16 | 17 | public const string SF_QUERY_WAREHOUSE = "warehouse"; 18 | 19 | public const string SF_QUERY_DB = "databaseName"; 20 | 21 | public const string SF_QUERY_SCHEMA = "schemaName"; 22 | 23 | public const string SF_QUERY_ROLE = "roleName"; 24 | 25 | public const string SF_QUERY_REQUEST_ID = "requestId"; 26 | 27 | public const string SF_QUERY_REQUEST_GUID = "request_guid"; 28 | 29 | public const string SF_QUERY_START_TIME = "clientStartTime"; 30 | 31 | public const string SF_QUERY_RETRY_COUNT = "retryCount"; 32 | 33 | public const string SF_QUERY_SESSION_DELETE = "delete"; 34 | } 35 | } -------------------------------------------------------------------------------- /Snowflake.Client/Model/SnowflakeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Snowflake.Client.Model 4 | { 5 | [Serializable] 6 | public class SnowflakeException : Exception 7 | { 8 | public int? Code { get; private set; } 9 | 10 | public SnowflakeException() 11 | { 12 | } 13 | 14 | public SnowflakeException(string message) : base(message) 15 | { 16 | } 17 | 18 | public SnowflakeException(string message, int? code) : base(message) 19 | { 20 | Code = code; 21 | } 22 | 23 | public SnowflakeException(string message, Exception innerException) : base(message, innerException) 24 | { 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Snowflake.Client/Model/SnowflakeQueryRawResponse.cs: -------------------------------------------------------------------------------- 1 | using Snowflake.Client.Json; 2 | using System.Collections.Generic; 3 | 4 | namespace Snowflake.Client.Model 5 | { 6 | public class SnowflakeQueryRawResponse 7 | { 8 | public List Parameters { get; private set; } 9 | public List Columns { get; private set; } 10 | public List> Rows { get; private set; } 11 | public long Total { get; private set; } 12 | public long Returned { get; private set; } 13 | public string QueryId { get; private set; } 14 | public string SqlState { get; private set; } 15 | public string DatabaseProvider { get; private set; } 16 | public string FinalDatabaseName { get; private set; } 17 | public string FinalSchemaName { get; private set; } 18 | public string FinalWarehouseName { get; private set; } 19 | public string FinalRoleName { get; private set; } 20 | public int NumberOfBinds { get; private set; } 21 | public int StatementTypeId { get; private set; } 22 | public int Version { get; private set; } 23 | public List Chunks { get; private set; } 24 | public string Qrmk { get; private set; } 25 | public Dictionary ChunkHeaders { get; private set; } 26 | public string GetResultUrl { get; private set; } 27 | public string ProgressDesc { get; private set; } 28 | public long QueryAbortAfterSecs { get; private set; } 29 | 30 | public SnowflakeQueryRawResponse(QueryExecResponseData responseData) 31 | { 32 | Parameters = responseData.Parameters; 33 | Columns = responseData.RowType; 34 | Rows = responseData.RowSet; 35 | Total = responseData.Total; 36 | Returned = responseData.Returned; 37 | QueryId = responseData.QueryId; 38 | SqlState = responseData.SqlState; 39 | DatabaseProvider = responseData.DatabaseProvider; 40 | FinalDatabaseName = responseData.FinalDatabaseName; 41 | FinalRoleName = responseData.FinalRoleName; 42 | FinalSchemaName = responseData.FinalSchemaName; 43 | FinalWarehouseName = responseData.FinalWarehouseName; 44 | NumberOfBinds = responseData.NumberOfBinds; 45 | StatementTypeId = responseData.StatementTypeId; 46 | Version = responseData.Version; 47 | Chunks = responseData.Chunks; 48 | Qrmk = responseData.Qrmk; 49 | ChunkHeaders = responseData.ChunkHeaders; 50 | GetResultUrl = responseData.GetResultUrl; 51 | ProgressDesc = responseData.ProgressDesc; 52 | QueryAbortAfterSecs = responseData.QueryAbortAfterSecs; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /Snowflake.Client/Model/SnowflakeRawData.cs: -------------------------------------------------------------------------------- 1 | using Snowflake.Client.Json; 2 | using System.Collections.Generic; 3 | 4 | namespace Snowflake.Client.Model 5 | { 6 | /// 7 | /// Raw data returned from Snowflake REST API. 8 | /// 9 | public class SnowflakeRawData 10 | { 11 | public List Columns { get; set; } 12 | 13 | public List> Rows { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /Snowflake.Client/Model/SnowflakeSession.cs: -------------------------------------------------------------------------------- 1 | using Snowflake.Client.Json; 2 | 3 | namespace Snowflake.Client.Model 4 | { 5 | /// 6 | /// Snowflake Session information 7 | /// 8 | public class SnowflakeSession 9 | { 10 | public string MasterToken { get; private set; } 11 | public string SessionToken { get; private set; } 12 | public int ValidityInSeconds { get; private set; } 13 | public int MasterValidityInSeconds { get; private set; } 14 | public string DisplayUserName { get; private set; } 15 | public string ServerVersion { get; private set; } 16 | public bool FirstLogin { get; private set; } 17 | public string RemMeToken { get; private set; } 18 | public int RemMeValidityInSeconds { get; private set; } 19 | public int HealthCheckInterval { get; private set; } 20 | public string NewClientForUpgrade { get; private set; } 21 | public long SessionId { get; private set; } 22 | public string IdToken { get; private set; } 23 | public int IdTokenValidityInSeconds { get; private set; } 24 | public string DatabaseName { get; private set; } 25 | public string SchemaName { get; private set; } 26 | public string WarehouseName { get; private set; } 27 | public string RoleName { get; private set; } 28 | 29 | public SnowflakeSession(LoginResponseData loginResponseData) 30 | { 31 | SessionToken = loginResponseData.Token; 32 | 33 | MasterToken = loginResponseData.MasterToken; 34 | ValidityInSeconds = loginResponseData.ValidityInSeconds; 35 | MasterValidityInSeconds = loginResponseData.MasterValidityInSeconds; 36 | DisplayUserName = loginResponseData.DisplayUserName; 37 | ServerVersion = loginResponseData.ServerVersion; 38 | FirstLogin = loginResponseData.FirstLogin; 39 | RemMeToken = loginResponseData.RemMeToken; 40 | RemMeValidityInSeconds = loginResponseData.RemMeValidityInSeconds; 41 | HealthCheckInterval = loginResponseData.HealthCheckInterval; 42 | NewClientForUpgrade = loginResponseData.NewClientForUpgrade; 43 | SessionId = loginResponseData.SessionId; 44 | IdToken = loginResponseData.IdToken; 45 | IdTokenValidityInSeconds = loginResponseData.IdTokenValidityInSeconds; 46 | DatabaseName = loginResponseData.SessionInfo.DatabaseName; 47 | SchemaName = loginResponseData.SessionInfo.SchemaName; 48 | WarehouseName = loginResponseData.SessionInfo.WarehouseName; 49 | RoleName = loginResponseData.SessionInfo.RoleName; 50 | } 51 | 52 | internal void Renew(RenewSessionResponseData renewSessionResponseData) 53 | { 54 | SessionToken = renewSessionResponseData.SessionToken; 55 | 56 | MasterToken = renewSessionResponseData.MasterToken; 57 | SessionId = renewSessionResponseData.SessionId; 58 | ValidityInSeconds = renewSessionResponseData.ValidityInSecondsST; 59 | MasterValidityInSeconds = renewSessionResponseData.ValidityInSecondsMT; 60 | } 61 | 62 | public override string ToString() 63 | { 64 | return $"User: {DisplayUserName}; Role: {RoleName}; Warehouse: {WarehouseName}"; 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /Snowflake.Client/Model/SnowflakeStatementType.cs: -------------------------------------------------------------------------------- 1 | namespace Snowflake.Client.Model 2 | { 3 | public enum SnowflakeStatementType 4 | { 5 | UNKNOWN = 0x0000, 6 | SELECT = 0x1000, 7 | 8 | DML = 0x3000, 9 | INSERT = 0x3000 + 0x100, 10 | UPDATE = 0x3000 + 0x200, 11 | DELETE = 0x3000 + 0x300, 12 | MERGE = 0x3000 + 0x400, 13 | MULTI_INSERT = 0x3000 + 0x500, 14 | COPY = 0x3000 + 0x600, 15 | COPY_UNLOAD = 0x3000 + 0x700, 16 | 17 | SCL = 0x4000, 18 | ALTER_SESSION = 0x4000 + 0x100, 19 | USE = 0x4000 + 0x300, 20 | USE_DATABASE = 0x4000 + 0x300 + 0x10, 21 | USE_SCHEMA = 0x4000 + 0x300 + 0x20, 22 | USE_WAREHOUSE = 0x4000 + 0x300 + 0x30, 23 | SHOW = 0x4000 + 0x400, 24 | DESCRIBE = 0x4000 + 0x500, 25 | 26 | TCL = 0x5000, 27 | DDL = 0x6000 28 | } 29 | } -------------------------------------------------------------------------------- /Snowflake.Client/Model/UrlInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Snowflake.Client.Model 4 | { 5 | /// 6 | /// Represents information about Snowflake URL. 7 | /// 8 | public class UrlInfo 9 | { 10 | /// 11 | /// Snowflake URL host. Should end up with ".snowflakecomputing.com". 12 | /// If not specified, will be constructed as Account.Region.Cloud.snowflakecomputing.com. 13 | /// 14 | public string Host { get; set; } 15 | 16 | /// 17 | /// Supported values: "https" (default) and "http" 18 | /// 19 | public string Protocol { get; set; } 20 | 21 | /// 22 | /// Port number. Should be 443 (default) for https or 80 for http. 23 | /// 24 | public int Port { get; set; } 25 | 26 | public UrlInfo() 27 | { 28 | Protocol = "https"; 29 | Port = 443; 30 | } 31 | 32 | public UrlInfo(string host, string protocol = "https", int port = 443) 33 | { 34 | Host = host; 35 | Protocol = protocol; 36 | Port = port; 37 | } 38 | 39 | public UrlInfo(Uri snowflakeUrl) : this(snowflakeUrl.Host, snowflakeUrl.Scheme, snowflakeUrl.Port) 40 | { 41 | } 42 | 43 | public override string ToString() 44 | { 45 | return $"{Protocol}://{Host}:{Port}"; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /Snowflake.Client/ParameterBinder.cs: -------------------------------------------------------------------------------- 1 | using Snowflake.Client.Json; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Globalization; 6 | using System.Linq; 7 | 8 | namespace Snowflake.Client 9 | { 10 | // Bindings: https://docs.snowflake.com/en/user-guide/python-connector-api.html 11 | // Based on https://github.com/snowflakedb/snowflake-connector-net/blob/master/Snowflake.Data/Core/SFDataConverter.cs 12 | 13 | internal static class ParameterBinder 14 | { 15 | internal static Dictionary BuildParameterBindings(object param) 16 | { 17 | if (param == null) 18 | return null; 19 | 20 | var paramType = param.GetType(); 21 | 22 | if (IsSimpleType(paramType)) 23 | { 24 | var bindings = new Dictionary(); 25 | var binding = BuildParamFromSimpleType(param, paramType); 26 | bindings.Add("1", binding); 27 | return bindings; 28 | } 29 | 30 | if (param is IEnumerable enumerable) 31 | { 32 | return BuildParamsFromEnumerable(paramType, enumerable); 33 | } 34 | 35 | return BuildParamsFromComplexType(param, paramType); 36 | } 37 | 38 | private static Dictionary BuildParamsFromEnumerable(Type paramType, IEnumerable enumerable) 39 | { 40 | var result = new Dictionary(); 41 | 42 | if (IsDictionary(enumerable)) 43 | { 44 | if (!(enumerable is IEnumerable> dictionary)) 45 | { 46 | throw new ArgumentException("Only IEnumerable is supported"); 47 | } 48 | 49 | foreach (var item in dictionary) 50 | { 51 | var type = item.Value.GetType(); 52 | if (IsSimpleType(type)) 53 | { 54 | result.Add(item.Key, BuildParamFromSimpleType(item.Value, type)); 55 | } 56 | else 57 | { 58 | throw new ArgumentException($"Parameter binding doesn't support type {type.Name} in IEnumerable> values."); 59 | } 60 | } 61 | 62 | return result; 63 | } 64 | 65 | var elementType = GetItemTypeFromCollection(paramType); 66 | if (IsSimpleType(elementType)) 67 | { 68 | var i = 0; 69 | foreach (var item in enumerable) 70 | { 71 | i++; 72 | result.Add(i.ToString(), BuildParamFromSimpleType(item, elementType)); 73 | } 74 | 75 | return result; 76 | } 77 | 78 | throw new ArgumentException($"Parameter binding doesn't support type {elementType.Name} in collections."); 79 | } 80 | 81 | private static Type GetItemTypeFromCollection(Type type) 82 | { 83 | var elementType = type.GetGenericArguments().FirstOrDefault() 84 | ?? type.GetElementType() 85 | ?? type.GetInterfaces().FirstOrDefault(t => t.IsGenericType 86 | && t.GetGenericTypeDefinition() == typeof(IEnumerable<>))?.GenericTypeArguments.FirstOrDefault(); 87 | 88 | return elementType; 89 | } 90 | 91 | private static bool IsSimpleType(Type paramType) 92 | { 93 | var underlyingType = Nullable.GetUnderlyingType(paramType); 94 | if (underlyingType != null) 95 | paramType = underlyingType; 96 | 97 | return paramType == typeof(string) || !paramType.IsClass && !IsCustomValueType(paramType) || paramType == typeof(byte[]); 98 | } 99 | 100 | private static bool IsDictionary(object o) 101 | { 102 | if (o == null) return false; 103 | return o is IDictionary && 104 | o.GetType().IsGenericType && 105 | o.GetType().GetGenericTypeDefinition().IsAssignableFrom(typeof(Dictionary<,>)); 106 | } 107 | 108 | private static bool IsCustomValueType(Type type) 109 | { 110 | return type.IsValueType && !type.IsPrimitive && type.Namespace != null && !type.Namespace.StartsWith("System"); 111 | } 112 | 113 | private static Dictionary BuildParamsFromComplexType(object param, Type paramType) 114 | { 115 | var result = new Dictionary(); 116 | 117 | var typeProperties = paramType.GetProperties().Where(p => IsSimpleType(p.PropertyType)).ToList(); 118 | foreach (var property in typeProperties) 119 | { 120 | var propValue = property.GetValue(param); 121 | var binding = BuildParamFromSimpleType(propValue, property.PropertyType); 122 | result.Add(property.Name, binding); 123 | } 124 | 125 | var typeFields = paramType.GetFields().Where(p => IsSimpleType(p.FieldType)).ToList(); 126 | foreach (var field in typeFields) 127 | { 128 | var propValue = field.GetValue(param); 129 | var binding = BuildParamFromSimpleType(propValue, field.FieldType); 130 | result.Add(field.Name, binding); 131 | } 132 | 133 | return result; 134 | } 135 | 136 | private static ParamBinding BuildParamFromSimpleType(object param, Type paramType) 137 | { 138 | var underlyingType = Nullable.GetUnderlyingType(paramType); 139 | if (underlyingType != null) 140 | paramType = underlyingType; 141 | 142 | var stringValue = param == null ? null : string.Format(CultureInfo.InvariantCulture, "{0}", param); 143 | 144 | if (TextTypes.Contains(paramType)) 145 | return new ParamBinding() { Type = "TEXT", Value = stringValue }; 146 | 147 | if (FixedTypes.Contains(paramType)) 148 | return new ParamBinding() { Type = "FIXED", Value = stringValue }; 149 | 150 | if (paramType == typeof(bool)) 151 | return new ParamBinding() { Type = "BOOLEAN", Value = stringValue }; 152 | 153 | if (RealTypes.Contains(paramType)) 154 | return new ParamBinding() { Type = "REAL", Value = stringValue }; 155 | 156 | if (paramType == typeof(DateTime)) 157 | return new ParamBinding() 158 | { 159 | Type = "TIMESTAMP_NTZ", 160 | Value = param == null ? null : SnowflakeTypesConverter.ConvertToTimestampNtz((DateTime)param) 161 | }; 162 | 163 | if (paramType == typeof(DateTimeOffset)) 164 | return new ParamBinding() 165 | { 166 | Type = "TIMESTAMP_TZ", 167 | Value = param == null ? null : SnowflakeTypesConverter.ConvertToTimestampTz((DateTimeOffset)param) 168 | }; 169 | 170 | if (paramType == typeof(byte[])) 171 | return new ParamBinding() 172 | { 173 | Type = "BINARY", 174 | Value = param == null ? null : SnowflakeTypesConverter.BytesToHex((byte[])param) 175 | }; 176 | 177 | return null; 178 | } 179 | 180 | private static readonly HashSet FixedTypes = new HashSet() 181 | { 182 | typeof(int), typeof(long), typeof(short), typeof(sbyte), 183 | typeof(byte), typeof(ulong), typeof(ushort), typeof(uint) 184 | }; 185 | 186 | private static readonly HashSet TextTypes = new HashSet() { typeof(string), typeof(Guid) }; 187 | 188 | private static readonly HashSet RealTypes = new HashSet() { typeof(double), typeof(float), typeof(decimal) }; 189 | } 190 | } -------------------------------------------------------------------------------- /Snowflake.Client/RequestBuilder.cs: -------------------------------------------------------------------------------- 1 | using Snowflake.Client.Json; 2 | using Snowflake.Client.Model; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using System.Text; 8 | using System.Text.Json; 9 | using System.Text.Json.Serialization; 10 | using System.Web; 11 | 12 | namespace Snowflake.Client 13 | { 14 | internal class RequestBuilder 15 | { 16 | private readonly UrlInfo _urlInfo; 17 | private readonly JsonSerializerOptions _jsonSerializerOptions; 18 | private readonly ClientAppInfo _clientInfo; 19 | 20 | private string _masterToken; 21 | private string _sessionToken; 22 | 23 | internal RequestBuilder(UrlInfo urlInfo) 24 | { 25 | _urlInfo = urlInfo; 26 | 27 | #if NETSTANDARD 28 | _jsonSerializerOptions = new JsonSerializerOptions() 29 | { 30 | IgnoreNullValues = true 31 | }; 32 | #else 33 | _jsonSerializerOptions = new JsonSerializerOptions 34 | { 35 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull 36 | }; 37 | #endif 38 | 39 | _clientInfo = new ClientAppInfo(); 40 | } 41 | 42 | internal void SetSessionTokens(string sessionToken, string masterToken) 43 | { 44 | _sessionToken = sessionToken; 45 | _masterToken = masterToken; 46 | } 47 | 48 | internal void ClearSessionTokens() 49 | { 50 | _sessionToken = null; 51 | _masterToken = null; 52 | } 53 | 54 | internal HttpRequestMessage BuildLoginRequest(AuthInfo authInfo, SessionInfo sessionInfo) 55 | { 56 | var requestUri = BuildLoginUrl(sessionInfo); 57 | 58 | var data = new LoginRequestData() 59 | { 60 | LoginName = authInfo.User, 61 | Password = authInfo.Password, 62 | AccountName = authInfo.Account, 63 | ClientAppId = _clientInfo.DriverName, 64 | ClientAppVersion = _clientInfo.DriverVersion, 65 | ClientEnvironment = _clientInfo.Environment 66 | }; 67 | 68 | var requestBody = new LoginRequest() { Data = data }; 69 | var jsonBody = JsonSerializer.Serialize(requestBody, _jsonSerializerOptions); 70 | var request = BuildJsonRequestMessage(requestUri, HttpMethod.Post, jsonBody); 71 | 72 | return request; 73 | } 74 | 75 | internal HttpRequestMessage BuildCancelQueryRequest(string requestId) 76 | { 77 | var requestUri = BuildCancelQueryUrl(); 78 | var requestBody = new CancelQueryRequest() 79 | { 80 | RequestId = requestId 81 | }; 82 | 83 | var jsonBody = JsonSerializer.Serialize(requestBody, _jsonSerializerOptions); 84 | var request = BuildJsonRequestMessage(requestUri, HttpMethod.Post, jsonBody); 85 | 86 | return request; 87 | } 88 | 89 | internal HttpRequestMessage BuildRenewSessionRequest() 90 | { 91 | var requestUri = BuildRenewSessionUrl(); 92 | var requestBody = new RenewSessionRequest() 93 | { 94 | OldSessionToken = _sessionToken, 95 | RequestType = "RENEW" 96 | }; 97 | 98 | var jsonBody = JsonSerializer.Serialize(requestBody, _jsonSerializerOptions); 99 | var request = BuildJsonRequestMessage(requestUri, HttpMethod.Post, jsonBody, true); 100 | 101 | return request; 102 | } 103 | 104 | internal HttpRequestMessage BuildQueryRequest(string sql, object sqlParams, bool describeOnly) 105 | { 106 | var queryUri = BuildQueryUrl(); 107 | 108 | var requestBody = new QueryRequest() 109 | { 110 | SqlText = sql, 111 | DescribeOnly = describeOnly, 112 | Bindings = ParameterBinder.BuildParameterBindings(sqlParams) 113 | }; 114 | 115 | var jsonBody = JsonSerializer.Serialize(requestBody, _jsonSerializerOptions); 116 | var request = BuildJsonRequestMessage(queryUri, HttpMethod.Post, jsonBody); 117 | 118 | return request; 119 | } 120 | 121 | internal HttpRequestMessage BuildCloseSessionRequest() 122 | { 123 | var queryParams = new Dictionary(); 124 | queryParams[SnowflakeConst.SF_QUERY_SESSION_DELETE] = "true"; 125 | queryParams[SnowflakeConst.SF_QUERY_REQUEST_ID] = Guid.NewGuid().ToString(); 126 | 127 | var requestUri = BuildUri(SnowflakeConst.SF_SESSION_PATH, queryParams); 128 | var request = BuildJsonRequestMessage(requestUri, HttpMethod.Post); 129 | 130 | return request; 131 | } 132 | 133 | internal HttpRequestMessage BuildGetResultRequest(string getResultUrl) 134 | { 135 | var queryUri = BuildUri(getResultUrl); 136 | var request = BuildJsonRequestMessage(queryUri, HttpMethod.Get); 137 | 138 | return request; 139 | } 140 | 141 | internal Uri BuildLoginUrl(SessionInfo sessionInfo) 142 | { 143 | var queryParams = new Dictionary 144 | { 145 | [SnowflakeConst.SF_QUERY_WAREHOUSE] = sessionInfo.Warehouse, 146 | [SnowflakeConst.SF_QUERY_DB] = sessionInfo.Database, 147 | [SnowflakeConst.SF_QUERY_SCHEMA] = sessionInfo.Schema, 148 | [SnowflakeConst.SF_QUERY_ROLE] = sessionInfo.Role, 149 | [SnowflakeConst.SF_QUERY_REQUEST_ID] = Guid.NewGuid().ToString() // extract to shared part ? 150 | }; 151 | 152 | var loginUrl = BuildUri(SnowflakeConst.SF_LOGIN_PATH, queryParams); 153 | return loginUrl; 154 | } 155 | 156 | internal Uri BuildCancelQueryUrl() 157 | { 158 | var queryParams = new Dictionary 159 | { 160 | [SnowflakeConst.SF_QUERY_REQUEST_ID] = Guid.NewGuid().ToString(), 161 | [SnowflakeConst.SF_QUERY_REQUEST_GUID] = Guid.NewGuid().ToString() 162 | }; 163 | 164 | var url = BuildUri(SnowflakeConst.SF_QUERY_CANCEL_PATH, queryParams); 165 | return url; 166 | } 167 | 168 | internal Uri BuildRenewSessionUrl() 169 | { 170 | var queryParams = new Dictionary 171 | { 172 | [SnowflakeConst.SF_QUERY_REQUEST_ID] = Guid.NewGuid().ToString(), 173 | [SnowflakeConst.SF_QUERY_REQUEST_GUID] = Guid.NewGuid().ToString() 174 | }; 175 | 176 | var url = BuildUri(SnowflakeConst.SF_TOKEN_REQUEST_PATH, queryParams); 177 | return url; 178 | } 179 | 180 | private Uri BuildQueryUrl() 181 | { 182 | var queryParams = new Dictionary 183 | { 184 | [SnowflakeConst.SF_QUERY_REQUEST_ID] = Guid.NewGuid().ToString() 185 | }; 186 | 187 | var loginUrl = BuildUri(SnowflakeConst.SF_QUERY_PATH, queryParams); 188 | return loginUrl; 189 | } 190 | 191 | internal Uri BuildUri(string basePath, Dictionary queryParams = null) 192 | { 193 | var uriBuilder = new UriBuilder 194 | { 195 | Scheme = _urlInfo.Protocol, 196 | Host = _urlInfo.Host, 197 | Port = _urlInfo.Port, 198 | Path = basePath 199 | }; 200 | 201 | if (queryParams != null && queryParams.Count > 0) 202 | { 203 | var paramCollection = HttpUtility.ParseQueryString(""); 204 | foreach (var kvp in queryParams) 205 | { 206 | if (!string.IsNullOrEmpty(kvp.Value)) 207 | paramCollection.Add(kvp.Key, kvp.Value); 208 | } 209 | uriBuilder.Query = paramCollection.ToString() ?? ""; 210 | } 211 | 212 | return uriBuilder.Uri; 213 | } 214 | 215 | private HttpRequestMessage BuildJsonRequestMessage(Uri uri, HttpMethod method, string jsonBody = null, bool useMasterToken = false) 216 | { 217 | var request = new HttpRequestMessage(); 218 | request.Method = method; 219 | request.RequestUri = uri; 220 | 221 | if (jsonBody != null && method != HttpMethod.Get) 222 | { 223 | request.Content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); 224 | } 225 | 226 | if (_sessionToken != null) 227 | { 228 | var authToken = useMasterToken ? _masterToken : _sessionToken; 229 | request.Headers.Add("Authorization", $"Snowflake Token=\"{authToken}\""); 230 | } 231 | 232 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/snowflake")); 233 | request.Headers.UserAgent.Add(new ProductInfoHeaderValue(_clientInfo.DriverName, _clientInfo.DriverVersion)); 234 | request.Headers.UserAgent.Add(new ProductInfoHeaderValue(_clientInfo.Environment.OSVersion)); 235 | request.Headers.UserAgent.Add(new ProductInfoHeaderValue(_clientInfo.Environment.NETRuntime, _clientInfo.Environment.NETVersion)); 236 | 237 | return request; 238 | } 239 | } 240 | } -------------------------------------------------------------------------------- /Snowflake.Client/RestClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Security.Authentication; 5 | using System.Text.Json; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace Snowflake.Client 10 | { 11 | internal class RestClient 12 | { 13 | private HttpClient _httpClient; 14 | private readonly JsonSerializerOptions _jsonSerializerOptions; 15 | 16 | internal RestClient() 17 | { 18 | var httpClientHandler = new HttpClientHandler 19 | { 20 | UseCookies = false, 21 | SslProtocols = SslProtocols.Tls12, 22 | CheckCertificateRevocationList = true, 23 | AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate 24 | }; 25 | 26 | _httpClient = new HttpClient(httpClientHandler) 27 | { 28 | Timeout = TimeSpan.FromHours(1) 29 | }; 30 | 31 | _jsonSerializerOptions = new JsonSerializerOptions() 32 | { 33 | PropertyNameCaseInsensitive = true 34 | }; 35 | } 36 | 37 | internal void SetHttpClient(HttpClient httpClient) 38 | { 39 | _httpClient = httpClient; 40 | } 41 | 42 | internal async Task SendAsync(HttpRequestMessage request, CancellationToken ct) 43 | { 44 | request.Headers.ExpectContinue = false; 45 | var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false); 46 | response.EnsureSuccessStatusCode(); 47 | 48 | #if NETSTANDARD 49 | var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 50 | #else 51 | var json = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false); 52 | #endif 53 | 54 | return JsonSerializer.Deserialize(json, _jsonSerializerOptions); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /Snowflake.Client/Snowflake.Client.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;net6.0 5 | true 6 | 0.4.6 7 | fixer_m 8 | .NET client for Snowflake DB REST API. 9 | Provides straightforward and efficient way to execute SQL queries in Snowflake and automatically map response to your models. 10 | Copyright (c) 2020-2022 Ilya Bystrov 11 | https://github.com/fixer-m/snowflake-db-net-client/ 12 | Apache-2.0 13 | https://github.com/fixer-m/snowflake-db-net-client/ 14 | snowflake;client;api;wrapper;snowflakedb;rest;restapi 15 | $(Version) 16 | $(Version) 17 | snowflake_icon.png 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | True 36 | \ 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Snowflake.Client/SnowflakeClient.cs: -------------------------------------------------------------------------------- 1 | using Snowflake.Client.Helpers; 2 | using Snowflake.Client.Json; 3 | using Snowflake.Client.Model; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net.Http; 8 | using System.Text.Json; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace Snowflake.Client 13 | { 14 | public class SnowflakeClient : ISnowflakeClient 15 | { 16 | /// 17 | /// Current Snowflake session. Null if not initialized. 18 | /// 19 | public SnowflakeSession SnowflakeSession => _snowflakeSession; 20 | 21 | /// 22 | /// Snowflake Client settings 23 | /// 24 | public SnowflakeClientSettings Settings => _clientSettings; 25 | 26 | private SnowflakeSession _snowflakeSession; 27 | private readonly RestClient _restClient; 28 | private readonly RequestBuilder _requestBuilder; 29 | private readonly SnowflakeClientSettings _clientSettings; 30 | 31 | /// 32 | /// Creates new Snowflake client. 33 | /// 34 | /// Username 35 | /// Password 36 | /// Account 37 | /// Region: "us-east-1", etc. Required for all except for US West Oregon (us-west-2). 38 | public SnowflakeClient(string user, string password, string account, string region = null) 39 | : this(new AuthInfo(user, password, account, region)) 40 | { 41 | } 42 | 43 | /// 44 | /// Creates new Snowflake client. 45 | /// 46 | /// Auth information: user, password, account, region 47 | /// Session information: role, schema, database, warehouse 48 | /// URL information: host, protocol and port 49 | /// JsonSerializerOptions which will be used to map response to your model 50 | public SnowflakeClient(AuthInfo authInfo, SessionInfo sessionInfo = null, UrlInfo urlInfo = null, JsonSerializerOptions jsonMapperOptions = null) 51 | : this(new SnowflakeClientSettings(authInfo, sessionInfo, urlInfo, jsonMapperOptions)) 52 | { 53 | } 54 | 55 | /// 56 | /// Creates new Snowflake client. 57 | /// 58 | /// Client settings to initialize new session. 59 | public SnowflakeClient(SnowflakeClientSettings settings) 60 | { 61 | ValidateClientSettings(settings); 62 | 63 | _clientSettings = settings; 64 | _restClient = new RestClient(); 65 | _requestBuilder = new RequestBuilder(settings.UrlInfo); 66 | 67 | SnowflakeDataMapper.Configure(settings.JsonMapperOptions); 68 | ChunksDownloader.Configure(settings.ChunksDownloaderOptions); 69 | } 70 | 71 | private static void ValidateClientSettings(SnowflakeClientSettings settings) 72 | { 73 | if (settings == null) 74 | throw new ArgumentException("Settings object cannot be null."); 75 | 76 | if (string.IsNullOrEmpty(settings.AuthInfo?.User)) 77 | throw new ArgumentException("User name is either empty or null."); 78 | 79 | if (string.IsNullOrEmpty(settings.AuthInfo?.Password)) 80 | throw new ArgumentException("User password is either empty or null."); 81 | 82 | if (string.IsNullOrEmpty(settings.AuthInfo?.Account)) 83 | throw new ArgumentException("Snowflake account is either empty or null."); 84 | 85 | if (settings.UrlInfo?.Protocol != "https" && settings.UrlInfo?.Protocol != "http") 86 | throw new ArgumentException("URL Protocol should be either http or https."); 87 | 88 | if (string.IsNullOrEmpty(settings.UrlInfo?.Host)) 89 | throw new ArgumentException("URL Host cannot be empty."); 90 | 91 | if (!settings.UrlInfo.Host.ToLower().EndsWith(".snowflakecomputing.com")) 92 | throw new ArgumentException("URL Host should end up with '.snowflakecomputing.com'."); 93 | } 94 | 95 | /// 96 | /// Initializes new Snowflake session. 97 | /// 98 | /// True if session successfully initialized 99 | public async Task InitNewSessionAsync(CancellationToken ct = default) 100 | { 101 | _snowflakeSession = await AuthenticateAsync(_clientSettings.AuthInfo, _clientSettings.SessionInfo, ct).ConfigureAwait(false); 102 | _requestBuilder.SetSessionTokens(_snowflakeSession.SessionToken, _snowflakeSession.MasterToken); 103 | 104 | return true; 105 | } 106 | 107 | private async Task AuthenticateAsync(AuthInfo authInfo, SessionInfo sessionInfo, CancellationToken ct) 108 | { 109 | var loginRequest = _requestBuilder.BuildLoginRequest(authInfo, sessionInfo); 110 | 111 | var response = await _restClient.SendAsync(loginRequest, ct).ConfigureAwait(false); 112 | 113 | if (!response.Success) 114 | throw new SnowflakeException($"Authentication failed. Message: {response.Message}", response.Code); 115 | 116 | return new SnowflakeSession(response.Data); 117 | } 118 | 119 | /// 120 | /// Renew session 121 | /// 122 | /// True if session successfully renewed 123 | public async Task RenewSessionAsync(CancellationToken ct = default) 124 | { 125 | if (_snowflakeSession == null) 126 | throw new SnowflakeException("Session is not initialized yet."); 127 | 128 | var renewSessionRequest = _requestBuilder.BuildRenewSessionRequest(); 129 | var response = await _restClient.SendAsync(renewSessionRequest, ct).ConfigureAwait(false); 130 | 131 | if (response.Success) 132 | _snowflakeSession.Renew(response.Data); 133 | else if (response.Code == 390114) 134 | // Authentication token expired, re-authenticate 135 | await InitNewSessionAsync(ct).ConfigureAwait(false); 136 | else 137 | throw new SnowflakeException($"Renew session failed. Message: {response.Message}", response.Code); 138 | 139 | _requestBuilder.SetSessionTokens(_snowflakeSession.SessionToken, _snowflakeSession.MasterToken); 140 | 141 | return true; 142 | } 143 | 144 | /// 145 | /// Execute SQL that selects a single value. 146 | /// 147 | /// The SQL to execute. 148 | /// The parameters to use for this command. 149 | /// The first cell value returned as string. 150 | public async Task ExecuteScalarAsync(string sql, object sqlParams = null, CancellationToken ct = default) 151 | { 152 | var response = await QueryInternalAsync(sql, sqlParams, false, ct).ConfigureAwait(false); 153 | var rawResult = response.Data.RowSet.FirstOrDefault()?.FirstOrDefault(); 154 | 155 | return rawResult; 156 | } 157 | 158 | /// 159 | /// Execute SQL that selects a single value. 160 | /// 161 | /// The SQL to execute. 162 | /// The parameters to use for this command. 163 | /// The first cell value returned as of type T. 164 | public async Task ExecuteScalarAsync(string sql, object sqlParams = null, CancellationToken ct = default) 165 | { 166 | var response = await QueryInternalAsync(sql, sqlParams, false, ct).ConfigureAwait(false); 167 | 168 | var firstColumn = response.Data.RowType.FirstOrDefault(); 169 | var firstColumnValue = response.Data.RowSet.FirstOrDefault()?.FirstOrDefault(); 170 | 171 | var result = SnowflakeDataMapper.MapTo(firstColumn, firstColumnValue); 172 | return result; 173 | } 174 | 175 | /// 176 | /// Execute parameterized SQL. 177 | /// 178 | /// The SQL to execute for this query. 179 | /// The parameters to use for this query. 180 | /// The number of rows affected. 181 | public async Task ExecuteAsync(string sql, object sqlParams = null, CancellationToken ct = default) 182 | { 183 | var response = await QueryInternalAsync(sql, sqlParams, false, ct).ConfigureAwait(false); 184 | var affectedRows = SnowflakeUtils.GetAffectedRowsCount(response); 185 | 186 | return affectedRows; 187 | } 188 | 189 | /// 190 | /// Executes a query, returning the data typed as . 191 | /// 192 | /// The type of results to return. 193 | /// The SQL to execute. 194 | /// The parameters to use for this command. 195 | /// A sequence of data of the supplied type: one instance per row. 196 | public async Task> QueryAsync(string sql, object sqlParams = null, CancellationToken ct = default) 197 | { 198 | var response = await QueryInternalAsync(sql, sqlParams, false, ct).ConfigureAwait(false); 199 | 200 | var rowSet = response.Data.RowSet; 201 | 202 | if (response.Data.Chunks != null && response.Data.Chunks.Count > 0) 203 | { 204 | var chunksDownloadInfo = new ChunksDownloadInfo() 205 | { 206 | ChunkHeaders = response.Data.ChunkHeaders, 207 | Chunks = response.Data.Chunks, 208 | Qrmk = response.Data.Qrmk 209 | }; 210 | var parsedRowSet = await ChunksDownloader.DownloadAndParseChunksAsync(chunksDownloadInfo, ct).ConfigureAwait(false); 211 | rowSet.AddRange(parsedRowSet); 212 | } 213 | 214 | var result = SnowflakeDataMapper.MapTo(response.Data.RowType, rowSet); 215 | return result; 216 | } 217 | 218 | /// 219 | /// Executes a query, returning the raw data returned by REST API (rows, columns and query information). 220 | /// 221 | /// The SQL to execute. 222 | /// The parameters to use for this command. 223 | /// Return only columns information. 224 | /// Rows and columns. 225 | public async Task QueryRawResponseAsync(string sql, object sqlParams = null, bool describeOnly = false, CancellationToken ct = default) 226 | { 227 | var response = await QueryInternalAsync(sql, sqlParams, describeOnly, ct).ConfigureAwait(false); 228 | 229 | if (_clientSettings.DownloadChunksForQueryRawResponses 230 | && response.Data.Chunks != null && response.Data.Chunks.Count > 0) 231 | { 232 | var rowSet = response.Data.RowSet; 233 | var chunksDownloadInfo = new ChunksDownloadInfo() 234 | { 235 | ChunkHeaders = response.Data.ChunkHeaders, 236 | Chunks = response.Data.Chunks, 237 | Qrmk = response.Data.Qrmk 238 | }; 239 | var parsedRowSet = await ChunksDownloader.DownloadAndParseChunksAsync(chunksDownloadInfo, ct).ConfigureAwait(false); 240 | rowSet.AddRange(parsedRowSet); 241 | } 242 | 243 | return new SnowflakeQueryRawResponse(response.Data); 244 | } 245 | 246 | /// 247 | /// Cancels running query 248 | /// 249 | /// Request ID to cancel. 250 | public async Task CancelQueryAsync(string requestId, CancellationToken ct = default) 251 | { 252 | var cancelQueryRequest = _requestBuilder.BuildCancelQueryRequest(requestId); 253 | 254 | var response = await _restClient.SendAsync(cancelQueryRequest, ct).ConfigureAwait(false); 255 | 256 | if (!response.Success) 257 | throw new SnowflakeException($"Cancelling query failed. Message: {response.Message}", response.Code); 258 | 259 | return true; 260 | } 261 | 262 | private async Task QueryInternalAsync(string sql, object sqlParams = null, bool describeOnly = false, CancellationToken ct = default) 263 | { 264 | if (_snowflakeSession == null) 265 | { 266 | await InitNewSessionAsync(ct).ConfigureAwait(false); 267 | } 268 | 269 | var queryRequest = _requestBuilder.BuildQueryRequest(sql, sqlParams, describeOnly); 270 | var response = await _restClient.SendAsync(queryRequest, ct).ConfigureAwait(false); 271 | 272 | // Auto renew session, if it's expired 273 | if (response.Code == 390112) 274 | { 275 | await RenewSessionAsync(ct).ConfigureAwait(false); 276 | 277 | // A new instance of HttpQueryRequest should be created for every request 278 | queryRequest = _requestBuilder.BuildQueryRequest(sql, sqlParams, describeOnly); 279 | response = await _restClient.SendAsync(queryRequest, ct).ConfigureAwait(false); 280 | } 281 | 282 | // If query execution takes more than 45 seconds we will get this 283 | if (response.Code == 333334 || response.Code == 333333) 284 | { 285 | response = await RepeatUntilQueryCompleted(response.Data.GetResultUrl, ct).ConfigureAwait(false); 286 | } 287 | 288 | if (!response.Success) 289 | throw new SnowflakeException($"Query execution failed. Message: {response.Message}", response.Code); 290 | 291 | return response; 292 | } 293 | 294 | private async Task RepeatUntilQueryCompleted(string getResultUrl, CancellationToken ct = default) 295 | { 296 | var lastResultUrl = getResultUrl; 297 | QueryExecResponse response; 298 | do 299 | { 300 | var getResultRequest = _requestBuilder.BuildGetResultRequest(lastResultUrl); 301 | response = await _restClient.SendAsync(getResultRequest, ct).ConfigureAwait(false); 302 | 303 | if (response.Code == 390112) 304 | { 305 | await RenewSessionAsync(ct).ConfigureAwait(false); 306 | } 307 | else 308 | { 309 | lastResultUrl = response.Data?.GetResultUrl; 310 | } 311 | } while (response.Code == 333334 || response.Code == 333333 || response.Code == 390112); 312 | 313 | return response; 314 | } 315 | 316 | /// 317 | /// Closes current Snowflake session. 318 | /// 319 | /// True if session was successfully closed. 320 | public async Task CloseSessionAsync(CancellationToken ct = default) 321 | { 322 | var closeSessionRequest = _requestBuilder.BuildCloseSessionRequest(); 323 | var response = await _restClient.SendAsync(closeSessionRequest, ct).ConfigureAwait(false); 324 | 325 | _snowflakeSession = null; 326 | _requestBuilder.ClearSessionTokens(); 327 | 328 | if (!response.Success) 329 | throw new SnowflakeException($"Closing session failed. Message: {response.Message}", response.Code); 330 | 331 | return response.Success; 332 | } 333 | 334 | /// 335 | /// Overrides internal HttpClient 336 | /// 337 | public void SetHttpClient(HttpClient httpClient) 338 | { 339 | if (httpClient == null) 340 | throw new ArgumentException("HttpClient cannot be null."); 341 | 342 | _restClient.SetHttpClient(httpClient); 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /Snowflake.Client/SnowflakeDataMapper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.IO; 2 | using Snowflake.Client.Helpers; 3 | using Snowflake.Client.Json; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Text; 8 | using System.Text.Json; 9 | 10 | namespace Snowflake.Client 11 | { 12 | public static class SnowflakeDataMapper 13 | { 14 | private const int DefaultBufferSize = 1024; 15 | private static readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); 16 | private static readonly Encoding __utf8EncodingNoBom = new UTF8Encoding(false); 17 | private static JsonSerializerOptions __jsonMapperOptions = new JsonSerializerOptions(); 18 | 19 | public static void Configure(JsonSerializerOptions jsonMapperOptions) 20 | { 21 | if (jsonMapperOptions != null) 22 | __jsonMapperOptions = jsonMapperOptions; 23 | } 24 | 25 | [Obsolete("Please use Configure method instead")] 26 | public static void SetJsonMapperOptions(JsonSerializerOptions jsonMapperOptions) 27 | { 28 | Configure(jsonMapperOptions); 29 | } 30 | 31 | public static T MapTo(ColumnDescription column, string value) 32 | { 33 | if (column == null) 34 | throw new ArgumentNullException(nameof(column)); 35 | 36 | // Get a Recyclable memory stream to write the json content into. 37 | using (MemoryStream ms = _recyclableMemoryStreamManager.GetStream()) 38 | { 39 | // Create a stream writer that will encode characters to utf8 with no byte-order-mark. 40 | using(StreamWriter sw = new StreamWriter(ms, __utf8EncodingNoBom, DefaultBufferSize, true)) 41 | { 42 | // Write the JSON into the stream writer. 43 | ConvertColumnValueToJsonToken(value, column.Type, sw); 44 | 45 | // Dispose of the stream writer to flush all character into the memory stream. 46 | } 47 | 48 | // Reset the stream's position to the start, ready for reading the json content that was just 49 | // written into it. 50 | ms.Position = 0; 51 | 52 | // Deserialize the JSON to the required object type. 53 | T val = JsonSerializer.Deserialize(ms, __jsonMapperOptions); 54 | return val; 55 | } 56 | } 57 | 58 | public static IEnumerable MapTo(List columns, List> rows) 59 | { 60 | if (columns == null || columns.Count == 0) 61 | throw new ArgumentNullException(nameof(columns)); 62 | 63 | if (rows == null) 64 | throw new ArgumentNullException(nameof(rows)); 65 | 66 | // Get a RecyclableMemoryStream to write the json content into. 67 | using (MemoryStream ms = _recyclableMemoryStreamManager.GetStream()) 68 | { 69 | // Create a stream writer that will encode characters to utf8 with no byte-order-mark. 70 | using(StreamWriter sw = new StreamWriter(ms, __utf8EncodingNoBom, DefaultBufferSize, true)) 71 | { 72 | foreach(var rowRecord in rows) 73 | { 74 | // Write the JSON into the stream writer. 75 | BuildJsonString(columns, rowRecord, sw); 76 | 77 | // Flush any buffered content into the memory stream. 78 | sw.Flush(); 79 | 80 | // Reset the stream's position to the start, ready for reading the json content that was just 81 | // written into it. 82 | ms.Position = 0; 83 | 84 | // Deserialize the JSON to the required object type. 85 | T val = JsonSerializer.Deserialize(ms, __jsonMapperOptions); 86 | yield return val; 87 | 88 | // Reset the memory stream for re-use in the next loop. 89 | ms.SetLength(0); 90 | } 91 | } 92 | } 93 | } 94 | 95 | private static void BuildJsonString(List columns, List rowRecord, TextWriter tw) 96 | { 97 | // Append json opening brace. 98 | tw.Write('{'); 99 | 100 | if (columns.Count != 0) 101 | { 102 | // Append first property. 103 | AppendAsJsonProperty(columns[0].Name, rowRecord[0], columns[0].Type, tw); 104 | 105 | // Append all other properties, prefixed with a comma to separate from previous property. 106 | for (int i = 1; i < columns.Count; i++) 107 | { 108 | tw.Write(","); 109 | AppendAsJsonProperty(columns[i].Name, rowRecord[i], columns[i].Type, tw); 110 | } 111 | } 112 | 113 | // Append json closing brace. 114 | tw.Write('}'); 115 | } 116 | 117 | private static void AppendAsJsonProperty( 118 | string propertyName, 119 | string columnValue, 120 | string columnType, 121 | TextWriter tw) 122 | { 123 | // Append property name and colon separator. 124 | tw.Write('"'); 125 | tw.Write(propertyName); 126 | tw.Write("\":"); 127 | 128 | // Append json property value. 129 | ConvertColumnValueToJsonToken(columnValue, columnType, tw); 130 | } 131 | 132 | private static void ConvertColumnValueToJsonToken( 133 | string value, 134 | string columnType, 135 | TextWriter tw) 136 | { 137 | if(value is null || value == "null") 138 | { 139 | tw.Write("null"); 140 | return; 141 | } 142 | 143 | switch(columnType) 144 | { 145 | case "text": 146 | tw.Write(JsonSerializer.Serialize(value)); 147 | break; 148 | 149 | case "fixed": 150 | case "real": 151 | tw.Write(value); 152 | break; 153 | 154 | case "boolean": 155 | tw.Write(value == "1" || value.Equals("true", StringComparison.OrdinalIgnoreCase) ? "true" : "false"); 156 | break; 157 | 158 | case "date": 159 | case "time": 160 | case "timestamp_ntz": 161 | tw.Write('"'); 162 | tw.Write(SnowflakeTypesConverter.ConvertToDateTime(value, columnType).ToString("o")); 163 | tw.Write('"'); 164 | break; 165 | 166 | case "timestamp_ltz": 167 | case "timestamp_tz": 168 | tw.Write('"'); 169 | tw.Write(SnowflakeTypesConverter.ConvertToDateTimeOffset(value, columnType).ToString("o")); 170 | tw.Write('"'); 171 | break; 172 | 173 | case "object": 174 | case "variant": 175 | case "array": 176 | tw.Write(value); 177 | break; 178 | 179 | case "binary": 180 | tw.Write('"'); 181 | HexUtils.HexToBase64(value, tw); 182 | tw.Write('"'); 183 | break; 184 | 185 | default: 186 | tw.Write(value); 187 | break; 188 | } 189 | } 190 | } 191 | } -------------------------------------------------------------------------------- /Snowflake.Client/SnowflakeTypesConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Snowflake.Client.Model; 4 | 5 | namespace Snowflake.Client 6 | { 7 | /// 8 | /// Based on https://github.com/snowflakedb/snowflake-connector-net/blob/master/Snowflake.Data/Core/SFDataConverter.cs 9 | /// 10 | internal static class SnowflakeTypesConverter 11 | { 12 | private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Unspecified); 13 | 14 | internal static DateTime ConvertToDateTime(string value, string snowflakeType) 15 | { 16 | switch (snowflakeType) 17 | { 18 | case "date": 19 | var srcValLong = long.Parse(value); 20 | return UnixEpoch.AddDays(srcValLong); 21 | 22 | case "time": // to timespan? https://github.com/snowflakedb/snowflake-connector-net/issues/327 23 | // https://github.com/snowflakedb/snowflake-connector-net/commit/1fa03d92cdf6f7ae5720fdef8ecf25371f0f4c95 24 | case "timestamp_ntz": 25 | var secAndNsecTuple = ExtractTimestamp(value); 26 | var tickDiff = secAndNsecTuple.Item1 * 10000000L + secAndNsecTuple.Item2 / 100L; 27 | return UnixEpoch.AddTicks(tickDiff); 28 | 29 | default: 30 | throw new SnowflakeException($"Conversion from {snowflakeType} to DateTime is not supported."); 31 | } 32 | } 33 | 34 | internal static DateTimeOffset ConvertToDateTimeOffset(string value, string snowflakeType) 35 | { 36 | switch (snowflakeType) 37 | { 38 | case "timestamp_tz": 39 | var spaceIndex = value.IndexOf(' '); 40 | var offset = int.Parse(value.Substring(spaceIndex + 1, value.Length - spaceIndex - 1)); 41 | var offSetTimespan = new TimeSpan((offset - 1440) / 60, 0, 0); 42 | 43 | var secAndNsecTzPair = ExtractTimestamp(value.Substring(0, spaceIndex)); 44 | var ticksTz = (secAndNsecTzPair.Item1 * 1000 * 1000 * 1000 + secAndNsecTzPair.Item2) / 100; 45 | return new DateTimeOffset(UnixEpoch.Ticks + ticksTz, TimeSpan.Zero).ToOffset(offSetTimespan); 46 | 47 | case "timestamp_ltz": 48 | var secAndNsecLtzPair = ExtractTimestamp(value); 49 | var ticksLtz = (secAndNsecLtzPair.Item1 * 1000 * 1000 * 1000 + secAndNsecLtzPair.Item2) / 100; 50 | return new DateTimeOffset(UnixEpoch.Ticks + ticksLtz, TimeSpan.Zero).ToLocalTime(); 51 | 52 | default: 53 | throw new SnowflakeException($"Conversion from {snowflakeType} to DateTimeOffset is not supported."); 54 | } 55 | } 56 | 57 | internal static string ConvertToTimestampTz(DateTimeOffset value) 58 | { 59 | var ticksPart = value.UtcTicks - UnixEpoch.Ticks; 60 | var minutesPart = value.Offset.TotalMinutes + 1440; 61 | 62 | return $"{ticksPart}00 {minutesPart}"; 63 | } 64 | 65 | internal static string ConvertToTimestampNtz(DateTime value) 66 | { 67 | var diff = value.Subtract(UnixEpoch); 68 | 69 | return $"{diff.Ticks}00"; 70 | } 71 | 72 | internal static string BytesToHex(byte[] bytes) 73 | { 74 | var hexBuilder = new StringBuilder(bytes.Length * 2); 75 | foreach (var b in bytes) 76 | { 77 | hexBuilder.AppendFormat("{0:x2}", b); 78 | } 79 | 80 | return hexBuilder.ToString(); 81 | } 82 | 83 | private static Tuple ExtractTimestamp(string srcVal) 84 | { 85 | var dotIndex = srcVal.IndexOf('.'); 86 | 87 | if (dotIndex == -1) 88 | return Tuple.Create(long.Parse(srcVal), 0L); 89 | 90 | var intPart = long.Parse(srcVal.Substring(0, dotIndex)); 91 | var decimalPartLength = srcVal.Length - dotIndex - 1; 92 | var decimalPartStr = srcVal.Substring(dotIndex + 1, decimalPartLength); 93 | var decimalPart = long.Parse(decimalPartStr); 94 | 95 | // If the decimal part contained less than nine characters, we must convert the value to nanoseconds by 96 | // multiplying by 10^[precision difference]. 97 | if (decimalPartLength < 9) 98 | { 99 | decimalPart *= (int)Math.Pow(10, 9 - decimalPartLength); 100 | } 101 | 102 | return Tuple.Create(intPart, decimalPart); 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /SnowflakeClient.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30413.136 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snowflake.Client", "Snowflake.Client\Snowflake.Client.csproj", "{2D0DF1AE-9CF2-4D1E-9D05-C5063D2BB4C5}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snowflake.Client.Tests", "Snowflake.Client.Tests\Snowflake.Client.Tests.csproj", "{B9B59C25-9601-4AFC-812E-8A9228CEBC2F}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snowflake.Client.Benchmarks", "Snowflake.Client.Benchmarks\Snowflake.Client.Benchmarks.csproj", "{C9F6FB48-6340-4407-A466-4545BA003C6C}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {2D0DF1AE-9CF2-4D1E-9D05-C5063D2BB4C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {2D0DF1AE-9CF2-4D1E-9D05-C5063D2BB4C5}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {2D0DF1AE-9CF2-4D1E-9D05-C5063D2BB4C5}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {2D0DF1AE-9CF2-4D1E-9D05-C5063D2BB4C5}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {B9B59C25-9601-4AFC-812E-8A9228CEBC2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {B9B59C25-9601-4AFC-812E-8A9228CEBC2F}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {B9B59C25-9601-4AFC-812E-8A9228CEBC2F}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {B9B59C25-9601-4AFC-812E-8A9228CEBC2F}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {C9F6FB48-6340-4407-A466-4545BA003C6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {C9F6FB48-6340-4407-A466-4545BA003C6C}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {C9F6FB48-6340-4407-A466-4545BA003C6C}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {C9F6FB48-6340-4407-A466-4545BA003C6C}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {8AFB3956-A4D2-4B9A-80A9-A84D834EBD33} 36 | EndGlobalSection 37 | EndGlobal 38 | --------------------------------------------------------------------------------