├── .github └── workflows │ ├── build.yaml │ └── myget_release.yaml ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── README.md ├── ReleaseNotes └── v1.0.0-beta.3.md ├── SciSharp.MySQL.Replication.sln ├── src └── SciSharp.MySQL.Replication │ ├── BinlogPosition.cs │ ├── ChecksumType.cs │ ├── ColumnMetadata.cs │ ├── ColumnType.cs │ ├── DefaultCharset.cs │ ├── Events │ ├── CellValue.cs │ ├── DefaultEventFactory.cs │ ├── DeleteRowsEvent.cs │ ├── EmptyPayloadEvent.cs │ ├── EnumTypeTableMetadataInitializer.cs │ ├── ErrorEvent.cs │ ├── FormatDescriptionEvent.cs │ ├── ILogEventFactory.cs │ ├── ITableMetadataInitializer.cs │ ├── LogEvent.cs │ ├── LogEventFlag.cs │ ├── LogEventType.cs │ ├── NotImplementedEvent.cs │ ├── NumericTypeTableMetadataInitializer.cs │ ├── QueryEvent.cs │ ├── RotateEvent.cs │ ├── RowEventFlags.cs │ ├── RowSet.cs │ ├── RowsEvent.cs │ ├── SetTypeTableMetadataInitializer.cs │ ├── TableMapEvent.cs │ ├── UpdateRowsEvent.cs │ ├── WriteRowsEvent.cs │ └── XIDEvent.cs │ ├── IReplicationClient.cs │ ├── LogEventPackageDecoder.cs │ ├── LogEventPipelineFilter.cs │ ├── LoginResult.cs │ ├── ReplicationClient.cs │ ├── ReplicationState.cs │ ├── SciSharp.MySQL.Replication.csproj │ ├── SequenceReaderExtensions.cs │ ├── TableMetadata.cs │ ├── TableSchema.cs │ └── Types │ ├── BitType.cs │ ├── BlobType.cs │ ├── ColumnTypeExtensions.cs │ ├── DateTimeType.cs │ ├── DateTimeV2Type.cs │ ├── DateType.cs │ ├── DoubleType.cs │ ├── EnumType.cs │ ├── FloatType.cs │ ├── GeometryType.cs │ ├── IColumnMetadataLoader.cs │ ├── IMySQLDataType.cs │ ├── Int24Type.cs │ ├── JsonType.cs │ ├── LongLongType.cs │ ├── LongType.cs │ ├── NewDecimalType.cs │ ├── SetType.cs │ ├── ShortType.cs │ ├── StringType.cs │ ├── TimeBaseType.cs │ ├── TimeType.cs │ ├── TimeV2Type.cs │ ├── TimestampType.cs │ ├── TimestampV2Type.cs │ ├── TinyType.cs │ ├── VarCharType.cs │ └── YearType.cs ├── tests └── Test │ ├── BinlogPositionTest.cs │ ├── DataTypesTest.cs │ ├── MainTest.cs │ ├── MySQLFixture.cs │ ├── Test.csproj │ ├── docker-compose.yml │ ├── dump.sql │ ├── mysql.cnf │ └── xunit.runner.json └── version.json /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Setup .NET Core 14 | uses: actions/setup-dotnet@v3 15 | with: 16 | dotnet-version: '9.0.x' 17 | - name: Set env 18 | run: | 19 | echo "DOTNET_CLI_TELEMETRY_OPTOUT=1" >> $GITHUB_ENV 20 | echo "DOTNET_hostBuilder:reloadConfigOnChange=false" >> $GITHUB_ENV 21 | - uses: dotnet/nbgv@master 22 | id: nbgv 23 | - name: Clean 24 | run: | 25 | dotnet clean ./SciSharp.MySQL.Replication.sln --configuration Release 26 | dotnet nuget locals all --clear 27 | - name: Build 28 | run: dotnet build -c Debug 29 | - name: Run MySQL 30 | run: | 31 | cp tests/Test/mysql.cnf ~/.my.cnf 32 | sudo systemctl start mysql.service 33 | mysql -V 34 | mysql -e "SET PERSIST binlog_row_metadata = 'FULL';" -uroot -proot 35 | mysql -uroot -proot < tests/Test/dump.sql 36 | - name: Test 37 | run: | 38 | cd tests/Test 39 | dotnet test 40 | - name: Pack 41 | run: dotnet pack -c Release -p:PackageVersion=${{ steps.nbgv.outputs.NuGetPackageVersion }}.${{ github.run_number }} -p:Version=${{ steps.nbgv.outputs.NuGetPackageVersion }}.${{ github.run_number }} -p:AssemblyVersion=${{ steps.nbgv.outputs.AssemblyVersion }} -p:AssemblyFileVersion=${{ steps.nbgv.outputs.AssemblyFileVersion }} -p:AssemblyInformationalVersion=${{ steps.nbgv.outputs.AssemblyInformationalVersion }} /p:NoPackageAnalysis=true /p:IncludeReleaseNotes=false 42 | - name: Push 43 | run: dotnet nuget push **/*.nupkg --api-key ${{ secrets.MYGET_API_KEY }} --source https://www.myget.org/F/scisharp/api/v3/index.json -------------------------------------------------------------------------------- /.github/workflows/myget_release.yaml: -------------------------------------------------------------------------------- 1 | name: myget-release 2 | on: [workflow_dispatch] 3 | jobs: 4 | push: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v1 8 | - name: Setup .NET Core 9 | uses: actions/setup-dotnet@v3 10 | with: 11 | dotnet-version: '9.0.x' 12 | - name: Set env 13 | run: echo "DOTNET_CLI_TELEMETRY_OPTOUT=1" >> $GITHUB_ENV 14 | - uses: dotnet/nbgv@master 15 | id: nbgv 16 | - name: Pack 17 | run: dotnet pack -c Release -p:PackageVersion=${{ steps.nbgv.outputs.NuGetPackageVersion }} -p:Version=${{ steps.nbgv.outputs.NuGetPackageVersion }}.${{ github.run_number }} -p:AssemblyVersion=${{ steps.nbgv.outputs.AssemblyVersion }} -p:AssemblyFileVersion=${{ steps.nbgv.outputs.AssemblyFileVersion }} -p:AssemblyInformationalVersion=${{ steps.nbgv.outputs.AssemblyInformationalVersion }} /p:NoPackageAnalysis=true /p:IncludeReleaseNotes=true 18 | - name: Push 19 | run: dotnet nuget push **/*.nupkg --api-key ${{ secrets.MYGET_API_KEY }} --source https://www.myget.org/F/scisharp/api/v3/index.json -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | mono: none 3 | dotnet: 3.0.100 4 | sudo: required 5 | script: 6 | - dotnet restore 7 | - dotnet build 8 | global: 9 | - DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true 10 | - DOTNET_CLI_TELEMETRY_OPTOUT=1 -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/tests/Test/bin/Debug/netcoreapp3.0/Test.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}/tests/Test", 15 | "console": "internalConsole", 16 | "stopAtEntry": false 17 | }, 18 | { 19 | "name": ".NET Core Attach", 20 | "type": "coreclr", 21 | "request": "attach", 22 | "processId": "${command:pickProcess}" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/tests/Test/Test.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/tests/Test/Test.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/tests/Test/Test.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /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 | # dotnet-mysql-replication 2 | 3 | [![build](https://github.com/SciSharp/dotnet-mysql-replication/actions/workflows/build.yaml/badge.svg)](https://github.com/SciSharp/dotnet-mysql-replication/actions/workflows/build.yaml) 4 | [![MyGet Version](https://img.shields.io/myget/scisharp/vpre/SciSharp.MySQL.Replication)](https://www.myget.org/feed/scisharp/package/nuget/SciSharp.MySQL.Replication) 5 | [![NuGet Version](https://img.shields.io/nuget/v/SciSharp.MySQL.Replication.svg?style=flat)](https://www.nuget.org/packages/SciSharp.MySQL.Replication/) 6 | 7 | A C# Implementation of MySQL replication protocol client 8 | 9 | This library allows you to receive events like insert, update, delete with their data and raw SQL queries from MySQL. 10 | 11 | ## Features 12 | 13 | - Connect to MySQL server as a replica 14 | - Parse and process binary log events in real-time 15 | - Support for all MySQL data types including JSON, BLOB, TEXT, etc. 16 | - Handle various events including: 17 | - Query events (raw SQL statements) 18 | - Table maps 19 | - Row events (insert, update, delete) 20 | - Format description events 21 | - Rotate events 22 | - XID events (transaction identifiers) 23 | - Checksum verification support 24 | - Built-in support for MySQL binary format parsing 25 | - Async/await first design 26 | - Track and save binary log position 27 | - Start replication from a specific binary log position 28 | 29 | ## Requirements 30 | 31 | - .NET 6.0+ or .NET Core 3.1+ 32 | - MySQL server with binary logging enabled 33 | - MySQL user with replication privileges 34 | 35 | ## Installation 36 | 37 | ``` 38 | dotnet add package SciSharp.MySQL.Replication 39 | ``` 40 | 41 | ## Basic Usage 42 | 43 | ```csharp 44 | using SciSharp.MySQL.Replication; 45 | 46 | var serverHost = "localhost"; 47 | var username = "root"; 48 | var password = "scisharp"; 49 | var serverId = 1; // replication server id 50 | 51 | var client = new ReplicationClient(); 52 | var result = await client.ConnectAsync(serverHost, username, password, serverId); 53 | 54 | if (!result.Result) 55 | { 56 | Console.WriteLine($"Failed to connect: {result.Message}."); 57 | return; 58 | } 59 | 60 | client.PackageHandler += (s, p) => 61 | { 62 | Console.WriteLine(p.ToString()); 63 | Console.WriteLine(); 64 | } 65 | 66 | client.StartReceive(); 67 | 68 | // Keep your application running to receive events 69 | // ... 70 | 71 | await client.CloseAsync(); 72 | ``` 73 | 74 | ## Using Async Stream API 75 | 76 | You can use the modern C# async stream pattern to process MySQL events using `GetEventLogStream()`: 77 | 78 | ```csharp 79 | using SciSharp.MySQL.Replication; 80 | using SciSharp.MySQL.Replication.Events; 81 | 82 | var client = new ReplicationClient(); 83 | var result = await client.ConnectAsync("localhost", "root", "password", 1); 84 | 85 | if (!result.Result) 86 | { 87 | Console.WriteLine($"Failed to connect: {result.Message}."); 88 | return; 89 | } 90 | 91 | // Process events as they arrive using await foreach 92 | await foreach (var logEvent in client.GetEventLogStream()) 93 | { 94 | switch (logEvent) 95 | { 96 | case WriteRowsEvent writeEvent: 97 | Console.WriteLine($"INSERT on table: {writeEvent.TableId}"); 98 | break; 99 | 100 | case UpdateRowsEvent updateEvent: 101 | Console.WriteLine($"UPDATE on table: {updateEvent.TableId}"); 102 | break; 103 | 104 | case QueryEvent queryEvent: 105 | Console.WriteLine($"SQL Query: {queryEvent.Query}"); 106 | break; 107 | 108 | // Handle other event types as needed 109 | } 110 | } 111 | 112 | await client.CloseAsync(); 113 | ``` 114 | 115 | This approach is useful for: 116 | - Modern C# applications using .NET Core 3.0+ 117 | - Processing events sequentially in a more fluent, readable way 118 | - Easier integration with async/await patterns 119 | - Avoiding event handler callback complexity 120 | 121 | ## Position Tracking and Custom Starting Position 122 | 123 | You can track the current binary log position and start from a specific position: 124 | 125 | ```csharp 126 | using SciSharp.MySQL.Replication; 127 | 128 | var client = new ReplicationClient(); 129 | 130 | // Track position changes 131 | client.PositionChanged += (sender, position) => 132 | { 133 | Console.WriteLine($"Current position: {position}"); 134 | // Save position to a file, database, etc. 135 | File.WriteAllText("binlog-position.txt", $"{position.Filename}:{position.Position}"); 136 | }; 137 | 138 | // Start from a specific position 139 | var startPosition = new BinlogPosition("mysql-bin.000001", 4); 140 | var result = await client.ConnectAsync("localhost", "root", "password", 1, startPosition); 141 | 142 | // Get current position at any time 143 | var currentPosition = client.CurrentPosition; 144 | Console.WriteLine($"Current log file: {currentPosition.Filename}, position: {currentPosition.Position}"); 145 | ``` 146 | 147 | ## Advanced Usage 148 | 149 | ### Working with Specific Events 150 | 151 | ```csharp 152 | using SciSharp.MySQL.Replication; 153 | using SciSharp.MySQL.Replication.Events; 154 | 155 | var client = new ReplicationClient(); 156 | // ... connect to MySQL 157 | 158 | client.PackageHandler += (s, e) => 159 | { 160 | switch (e) 161 | { 162 | case WriteRowsEvent writeEvent: 163 | Console.WriteLine($"INSERT on table: {writeEvent.TableId}"); 164 | foreach (var row in writeEvent.Rows) 165 | { 166 | // Process inserted rows 167 | foreach (var cell in row.Cells) 168 | { 169 | Console.WriteLine($" Column: {cell.ColumnIndex}, Value: {cell.Value}"); 170 | } 171 | } 172 | break; 173 | 174 | case UpdateRowsEvent updateEvent: 175 | Console.WriteLine($"UPDATE on table: {updateEvent.TableId}"); 176 | foreach (var row in updateEvent.Rows) 177 | { 178 | // Process before/after values for updated rows 179 | Console.WriteLine(" Before update:"); 180 | foreach (var cell in row.BeforeUpdate) 181 | { 182 | Console.WriteLine($" Column: {cell.ColumnIndex}, Value: {cell.Value}"); 183 | } 184 | Console.WriteLine(" After update:"); 185 | foreach (var cell in row.AfterUpdate) 186 | { 187 | Console.WriteLine($" Column: {cell.ColumnIndex}, Value: {cell.Value}"); 188 | } 189 | } 190 | break; 191 | 192 | case DeleteRowsEvent deleteEvent: 193 | Console.WriteLine($"DELETE on table: {deleteEvent.TableId}"); 194 | foreach (var row in deleteEvent.Rows) 195 | { 196 | // Process deleted rows 197 | foreach (var cell in row.Cells) 198 | { 199 | Console.WriteLine($" Column: {cell.ColumnIndex}, Value: {cell.Value}"); 200 | } 201 | } 202 | break; 203 | 204 | case QueryEvent queryEvent: 205 | Console.WriteLine($"SQL Query: {queryEvent.Query}"); 206 | Console.WriteLine($"Database: {queryEvent.Schema}"); 207 | break; 208 | 209 | case RotateEvent rotateEvent: 210 | Console.WriteLine($"Rotating to new binary log: {rotateEvent.NextBinlogFileName}"); 211 | Console.WriteLine($"New position: {rotateEvent.RotatePosition}"); 212 | break; 213 | } 214 | }; 215 | 216 | client.StartReceive(); 217 | ``` 218 | 219 | ### Setting Up MySQL for Replication 220 | 221 | 1. Enable binary logging in your MySQL server's `my.cnf` or `my.ini`: 222 | 223 | ``` 224 | [mysqld] 225 | server-id=1 226 | log-bin=mysql-bin 227 | binlog_format=ROW 228 | ``` 229 | 230 | 2. Create a user with replication privileges: 231 | 232 | ```sql 233 | CREATE USER 'replication_user'@'%' IDENTIFIED BY 'password'; 234 | GRANT REPLICATION SLAVE ON *.* TO 'replication_user'@'%'; 235 | FLUSH PRIVILEGES; 236 | ``` 237 | 238 | ### Logging 239 | 240 | You can provide a logger for detailed diagnostics: 241 | 242 | ```csharp 243 | using Microsoft.Extensions.Logging; 244 | 245 | // Create a logger factory 246 | var loggerFactory = LoggerFactory.Create(builder => 247 | { 248 | builder.AddConsole(); 249 | builder.AddDebug(); 250 | }); 251 | 252 | var client = new ReplicationClient(); 253 | client.Logger = loggerFactory.CreateLogger(); 254 | ``` 255 | 256 | ## License 257 | 258 | This project is licensed under the MIT License - see the LICENSE file for details. 259 | -------------------------------------------------------------------------------- /ReleaseNotes/v1.0.0-beta.3.md: -------------------------------------------------------------------------------- 1 | # SciSharp.MySQL.Replication 1.0.0-beta.3 2 | 3 | ## Overview 4 | SciSharp.MySQL.Replication is a C# implementation of the MySQL replication protocol client. This library allows you to receive events like insert, update, delete with their data and raw SQL queries directly from MySQL. 5 | 6 | ## What's New in 1.0.0-beta.3 7 | - Added support for .NET 9.0 8 | - Improved handling of binary log events 9 | - Enhanced parsing of JSON type data 10 | - Fixed issues with timestamp conversion 11 | - Performance improvements for large data sets 12 | - Better error handling and reporting 13 | - Updated dependencies to latest stable versions 14 | 15 | ## Key Features 16 | - Connect to MySQL server as a replica 17 | - Parse and process binary log events in real-time 18 | - Support for all MySQL data types including JSON, BLOB, TEXT, etc. 19 | - Handle various events including: 20 | - Query events (raw SQL statements) 21 | - Table maps 22 | - Row events (insert, update, delete) 23 | - Format description events 24 | - Rotate events 25 | - XID events (transaction identifiers) 26 | - Checksum verification support 27 | - Built-in support for MySQL binary format parsing 28 | - Async/await first design 29 | 30 | ## Requirements 31 | - .NET 6.0+ or higher 32 | - MySQL server with binary logging enabled 33 | - MySQL user with replication privileges 34 | 35 | ## Getting Started 36 | ```csharp 37 | using SciSharp.MySQL.Replication; 38 | 39 | var serverHost = "localhost"; 40 | var username = "root"; 41 | var password = "your_password"; 42 | var serverId = 1; // replication server id 43 | 44 | var client = new ReplicationClient(); 45 | var result = await client.ConnectAsync(serverHost, username, password, serverId); 46 | 47 | if (!result.Result) 48 | { 49 | Console.WriteLine($"Failed to connect: {result.Message}."); 50 | return; 51 | } 52 | 53 | client.PackageHandler += (s, p) => 54 | { 55 | Console.WriteLine(p.ToString()); 56 | Console.WriteLine(); 57 | } 58 | 59 | client.StartReceive(); 60 | 61 | // Keep your application running to receive events 62 | // ... 63 | 64 | await client.CloseAsync(); 65 | ``` 66 | 67 | For more detailed documentation and examples, please visit our [GitHub repository](https://github.com/SciSharp/dotnet-mysql-replication). -------------------------------------------------------------------------------- /SciSharp.MySQL.Replication.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A3989C2D-CD30-4034-BD92-01B840838FC6}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SciSharp.MySQL.Replication", "src\SciSharp.MySQL.Replication\SciSharp.MySQL.Replication.csproj", "{42257915-DB08-4065-88FD-D2EBDE52D952}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AC6AC2C9-6D03-4605-8805-E27908679352}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "tests\Test\Test.csproj", "{8629BD0E-CE93-4932-A16C-48C33C4CC5E4}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Debug|x64 = Debug|x64 18 | Debug|x86 = Debug|x86 19 | Release|Any CPU = Release|Any CPU 20 | Release|x64 = Release|x64 21 | Release|x86 = Release|x86 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {42257915-DB08-4065-88FD-D2EBDE52D952}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {42257915-DB08-4065-88FD-D2EBDE52D952}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {42257915-DB08-4065-88FD-D2EBDE52D952}.Debug|x64.ActiveCfg = Debug|Any CPU 30 | {42257915-DB08-4065-88FD-D2EBDE52D952}.Debug|x64.Build.0 = Debug|Any CPU 31 | {42257915-DB08-4065-88FD-D2EBDE52D952}.Debug|x86.ActiveCfg = Debug|Any CPU 32 | {42257915-DB08-4065-88FD-D2EBDE52D952}.Debug|x86.Build.0 = Debug|Any CPU 33 | {42257915-DB08-4065-88FD-D2EBDE52D952}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {42257915-DB08-4065-88FD-D2EBDE52D952}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {42257915-DB08-4065-88FD-D2EBDE52D952}.Release|x64.ActiveCfg = Release|Any CPU 36 | {42257915-DB08-4065-88FD-D2EBDE52D952}.Release|x64.Build.0 = Release|Any CPU 37 | {42257915-DB08-4065-88FD-D2EBDE52D952}.Release|x86.ActiveCfg = Release|Any CPU 38 | {42257915-DB08-4065-88FD-D2EBDE52D952}.Release|x86.Build.0 = Release|Any CPU 39 | {8629BD0E-CE93-4932-A16C-48C33C4CC5E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {8629BD0E-CE93-4932-A16C-48C33C4CC5E4}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {8629BD0E-CE93-4932-A16C-48C33C4CC5E4}.Debug|x64.ActiveCfg = Debug|Any CPU 42 | {8629BD0E-CE93-4932-A16C-48C33C4CC5E4}.Debug|x64.Build.0 = Debug|Any CPU 43 | {8629BD0E-CE93-4932-A16C-48C33C4CC5E4}.Debug|x86.ActiveCfg = Debug|Any CPU 44 | {8629BD0E-CE93-4932-A16C-48C33C4CC5E4}.Debug|x86.Build.0 = Debug|Any CPU 45 | {8629BD0E-CE93-4932-A16C-48C33C4CC5E4}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {8629BD0E-CE93-4932-A16C-48C33C4CC5E4}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {8629BD0E-CE93-4932-A16C-48C33C4CC5E4}.Release|x64.ActiveCfg = Release|Any CPU 48 | {8629BD0E-CE93-4932-A16C-48C33C4CC5E4}.Release|x64.Build.0 = Release|Any CPU 49 | {8629BD0E-CE93-4932-A16C-48C33C4CC5E4}.Release|x86.ActiveCfg = Release|Any CPU 50 | {8629BD0E-CE93-4932-A16C-48C33C4CC5E4}.Release|x86.Build.0 = Release|Any CPU 51 | EndGlobalSection 52 | GlobalSection(NestedProjects) = preSolution 53 | {42257915-DB08-4065-88FD-D2EBDE52D952} = {A3989C2D-CD30-4034-BD92-01B840838FC6} 54 | {8629BD0E-CE93-4932-A16C-48C33C4CC5E4} = {AC6AC2C9-6D03-4605-8805-E27908679352} 55 | EndGlobalSection 56 | EndGlobal 57 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/BinlogPosition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SciSharp.MySQL.Replication 4 | { 5 | /// 6 | /// Represents a position in a MySQL binary log file. 7 | /// 8 | public class BinlogPosition 9 | { 10 | /// 11 | /// Gets or sets the filename of the binary log. 12 | /// 13 | public string Filename { get; set; } 14 | 15 | /// 16 | /// Gets or sets the position within the binary log file. 17 | /// 18 | public int Position { get; set; } 19 | 20 | /// 21 | /// Initializes a new instance of the class. 22 | /// 23 | public BinlogPosition() 24 | { 25 | } 26 | 27 | /// 28 | /// Initializes a new instance of the class by copying another instance. 29 | /// 30 | /// The other binlogPosition. 31 | public BinlogPosition(BinlogPosition binlogPosition) 32 | { 33 | Filename = binlogPosition.Filename; 34 | Position = binlogPosition.Position; 35 | } 36 | 37 | /// 38 | /// Initializes a new instance of the class. 39 | /// 40 | /// The binary log filename. 41 | /// The position within the binary log file. 42 | public BinlogPosition(string filename, int position) 43 | { 44 | Filename = filename ?? throw new ArgumentNullException(nameof(filename)); 45 | Position = position; 46 | } 47 | 48 | /// 49 | /// Returns a string representation of the binlog position. 50 | /// 51 | /// A string containing the filename and position. 52 | public override string ToString() 53 | { 54 | return $"{Filename}:{Position}"; 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/ChecksumType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | 5 | namespace SciSharp.MySQL.Replication 6 | { 7 | /// 8 | /// Defines the types of checksums used in MySQL replication events. 9 | /// 10 | public enum ChecksumType : int 11 | { 12 | /// 13 | /// No checksum is used. 14 | /// 15 | NONE = 0, 16 | 17 | /// 18 | /// CRC32 checksum algorithm is used. 19 | /// 20 | CRC32 = 4 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/ColumnMetadata.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SciSharp.MySQL.Replication 4 | { 5 | /// 6 | /// Represents metadata information for a database column. 7 | /// 8 | public class ColumnMetadata 9 | { 10 | /// 11 | /// Gets or sets the name of the column. 12 | /// 13 | public string Name { get; set; } 14 | 15 | /// 16 | /// Gets or sets the column type. 17 | /// 18 | public ColumnType Type { get; set; } 19 | 20 | /// 21 | /// Gets or sets the underlying type of the column. 22 | /// 23 | public ColumnType? UnderlyingType { get; set; } 24 | 25 | /// 26 | /// Gets or sets the metadata value associated with the column. 27 | /// 28 | public byte[] MetadataValue { get; set; } 29 | 30 | /// 31 | /// Gets or sets the column max value length. 32 | /// 33 | public int MaxLength { get; set; } 34 | 35 | /// 36 | /// Gets or sets the character set ID for the column. 37 | /// 38 | public int CharsetId { get; set; } 39 | 40 | /// 41 | /// Gets or sets the set of possible values for an ENUM column type. 42 | /// 43 | public IReadOnlyList EnumValues { get; set; } 44 | 45 | /// 46 | /// Gets or sets the set of possible values for a SET column type. 47 | /// 48 | public IReadOnlyList SetValues { get; set; } 49 | 50 | /// 51 | /// Gets or sets a value indicating whether the column is visible. 52 | /// 53 | public bool IsVisible { get; set; } 54 | 55 | /// 56 | /// Gets or sets a value indicating whether the column is unsigned. 57 | /// 58 | public bool IsUnsigned { get; set; } 59 | 60 | /// 61 | /// Gets or sets the additional options of this column. 62 | /// 63 | public object Options { get; set; } 64 | } 65 | } -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/ColumnType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | 5 | namespace SciSharp.MySQL.Replication 6 | { 7 | /// 8 | /// Represents the MySQL column data types as they appear in the binary log format. 9 | /// These values correspond to the type codes used in TABLE_MAP_EVENT to describe table columns. 10 | /// 11 | /// 12 | /// The values match those defined in the MySQL source code in the enum_field_types enumeration, 13 | /// which can be found in mysql-server/include/mysql_com.h. 14 | /// 15 | public enum ColumnType : byte 16 | { 17 | /// 18 | /// MySQL DECIMAL type (old precision-based format, rarely used). 19 | /// 20 | DECIMAL = 0, 21 | 22 | /// 23 | /// MySQL TINY (TINYINT) type, 1-byte integer. 24 | /// 25 | TINY = 1, 26 | 27 | /// 28 | /// MySQL SHORT (SMALLINT) type, 2-byte integer. 29 | /// 30 | SHORT = 2, 31 | 32 | /// 33 | /// MySQL LONG (INT) type, 4-byte integer. 34 | /// 35 | LONG = 3, 36 | 37 | /// 38 | /// MySQL FLOAT type, 4-byte single-precision floating point. 39 | /// 40 | FLOAT = 4, 41 | 42 | /// 43 | /// MySQL DOUBLE type, 8-byte double-precision floating point. 44 | /// 45 | DOUBLE = 5, 46 | 47 | /// 48 | /// MySQL NULL type, represents NULL values. 49 | /// 50 | NULL = 6, 51 | 52 | /// 53 | /// MySQL TIMESTAMP type, represents a point in time. 54 | /// 55 | TIMESTAMP = 7, 56 | 57 | /// 58 | /// MySQL LONGLONG (BIGINT) type, 8-byte integer. 59 | /// 60 | LONGLONG = 8, 61 | 62 | /// 63 | /// MySQL INT24 (MEDIUMINT) type, 3-byte integer. 64 | /// 65 | INT24 = 9, 66 | 67 | /// 68 | /// MySQL DATE type, represents a calendar date. 69 | /// 70 | DATE = 10, 71 | 72 | /// 73 | /// MySQL TIME type, represents a time of day. 74 | /// 75 | TIME = 11, 76 | 77 | /// 78 | /// MySQL DATETIME type, represents a combined date and time. 79 | /// 80 | DATETIME = 12, 81 | 82 | /// 83 | /// MySQL YEAR type, 1-byte representation of a year. 84 | /// 85 | YEAR = 13, 86 | 87 | /// 88 | /// MySQL NEWDATE type, new internal representation of DATE (rarely used externally). 89 | /// 90 | NEWDATE = 14, 91 | 92 | /// 93 | /// MySQL VARCHAR type, variable-length character string. 94 | /// 95 | VARCHAR = 15, 96 | 97 | /// 98 | /// MySQL BIT type, for storing bit values. 99 | /// 100 | BIT = 16, 101 | 102 | /// 103 | /// MySQL TIMESTAMP2 type, timestamp with fractional seconds, introduced in MySQL 5.6.4. 104 | /// 105 | TIMESTAMP_V2 = 17, 106 | 107 | /// 108 | /// MySQL DATETIME2 type, datetime with fractional seconds, introduced in MySQL 5.6.4. 109 | /// 110 | DATETIME_V2 = 18, 111 | 112 | /// 113 | /// MySQL TIME2 type, time with fractional seconds, introduced in MySQL 5.6.4. 114 | /// 115 | TIME_V2 = 19, 116 | 117 | /// 118 | /// MySQL JSON type for storing JSON documents, introduced in MySQL 5.7. 119 | /// 120 | JSON = 245, 121 | 122 | /// 123 | /// MySQL NEWDECIMAL type, new precision-based decimal implementation. 124 | /// 125 | NEWDECIMAL = 246, 126 | 127 | /// 128 | /// MySQL ENUM type, enumeration of string values. 129 | /// 130 | ENUM = 247, 131 | 132 | /// 133 | /// MySQL SET type, string object that can have zero or more values. 134 | /// 135 | SET = 248, 136 | 137 | /// 138 | /// MySQL TINY_BLOB type, small binary object. 139 | /// 140 | TINY_BLOB = 249, 141 | 142 | /// 143 | /// MySQL MEDIUM_BLOB type, medium-sized binary object. 144 | /// 145 | MEDIUM_BLOB = 250, 146 | 147 | /// 148 | /// MySQL LONG_BLOB type, large binary object. 149 | /// 150 | LONG_BLOB = 251, 151 | 152 | /// 153 | /// MySQL BLOB type, binary large object. 154 | /// 155 | BLOB = 252, 156 | 157 | /// 158 | /// MySQL VAR_STRING type, variable-length string (deprecated, VARCHAR is used instead). 159 | /// 160 | VAR_STRING = 253, 161 | 162 | /// 163 | /// MySQL STRING type, fixed-length string. 164 | /// 165 | STRING = 254, 166 | 167 | /// 168 | /// MySQL GEOMETRY type, for storing geometric data. 169 | /// 170 | GEOMETRY = 255 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/DefaultCharset.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SciSharp.MySQL.Replication 5 | { 6 | /// 7 | /// Represents charset information for MySQL replication. 8 | /// 9 | public class DefaultCharset 10 | { 11 | /// 12 | /// Gets or sets the default collation ID for the charset. 13 | /// 14 | public int DefaultCharsetCollation { get; set; } 15 | 16 | /// 17 | /// Gets or sets a dictionary mapping charset IDs to their corresponding collation IDs. 18 | /// 19 | public Dictionary CharsetCollations { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/CellValue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SciSharp.MySQL.Replication.Events 4 | { 5 | /// 6 | /// Represents a value of a cell in a row, containing both old and new values. 7 | /// Used in replication events to track changes between states of a row. 8 | /// 9 | public class CellValue 10 | { 11 | /// 12 | /// Gets or sets the old value of the cell. 13 | /// 14 | public object OldValue { get; set; } 15 | 16 | /// 17 | /// Gets or sets the new value of the cell. 18 | /// 19 | public object NewValue { get; set; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/DefaultEventFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | 4 | namespace SciSharp.MySQL.Replication.Events 5 | { 6 | /// 7 | /// Default implementation of the log event factory that creates events of a specified type. 8 | /// 9 | /// The type of log event to create. 10 | class DefaultEventFactory : ILogEventFactory 11 | where TEventType : LogEvent, new() 12 | { 13 | /// 14 | /// Creates a new instance of a log event. 15 | /// 16 | /// The context information for creating the event. 17 | /// A new instance of the specified log event type. 18 | public LogEvent Create(object context) 19 | { 20 | return new TEventType(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/DeleteRowsEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using SuperSocket.ProtoBase; 7 | 8 | namespace SciSharp.MySQL.Replication.Events 9 | { 10 | /// 11 | /// Represents a MySQL binary log event that contains rows deleted from a table. 12 | /// This event is generated for a delete operation on a row in a MySQL table. 13 | /// 14 | public sealed class DeleteRowsEvent : RowsEvent 15 | { 16 | public DeleteRowsEvent() 17 | : base() 18 | { 19 | 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/EmptyPayloadEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | 4 | namespace SciSharp.MySQL.Replication.Events 5 | { 6 | /// 7 | /// Represents a log event with an empty payload. 8 | /// This is used for events that don't contain any additional data beyond their headers. 9 | /// 10 | public sealed class EmptyPayloadEvent : LogEvent 11 | { 12 | /// 13 | /// Decodes the body of the empty payload event, which contains no data. 14 | /// 15 | /// The sequence reader containing the binary data. 16 | /// The context for decoding. 17 | protected internal override void DecodeBody(ref SequenceReader reader, object context) 18 | { 19 | 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/EnumTypeTableMetadataInitializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SciSharp.MySQL.Replication.Types; 3 | 4 | namespace SciSharp.MySQL.Replication.Events 5 | { 6 | class EnumTypeTableMetadataInitializer : ITableMetadataInitializer 7 | { 8 | public void InitializeMetadata(TableMetadata metadata) 9 | { 10 | var enumColumnIndex = 0; 11 | 12 | foreach (var column in metadata.Columns) 13 | { 14 | if (!IsEnumColumn(column)) 15 | continue; 16 | 17 | column.EnumValues = metadata.EnumStrValues[enumColumnIndex++]; 18 | } 19 | } 20 | 21 | private bool IsEnumColumn(ColumnMetadata columnMetadata) 22 | { 23 | if (columnMetadata.Type == ColumnType.ENUM) 24 | return true; 25 | 26 | if (columnMetadata.Type != ColumnType.STRING) 27 | return false; 28 | 29 | var meta0 = columnMetadata.MetadataValue[0]; 30 | 31 | if (meta0 != (byte)ColumnType.ENUM) 32 | return false; 33 | 34 | columnMetadata.UnderlyingType = ColumnType.ENUM; 35 | return true; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/ErrorEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Text; 4 | using SuperSocket.ProtoBase; 5 | 6 | namespace SciSharp.MySQL.Replication.Events 7 | { 8 | /// 9 | /// Represents an error event in the MySQL binary log. 10 | /// Contains information about errors that occurred during the replication process. 11 | /// 12 | public sealed class ErrorEvent : LogEvent 13 | { 14 | /// 15 | /// Gets the error code associated with this error event. 16 | /// 17 | public short ErrorCode { get; private set; } 18 | 19 | /// 20 | /// Gets the SQL state code, a standard error code consisting of five characters. 21 | /// 22 | public string SqlState { get; private set; } 23 | 24 | /// 25 | /// Gets the human-readable error message. 26 | /// 27 | public String ErrorMessage { get; private set; } 28 | 29 | /// 30 | /// Decodes the body of the error event from the binary log. 31 | /// 32 | /// The sequence reader containing the binary data. 33 | /// The context for decoding. 34 | protected internal override void DecodeBody(ref SequenceReader reader, object context) 35 | { 36 | reader.TryReadLittleEndian(out short errorCode); 37 | 38 | ErrorCode = errorCode; 39 | 40 | reader.TryPeek(out byte checkValue); 41 | 42 | if (checkValue == '#') 43 | { 44 | reader.Advance(1); 45 | SqlState = reader.Sequence.Slice(reader.Consumed, 5).GetString(Encoding.UTF8); 46 | reader.Advance(5); 47 | } 48 | 49 | ErrorMessage = reader.Sequence.Slice(reader.Consumed).GetString(Encoding.UTF8); 50 | } 51 | 52 | /// 53 | /// Returns a string representation of the error event. 54 | /// 55 | /// A string containing the event type, SQL state, and error message. 56 | public override string ToString() 57 | { 58 | return $"{EventType.ToString()}\r\nSqlState: {SqlState}\r\nErrorMessage: {ErrorMessage}"; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/FormatDescriptionEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Text; 4 | using SuperSocket.ProtoBase; 5 | 6 | namespace SciSharp.MySQL.Replication.Events 7 | { 8 | /// 9 | /// Represents a MySQL FORMAT_DESCRIPTION_EVENT that describes the format of the binary log. 10 | /// This event appears at the beginning of each binary log file and provides information 11 | /// about the server version and header lengths for each event type. 12 | /// https://dev.mysql.com/doc/internals/en/format-description-event.html 13 | /// 14 | public sealed class FormatDescriptionEvent : LogEvent 15 | { 16 | /// 17 | /// Gets or sets the binary log format version. 18 | /// 19 | public short BinlogVersion { get; set; } 20 | 21 | /// 22 | /// Gets or sets the MySQL server version string. 23 | /// 24 | public string ServerVersion { get; set; } 25 | 26 | /// 27 | /// Gets or sets the timestamp when the binary log was created. 28 | /// 29 | public DateTime CreateTimestamp { get; set; } 30 | 31 | /// 32 | /// Gets or sets the length of the event header. 33 | /// 34 | public byte EventHeaderLength { get; set; } 35 | 36 | /// 37 | /// Gets or sets the array of event type header lengths. 38 | /// Each index corresponds to a LogEventType and contains the header length for that type. 39 | /// 40 | public byte[] EventTypeHeaderLengths { get; set; } 41 | 42 | private string ReadServerVersion(ref SequenceReader reader, int len) 43 | { 44 | ReadOnlySequence seq; 45 | 46 | if (reader.TryReadTo(out seq, 0x00, false)) 47 | { 48 | if (seq.Length > len) 49 | { 50 | seq = seq.Slice(0, len); 51 | reader.Rewind(len - seq.Length); 52 | } 53 | 54 | var version = seq.GetString(Encoding.UTF8); 55 | 56 | if (seq.Length < len) 57 | reader.Advance(len - seq.Length); 58 | 59 | return version; 60 | } 61 | else 62 | { 63 | seq = reader.Sequence.Slice(reader.Consumed, len); 64 | var version = seq.GetString(Encoding.UTF8); 65 | reader.Advance(len); 66 | 67 | return version; 68 | } 69 | } 70 | 71 | /// 72 | /// Decodes the body of the event from the binary representation. 73 | /// 74 | /// The sequence reader containing the binary data. 75 | /// The context for decoding. 76 | protected internal override void DecodeBody(ref SequenceReader reader, object context) 77 | { 78 | reader.TryReadLittleEndian(out short version); 79 | BinlogVersion = version; 80 | 81 | ServerVersion = ReadServerVersion(ref reader, 50); 82 | 83 | reader.TryReadLittleEndian(out int seconds); 84 | CreateTimestamp = LogEvent.GetTimestampFromUnixEpoch(seconds); 85 | 86 | reader.TryRead(out byte eventLen); 87 | EventHeaderLength = eventLen; 88 | 89 | var eventTypeHeaderLens = new byte[reader.Remaining]; 90 | 91 | for (var i = 0; i < eventTypeHeaderLens.Length; i++) 92 | { 93 | reader.TryRead(out eventLen); 94 | eventTypeHeaderLens[i] = eventLen; 95 | } 96 | 97 | EventTypeHeaderLengths = eventTypeHeaderLens; 98 | } 99 | 100 | /// 101 | /// Returns a string representation of the FormatDescriptionEvent. 102 | /// 103 | /// A string containing event information. 104 | public override string ToString() 105 | { 106 | return $"{EventType.ToString()}\r\nBinlogVersion: {BinlogVersion}\r\nServerVersion: {ServerVersion}"; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/ILogEventFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | 5 | namespace SciSharp.MySQL.Replication.Events 6 | { 7 | /// 8 | /// Interface for factories that create log events from context objects. 9 | /// 10 | interface ILogEventFactory 11 | { 12 | /// 13 | /// Creates a log event from the provided context. 14 | /// 15 | /// The context object containing data to create the log event. 16 | /// A new log event instance. 17 | LogEvent Create(object context); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/ITableMetadataInitializer.cs: -------------------------------------------------------------------------------- 1 | namespace SciSharp.MySQL.Replication.Events 2 | { 3 | interface ITableMetadataInitializer 4 | { 5 | void InitializeMetadata(TableMetadata tableMetadata); 6 | } 7 | } -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/LogEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using SciSharp.MySQL.Replication.Types; 4 | 5 | namespace SciSharp.MySQL.Replication.Events 6 | { 7 | /// 8 | /// Base class for all MySQL binary log events. 9 | /// 10 | public abstract class LogEvent 11 | { 12 | /// 13 | /// Gets or sets the checksum type used for log events. 14 | /// 15 | public static ChecksumType ChecksumType { get; internal set; } 16 | 17 | /// 18 | /// Gets or sets the timestamp when the event was created. 19 | /// 20 | public DateTime Timestamp { get; set; } 21 | 22 | /// 23 | /// Gets or sets the type of the log event. 24 | /// 25 | public LogEventType EventType { get; internal set; } 26 | 27 | /// 28 | /// Gets or sets the server ID that generated the event. 29 | /// 30 | public int ServerID { get; set; } 31 | 32 | /// 33 | /// Gets or sets the total size of the event in bytes. 34 | /// 35 | public int EventSize { get; set; } 36 | 37 | /// 38 | /// Gets or sets the position of the event in the binary log. 39 | /// 40 | public int Position { get; set; } 41 | 42 | /// 43 | /// Gets or sets the flags associated with the event. 44 | /// 45 | public LogEventFlag Flags { get; set; } 46 | 47 | /// 48 | /// Gets or sets the array of MySQL data type handlers. 49 | /// 50 | internal static IMySQLDataType[] DataTypes { get; private set; } = new IMySQLDataType[256]; 51 | 52 | /// 53 | /// Static constructor to initialize data type handlers. 54 | /// 55 | static LogEvent() 56 | { 57 | DataTypes[(int)ColumnType.BIT] = new BitType(); 58 | DataTypes[(int)ColumnType.TINY] = new TinyType(); 59 | DataTypes[(int)ColumnType.SHORT] = new ShortType(); 60 | DataTypes[(int)ColumnType.INT24] = new Int24Type(); 61 | DataTypes[(int)ColumnType.LONG] = new LongType(); 62 | DataTypes[(int)ColumnType.LONGLONG] = new LongLongType(); 63 | DataTypes[(int)ColumnType.FLOAT] = new FloatType(); 64 | DataTypes[(int)ColumnType.DOUBLE] = new DoubleType(); 65 | DataTypes[(int)ColumnType.NEWDECIMAL] = new NewDecimalType(); 66 | DataTypes[(int)ColumnType.DATE] = new DateType(); 67 | DataTypes[(int)ColumnType.STRING] = new StringType(); 68 | DataTypes[(int)ColumnType.VARCHAR] = new VarCharType(); 69 | DataTypes[(int)ColumnType.DATETIME] = new DateTimeType(); 70 | DataTypes[(int)ColumnType.DATETIME_V2] = new DateTimeV2Type(); 71 | DataTypes[(int)ColumnType.TIME] = new TimeType(); 72 | DataTypes[(int)ColumnType.TIME_V2] = new TimeV2Type(); 73 | DataTypes[(int)ColumnType.TIMESTAMP] = new TimestampType(); 74 | DataTypes[(int)ColumnType.TIMESTAMP_V2] = new TimestampV2Type(); 75 | DataTypes[(int)ColumnType.ENUM] = new EnumType(); 76 | DataTypes[(int)ColumnType.SET] = new SetType(); 77 | DataTypes[(int)ColumnType.BLOB] = new BlobType(); 78 | DataTypes[(int)ColumnType.YEAR] = new YearType(); 79 | DataTypes[(int)ColumnType.JSON] = new JsonType(); 80 | } 81 | 82 | /// 83 | /// Decodes the body of the log event from the binary data. 84 | /// 85 | /// The sequence reader containing binary data. 86 | /// The context object containing additional information. 87 | protected internal abstract void DecodeBody(ref SequenceReader reader, object context); 88 | 89 | /// 90 | /// Maria DB slave capability flag for GTID support. 91 | /// 92 | public const int MARIA_SLAVE_CAPABILITY_GTID = 4; 93 | 94 | /// 95 | /// Maria DB slave capability flags used by this implementation. 96 | /// 97 | public const int MARIA_SLAVE_CAPABILITY_MINE = MARIA_SLAVE_CAPABILITY_GTID; 98 | 99 | /// 100 | /// The Unix epoch reference time (1970-01-01). 101 | /// 102 | private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1); 103 | 104 | /// 105 | /// Converts Unix timestamp (seconds since epoch) to DateTime. 106 | /// 107 | /// Seconds since Unix epoch. 108 | /// Corresponding DateTime. 109 | internal static DateTime GetTimestampFromUnixEpoch(int seconds) 110 | { 111 | return _unixEpoch.AddSeconds(seconds); 112 | } 113 | 114 | /// 115 | /// Gets or sets a value indicating whether this event has a CRC checksum. 116 | /// 117 | protected bool HasCRC { get; set; } = false; 118 | 119 | /// 120 | /// Rebuilds the reader to exclude the CRC checksum from the data if needed. 121 | /// 122 | /// The sequence reader to modify. 123 | /// True if the reader was modified, otherwise false. 124 | protected bool RebuildReaderAsCRC(ref SequenceReader reader) 125 | { 126 | if (!HasCRC || ChecksumType == ChecksumType.NONE) 127 | return false; 128 | 129 | reader = new SequenceReader(reader.Sequence.Slice(reader.Consumed, reader.Remaining - (int)ChecksumType)); 130 | return true; 131 | } 132 | 133 | /// 134 | /// Returns a string that represents the current object. 135 | /// 136 | /// A string that represents the current object. 137 | public override string ToString() 138 | { 139 | return EventType.ToString(); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/LogEventFlag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SciSharp.MySQL.Replication.Events 4 | { 5 | /// 6 | /// Defines the flags used in binary log events. 7 | /// See: https://dev.mysql.com/doc/internals/en/binlog-event-flag.html 8 | /// 9 | [Flags] 10 | public enum LogEventFlag : short 11 | { 12 | /// 13 | /// Indicates a binlog is in use. 14 | /// 15 | LOG_EVENT_BINLOG_IN_USE_F = 0x001, 16 | 17 | /// 18 | /// Indicates a forced rotation of the binlog. 19 | /// 20 | LOG_EVENT_FORCED_ROTATE_F = 0x0002, 21 | 22 | /// 23 | /// Indicates the event is thread specific. 24 | /// 25 | LOG_EVENT_THREAD_SPECIFIC_F = 0x0004, 26 | 27 | /// 28 | /// Indicates to suppress the USE command. 29 | /// 30 | LOG_EVENT_SUPPRESS_USE_F = 0x0008, 31 | 32 | /// 33 | /// Indicates that table map version is updated. 34 | /// 35 | LOG_EVENT_UPDATE_TABLE_MAP_VERSION_F = 0x0010, 36 | 37 | /// 38 | /// Indicates the event was artificially generated. 39 | /// 40 | LOG_EVENT_ARTIFICIAL_F = 0x0020, 41 | 42 | /// 43 | /// Indicates the event is a relay log event. 44 | /// 45 | LOG_EVENT_RELAY_LOG_F = 0x0040, 46 | 47 | /// 48 | /// Indicates the event can be safely ignored. 49 | /// 50 | LOG_EVENT_IGNORABLE_F = 0x0080, 51 | 52 | /// 53 | /// Indicates no filtering should be applied to the event. 54 | /// 55 | LOG_EVENT_NO_FILTER_F = 0x0100, 56 | 57 | /// 58 | /// Indicates the event should be isolated in MTS operation. 59 | /// 60 | LOG_EVENT_MTS_ISOLATE_F = 0x0200 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/LogEventType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SciSharp.MySQL.Replication.Events 4 | { 5 | /// 6 | /// Represents the different types of events in a MySQL binary log. 7 | /// See: https://dev.mysql.com/doc/internals/en/binlog-event-type.html 8 | /// 9 | public enum LogEventType : byte 10 | { 11 | /// Unknown event type 12 | UNKNOWN_EVENT= 0, 13 | /// Start event version 3 14 | START_EVENT_V3= 1, 15 | /// Query event containing SQL statement 16 | QUERY_EVENT= 2, 17 | /// Stop event 18 | STOP_EVENT= 3, 19 | /// Rotate event indicating a new binary log 20 | ROTATE_EVENT= 4, 21 | /// Internal variable event 22 | INTVAR_EVENT= 5, 23 | /// Load data event 24 | LOAD_EVENT= 6, 25 | /// Slave event 26 | SLAVE_EVENT= 7, 27 | /// Create file event 28 | CREATE_FILE_EVENT= 8, 29 | /// Append block event 30 | APPEND_BLOCK_EVENT= 9, 31 | /// Execute load event 32 | EXEC_LOAD_EVENT= 10, 33 | /// Delete file event 34 | DELETE_FILE_EVENT= 11, 35 | /// New load event 36 | NEW_LOAD_EVENT= 12, 37 | /// Random value event 38 | RAND_EVENT= 13, 39 | /// User variable event 40 | USER_VAR_EVENT= 14, 41 | /// Format description event containing binary log format information 42 | FORMAT_DESCRIPTION_EVENT= 15, 43 | /// XID transaction identifier event 44 | XID_EVENT= 16, 45 | /// Begin load query event 46 | BEGIN_LOAD_QUERY_EVENT= 17, 47 | /// Execute load query event 48 | EXECUTE_LOAD_QUERY_EVENT= 18, 49 | /// Table map event describing a table structure 50 | TABLE_MAP_EVENT = 19, 51 | /// Write rows event version 0 52 | WRITE_ROWS_EVENT_V0 = 20, 53 | /// Update rows event version 0 54 | UPDATE_ROWS_EVENT_V0 = 21, 55 | /// Delete rows event version 0 56 | DELETE_ROWS_EVENT_V0 = 22, 57 | /// Write rows event version 1 58 | WRITE_ROWS_EVENT_V1 = 23, 59 | /// Update rows event version 1 60 | UPDATE_ROWS_EVENT_V1 = 24, 61 | /// Delete rows event version 1 62 | DELETE_ROWS_EVENT_V1 = 25, 63 | /// Incident event indicating a server issue 64 | INCIDENT_EVENT= 26, 65 | /// Heartbeat log event 66 | HEARTBEAT_LOG_EVENT= 27, 67 | /// Ignorable log event 68 | IGNORABLE_LOG_EVENT= 28, 69 | /// Rows query log event containing the original SQL statement 70 | ROWS_QUERY_LOG_EVENT= 29, 71 | /// Write rows event (current version) 72 | WRITE_ROWS_EVENT = 30, 73 | /// Update rows event (current version) 74 | UPDATE_ROWS_EVENT = 31, 75 | /// Delete rows event (current version) 76 | DELETE_ROWS_EVENT = 32, 77 | /// GTID (Global Transaction ID) log event 78 | GTID_LOG_EVENT= 33, 79 | /// Anonymous GTID log event 80 | ANONYMOUS_GTID_LOG_EVENT= 34, 81 | /// Previous GTIDs log event 82 | PREVIOUS_GTIDS_LOG_EVENT= 35 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/NotImplementedEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | 4 | namespace SciSharp.MySQL.Replication.Events 5 | { 6 | /// 7 | /// Represents a placeholder for log event types that have not been implemented yet. 8 | /// This event is used when the system encounters an event type that it recognizes 9 | /// but doesn't have specific handling logic for. 10 | /// 11 | public sealed class NotImplementedEvent : LogEvent 12 | { 13 | /// 14 | /// Decodes the body of the not implemented event. This implementation is intentionally 15 | /// empty as the event doesn't process any content. 16 | /// 17 | /// The sequence reader containing the binary data. 18 | /// The context for decoding. 19 | protected internal override void DecodeBody(ref SequenceReader reader, object context) 20 | { 21 | 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/NumericTypeTableMetadataInitializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SciSharp.MySQL.Replication.Types; 3 | 4 | namespace SciSharp.MySQL.Replication.Events 5 | { 6 | class NumericTypeTableMetadataInitializer : ITableMetadataInitializer 7 | { 8 | public void InitializeMetadata(TableMetadata metadata) 9 | { 10 | var numericColumnIndex = 0; 11 | 12 | foreach (var column in metadata.Columns) 13 | { 14 | if (!column.Type.IsNumberColumn()) 15 | continue; 16 | 17 | column.IsUnsigned = metadata.Signedness[numericColumnIndex]; 18 | numericColumnIndex++; 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/QueryEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Text; 4 | 5 | namespace SciSharp.MySQL.Replication.Events 6 | { 7 | /// 8 | /// Represents a MySQL QUERY_EVENT that contains SQL statements executed on the server. 9 | /// This event is generated for statements like CREATE, ALTER, INSERT, UPDATE, DELETE, etc. 10 | /// that are not using row-based replication format. 11 | /// 12 | public sealed class QueryEvent : LogEvent 13 | { 14 | /// 15 | /// Gets or sets the ID of the thread that executed the query. 16 | /// 17 | public int SlaveProxyID { get; private set; } 18 | 19 | /// 20 | /// Gets or sets the time in seconds the query took to execute. 21 | /// 22 | public DateTime ExecutionTime { get; private set; } 23 | 24 | /// 25 | /// Gets or sets the error code returned by the query execution. 26 | /// A value of 0 indicates successful execution. 27 | /// 28 | public short ErrorCode { get; private set; } 29 | 30 | /// 31 | /// Gets or sets the status variables block. 32 | /// 33 | public string StatusVars { get; private set; } 34 | 35 | /// 36 | /// Gets or sets the database name that was active when the query was executed. 37 | /// 38 | public string Schema { get; private set; } 39 | 40 | /// 41 | /// Gets or sets the SQL query text that was executed. 42 | /// 43 | public String Query { get; private set; } 44 | 45 | /// 46 | /// Initializes a new instance of the class. 47 | /// 48 | public QueryEvent() 49 | { 50 | this.HasCRC = true; 51 | } 52 | 53 | /// 54 | /// Decodes the body of the event from the binary representation. 55 | /// 56 | /// The sequence reader containing the binary data. 57 | /// The context for decoding. 58 | protected internal override void DecodeBody(ref SequenceReader reader, object context) 59 | { 60 | reader.TryReadLittleEndian(out int slaveProxyID); 61 | SlaveProxyID = slaveProxyID; 62 | 63 | reader.TryReadLittleEndian(out int seconds); 64 | ExecutionTime = LogEvent.GetTimestampFromUnixEpoch(seconds); 65 | 66 | reader.TryRead(out byte schemaLen); 67 | 68 | reader.TryReadLittleEndian(out short errorCode); 69 | ErrorCode = errorCode; 70 | 71 | reader.TryReadLittleEndian(out short statusVarsLen); 72 | 73 | StatusVars = reader.ReadString(Encoding.UTF8, statusVarsLen); 74 | 75 | Schema = reader.ReadString(Encoding.UTF8, schemaLen); 76 | 77 | reader.Advance(1); //0x00 78 | 79 | this.RebuildReaderAsCRC(ref reader); 80 | 81 | Query = reader.ReadString(); 82 | } 83 | 84 | /// 85 | /// Returns a string representation of the QueryEvent. 86 | /// 87 | /// A string containing the event type, database name, and SQL query. 88 | public override string ToString() 89 | { 90 | return $"{EventType.ToString()}\r\nSlaveProxyID: {SlaveProxyID}\r\nExecutionTime: {ExecutionTime}\r\nErrorCode: {ErrorCode}\r\nStatusVars: {StatusVars}\r\nSchema: {Schema}\r\nQuery: {Query}"; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/RotateEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Text; 4 | using SuperSocket.ProtoBase; 5 | 6 | namespace SciSharp.MySQL.Replication.Events 7 | { 8 | /// 9 | /// Represents a MySQL ROTATE_EVENT that indicates a switch to a new binary log file. 10 | /// This event is generated when the master server switches to a new binary log file, 11 | /// either because the current file reached the max size or due to a manual rotation. 12 | /// https://dev.mysql.com/doc/internals/en/rotate-event.html 13 | /// 14 | public sealed class RotateEvent : LogEvent 15 | { 16 | /// 17 | /// Gets or sets the position in the new binary log file where the next event starts. 18 | /// 19 | public long RotatePosition { get; set; } 20 | 21 | /// 22 | /// Gets or sets the name of the new binary log file. 23 | /// 24 | public string NextBinlogFileName { get; set; } 25 | 26 | /// 27 | /// Decodes the body of the event from the binary representation. 28 | /// 29 | /// The sequence reader containing the binary data. 30 | /// The context for decoding. 31 | protected internal override void DecodeBody(ref SequenceReader reader, object context) 32 | { 33 | reader.TryReadLittleEndian(out long position); 34 | RotatePosition = position; 35 | 36 | var binglogFileNameSize = reader.Remaining - (int)LogEvent.ChecksumType; 37 | 38 | NextBinlogFileName = reader.Sequence.Slice(reader.Consumed, binglogFileNameSize).GetString(Encoding.UTF8); 39 | reader.Advance(binglogFileNameSize); 40 | } 41 | 42 | /// 43 | /// Returns a string representation of the RotateEvent. 44 | /// 45 | /// A string containing the event type, new filename, and position information. 46 | public override string ToString() 47 | { 48 | return $"{EventType.ToString()}\r\nRotatePosition: {RotatePosition}\r\nNextBinlogFileName: {NextBinlogFileName}"; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/RowEventFlags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SciSharp.MySQL.Replication.Events 4 | { 5 | /// 6 | /// Specifies flags that provide additional information about rows events in MySQL replication. 7 | /// These flags describe configuration settings and state information for row operations. 8 | /// 9 | [Flags] 10 | public enum RowsEventFlags : byte 11 | { 12 | /// 13 | /// Indicates the end of a statement. 14 | /// 15 | EndOfStatement = 0x01, 16 | 17 | /// 18 | /// Indicates that foreign key checks are disabled. 19 | /// 20 | NoForeignKeyChecks = 0x02, 21 | 22 | /// 23 | /// Indicates that unique key checks are disabled. 24 | /// 25 | NoUniqueKeyChecks = 0x04, 26 | 27 | /// 28 | /// Indicates that row has a columns bitmap. 29 | /// 30 | RowHasAColumns = 0x08 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/RowSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SciSharp.MySQL.Replication.Events 5 | { 6 | /// 7 | /// Represents a set of rows affected by a row-based replication event. 8 | /// Contains both the column names and the row data. 9 | /// 10 | public sealed class RowSet 11 | { 12 | /// 13 | /// Gets or sets the list of column names. 14 | /// 15 | /// 16 | /// May be null if column names are not available in the binlog. 17 | /// 18 | public IReadOnlyList ColumnNames { get; set; } 19 | 20 | /// 21 | /// Gets or sets the list of rows. 22 | /// Each row is represented as an array of cell values. 23 | /// 24 | /// 25 | /// For UPDATE_ROWS_EVENT, the cell values are instances of CellValue containing both before and after values. 26 | /// For WRITE_ROWS_EVENT and DELETE_ROWS_EVENT, the cell values are the direct column values. 27 | /// 28 | public IReadOnlyList Rows { get; set; } 29 | 30 | /// 31 | /// Converts the rows into a readable format as a list of dictionaries. 32 | /// Each dictionary represents a row with column names as keys and cell values as values. 33 | /// 34 | /// A list of dictionaries representing the rows. 35 | /// Thrown when column names are not available. 36 | public IReadOnlyList> ToReadableRows() 37 | { 38 | var columnNames = ColumnNames; 39 | 40 | if (columnNames == null || columnNames.Count == 0) 41 | throw new Exception("No column name is available."); 42 | 43 | var list = new List>(); 44 | 45 | foreach (var row in Rows) 46 | { 47 | var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); 48 | 49 | for (var i = 0; i < row.Length; i++) 50 | { 51 | var columnName = columnNames[i]; 52 | dict.Add(columnName, row[i]); 53 | } 54 | 55 | list.Add(dict); 56 | } 57 | 58 | return list; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/RowsEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using SciSharp.MySQL.Replication.Types; 7 | 8 | namespace SciSharp.MySQL.Replication.Events 9 | { 10 | /// 11 | /// Base class for all row-based replication events (WriteRows, UpdateRows, DeleteRows). 12 | /// Handles the common functionality for parsing row data from the binary log. 13 | /// 14 | public abstract class RowsEvent : LogEvent 15 | { 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | public RowsEvent() 20 | { 21 | HasCRC = true; 22 | } 23 | 24 | /// 25 | /// Gets or sets the ID of the table this event applies to. 26 | /// 27 | public long TableID { get; protected set; } 28 | 29 | /// 30 | /// Gets or sets the flags that affect how the row event is interpreted. 31 | /// 32 | public RowsEventFlags RowsEventFlags { get; protected set; } 33 | 34 | /// 35 | /// Gets or sets the bitmap indicating which columns are included in the event. 36 | /// 37 | public BitArray IncludedColumns { get; protected set; } 38 | 39 | /// 40 | /// Gets or sets the set of rows affected by this event. 41 | /// 42 | public RowSet RowSet { get; protected set; } 43 | 44 | /// 45 | /// Gets the table mapping information for this event. 46 | /// 47 | protected TableMapEvent TableMap { get; private set; } 48 | 49 | /// 50 | /// Decodes the body of the row event from the binary representation. 51 | /// 52 | /// The sequence reader containing the binary data. 53 | /// The context for decoding, typically a ReplicationState. 54 | protected internal override void DecodeBody(ref SequenceReader reader, object context) 55 | { 56 | TableID = reader.ReadLong(6); 57 | 58 | reader.TryReadLittleEndian(out short flags); 59 | RowsEventFlags = (RowsEventFlags)flags; 60 | 61 | reader.TryReadLittleEndian(out short extraDataLen); 62 | reader.Advance(extraDataLen - 2); 63 | 64 | ReadIncludedColumns(ref reader); 65 | 66 | TableMapEvent tableMap = null; 67 | 68 | if (context is ReplicationState repState) 69 | { 70 | if (repState.TableMap.TryGetValue(TableID, out tableMap)) 71 | { 72 | TableMap = tableMap; 73 | } 74 | } 75 | 76 | if (tableMap == null) 77 | throw new Exception($"The table's metadata was not found: {TableID}."); 78 | 79 | var columnCount = GetIncludedColumnCount(IncludedColumns); 80 | 81 | RebuildReaderAsCRC(ref reader); 82 | ReadData(ref reader, IncludedColumns, tableMap, columnCount); 83 | } 84 | 85 | /// 86 | /// Returns a string representation of the RowsEvent. 87 | /// 88 | /// A string containing the event type, table ID, and row information. 89 | public override string ToString() 90 | { 91 | var sb = new StringBuilder(); 92 | sb.Append($"{EventType.ToString()}\r\nTableID: {TableID}"); 93 | 94 | var rowSet = RowSet; 95 | var columnNames = rowSet.ColumnNames; 96 | 97 | for (var i = 0; i < rowSet.Rows.Count; i++) 98 | { 99 | var row = rowSet.Rows[i]; 100 | 101 | for (int j = 0; j < row.Length; j++) 102 | { 103 | var name = columnNames?[j]; 104 | var value = row[j++]; 105 | 106 | if (string.IsNullOrEmpty(name)) 107 | sb.Append($"\r\n{value}"); 108 | else 109 | sb.Append($"\r\n{name}: {value}"); 110 | } 111 | } 112 | 113 | return sb.ToString(); 114 | } 115 | 116 | /// 117 | /// Reads the included columns bitmap from the binary representation. 118 | /// This method may be overridden by derived classes for specific event types. 119 | /// 120 | /// The sequence reader containing the binary data. 121 | protected virtual void ReadIncludedColumns(ref SequenceReader reader) 122 | { 123 | IncludedColumns = reader.ReadBitArray((int)reader.ReadLengthEncodedInteger()); 124 | } 125 | 126 | /// 127 | /// Reads the row data from the binary representation. 128 | /// This method should be implemented by derived classes for specific event types. 129 | /// 130 | /// The sequence reader containing the binary data. 131 | /// The bitmap of columns present in the row data. 132 | /// The table mapping information. 133 | /// The number of columns. 134 | protected virtual void ReadData(ref SequenceReader reader, BitArray includedColumns, TableMapEvent tableMap, int columnCount) 135 | { 136 | RowSet = ReadRows(ref reader, tableMap, IncludedColumns, columnCount); 137 | } 138 | 139 | /// 140 | /// Gets the column names for the included columns. 141 | /// 142 | /// The table mapping information. 143 | /// The bitmap of included columns. 144 | /// The number of columns. 145 | /// A list of column names. 146 | protected IReadOnlyList GetColumnNames(TableMapEvent table, BitArray includedColumns, int columnCount) 147 | { 148 | var columns = new List(columnCount); 149 | var columnNames = table.Metadata.ColumnNames; 150 | 151 | if (columnNames != null && columnNames.Count > 0) 152 | { 153 | for (var i = 0; i < includedColumns.Count; i++) 154 | { 155 | if (!includedColumns.Get(i)) 156 | continue; 157 | 158 | columns.Add(columnNames[i]); 159 | } 160 | } 161 | 162 | return columns; 163 | } 164 | 165 | /// 166 | /// Reads the rows data from the binary representation. 167 | /// 168 | /// The sequence reader containing the binary data. 169 | /// The table mapping information. 170 | /// The bitmap of included columns. 171 | /// The number of columns. 172 | /// A RowSet containing the rows data. 173 | protected RowSet ReadRows(ref SequenceReader reader, TableMapEvent table, BitArray includedColumns, int columnCount) 174 | { 175 | var rows = new List(); 176 | var columns = GetColumnNames(table, includedColumns, columnCount); 177 | 178 | while (reader.Remaining > 0) 179 | { 180 | rows.Add(ReadRow(ref reader, table, includedColumns, columnCount)); 181 | } 182 | 183 | return new RowSet 184 | { 185 | Rows = rows, 186 | ColumnNames = columns 187 | }; 188 | } 189 | 190 | /// 191 | /// Counts the number of included columns based on the bitmap. 192 | /// 193 | /// The bitmap of included columns. 194 | /// The count of included columns. 195 | protected int GetIncludedColumnCount(BitArray includedColumns) 196 | { 197 | var count = 0; 198 | 199 | for (var i = 0; i < includedColumns.Count; i++) 200 | { 201 | if (includedColumns.Get(i)) 202 | count++; 203 | } 204 | 205 | return count; 206 | } 207 | 208 | /// 209 | /// Reads a single row from the binary representation. 210 | /// 211 | /// The sequence reader containing the binary data. 212 | /// The table mapping information. 213 | /// The bitmap of included columns. 214 | /// The number of columns. 215 | /// An array of cell values for a single row. 216 | protected object[] ReadRow(ref SequenceReader reader, TableMapEvent table, BitArray includedColumns, int columnCount) 217 | { 218 | var cells = new object[columnCount]; 219 | var nullColumns = reader.ReadBitArray(columnCount, true); 220 | var columnTypes = table.ColumnTypes; 221 | 222 | for (int i = 0, numberOfSkippedColumns = 0; i < columnTypes.Length; i++) 223 | { 224 | if (!includedColumns.Get(i)) 225 | { 226 | numberOfSkippedColumns++; 227 | continue; 228 | } 229 | 230 | int index = i - numberOfSkippedColumns; 231 | 232 | if (nullColumns.Get(index)) 233 | continue; 234 | 235 | var columnMetadata = table.Metadata.Columns[i]; 236 | 237 | cells[index] = ReadCell(ref reader, (ColumnType)(columnMetadata.UnderlyingType ?? columnMetadata.Type), columnMetadata); 238 | } 239 | 240 | return cells; 241 | } 242 | 243 | /// 244 | /// Reads a cell value from the binary representation based on its column type. 245 | /// 246 | /// The sequence reader containing the binary data. 247 | /// The type of the column. 248 | /// The metadata for the column. 249 | /// The parsed cell value. 250 | private object ReadCell(ref SequenceReader reader, ColumnType columnType, ColumnMetadata columnMetadata) 251 | { 252 | var dataType = DataTypes[(int)columnType] as IMySQLDataType; 253 | 254 | if (dataType == null) 255 | throw new NotImplementedException(); 256 | 257 | return dataType.ReadValue(ref reader, columnMetadata); 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/SetTypeTableMetadataInitializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SciSharp.MySQL.Replication.Types; 3 | 4 | namespace SciSharp.MySQL.Replication.Events 5 | { 6 | class SetTypeTableMetadataInitializer : ITableMetadataInitializer 7 | { 8 | public void InitializeMetadata(TableMetadata metadata) 9 | { 10 | var setColumnIndex = 0; 11 | 12 | foreach (var column in metadata.Columns) 13 | { 14 | if (!IsSetColumn(column)) 15 | continue; 16 | 17 | column.SetValues = metadata.SetStrValues[setColumnIndex]; 18 | setColumnIndex++; 19 | } 20 | } 21 | 22 | private bool IsSetColumn(ColumnMetadata columnMetadata) 23 | { 24 | if (columnMetadata.Type == ColumnType.SET) 25 | return true; 26 | 27 | if (columnMetadata.Type != ColumnType.STRING) 28 | return false; 29 | 30 | var meta0 = columnMetadata.MetadataValue[0]; 31 | 32 | if (meta0 != (byte)ColumnType.SET) 33 | return false; 34 | 35 | columnMetadata.UnderlyingType = ColumnType.SET; 36 | return true; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/UpdateRowsEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace SciSharp.MySQL.Replication.Events 8 | { 9 | /// 10 | /// Represents a MySQL binary log event that contains rows updated in a table. 11 | /// This event is generated for an update operation on rows in a MySQL table and 12 | /// contains both the before and after values of the affected rows. 13 | /// 14 | public sealed class UpdateRowsEvent : RowsEvent 15 | { 16 | /// 17 | /// Gets the bitmap indicating which columns are included in the before-update image. 18 | /// 19 | public BitArray IncludedColumnsBeforeUpdate { get; private set; } 20 | 21 | /// 22 | /// Reads the included columns bitmaps from the binary representation. 23 | /// For update events, this includes both the before and after update column bitmaps. 24 | /// 25 | /// The sequence reader containing the binary data. 26 | protected override void ReadIncludedColumns(ref SequenceReader reader) 27 | { 28 | var columnCount = (int)reader.ReadLengthEncodedInteger(); 29 | IncludedColumnsBeforeUpdate = reader.ReadBitArray(columnCount); 30 | IncludedColumns = reader.ReadBitArray(columnCount); 31 | } 32 | 33 | /// 34 | /// Reads the row data from the binary representation, including both 35 | /// before and after values for updated rows. 36 | /// 37 | /// The sequence reader containing the binary data. 38 | /// The bitmap of columns present in the row data. 39 | /// The table mapping information. 40 | /// The number of columns. 41 | protected override void ReadData(ref SequenceReader reader, BitArray includedColumns, TableMapEvent tableMap, int columnCount) 42 | { 43 | RowSet = ReadUpdatedRows(ref reader, tableMap, IncludedColumnsBeforeUpdate, includedColumns, columnCount); 44 | } 45 | 46 | /// 47 | /// Reads the updated rows data from the binary representation. 48 | /// 49 | /// The sequence reader containing the binary data. 50 | /// The table mapping information. 51 | /// The bitmap of columns included in the before-update image. 52 | /// The bitmap of columns included in the after-update image. 53 | /// The number of columns. 54 | /// A RowSet containing the updated rows data with both before and after values. 55 | private RowSet ReadUpdatedRows(ref SequenceReader reader, TableMapEvent tableMap, BitArray includedColumnsBeforeUpdate, BitArray includedColumns, int columnCount) 56 | { 57 | var columnCountBeforeUpdate = GetIncludedColumnCount(IncludedColumnsBeforeUpdate); 58 | 59 | var rows = new List(); 60 | var columns = GetColumnNames(tableMap, includedColumnsBeforeUpdate, columnCount); 61 | 62 | while (reader.Remaining > 0) 63 | { 64 | var oldCellValues = ReadRow(ref reader, tableMap, includedColumnsBeforeUpdate, columnCountBeforeUpdate); 65 | var newCellValues = ReadRow(ref reader, tableMap, includedColumnsBeforeUpdate, columnCount); 66 | 67 | var cellCount = Math.Min(oldCellValues.Length, newCellValues.Length); 68 | var cells = new object[cellCount]; 69 | 70 | for (var i = 0; i < cellCount; i++) 71 | { 72 | cells[i] = new CellValue 73 | { 74 | OldValue = oldCellValues[i], 75 | NewValue = newCellValues[i] 76 | }; 77 | } 78 | 79 | rows.Add(cells); 80 | } 81 | 82 | return new RowSet 83 | { 84 | ColumnNames = columns, 85 | Rows = rows 86 | }; 87 | } 88 | 89 | /// 90 | /// Returns a string representation of the UpdateRowsEvent. 91 | /// 92 | /// A string containing the event type, table ID, and before/after row values. 93 | public override string ToString() 94 | { 95 | var sb = new StringBuilder(); 96 | sb.Append($"{EventType.ToString()}\r\nTableID: {TableID}"); 97 | 98 | var columns = IncludedColumns; 99 | var rows = RowSet.Rows; 100 | var columnNames = RowSet.ColumnNames; 101 | 102 | for (var i = 0; i < rows.Count; i++) 103 | { 104 | var row = rows[i]; 105 | 106 | for (int j = 0; j < row.Length; j++) 107 | { 108 | var name = columnNames?[i]; 109 | var value = row[j++] as CellValue; 110 | 111 | if (string.IsNullOrEmpty(name)) 112 | sb.Append($"\r\n{value.OldValue}=>{value.NewValue}"); 113 | else 114 | sb.Append($"\r\n{name}: {value.OldValue}=>{value.NewValue}"); 115 | } 116 | } 117 | 118 | return sb.ToString(); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/WriteRowsEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SciSharp.MySQL.Replication.Events 4 | { 5 | /// 6 | /// Represents a MySQL binary log event that contains rows inserted into a table. 7 | /// This event is generated for an insert operation on a MySQL table. 8 | /// 9 | public sealed class WriteRowsEvent : RowsEvent 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | public WriteRowsEvent() 15 | : base() 16 | { 17 | 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Events/XIDEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | 4 | namespace SciSharp.MySQL.Replication.Events 5 | { 6 | /// 7 | /// Represents a MySQL XID_EVENT that marks the end of a transaction that modifies data. 8 | /// This event contains the transaction ID used for the binary log. 9 | /// 10 | public sealed class XIDEvent : LogEvent 11 | { 12 | /// 13 | /// Gets or sets the ID of the transaction. 14 | /// 15 | public long TransactionID { get; set; } 16 | 17 | /// 18 | /// Decodes the body of the XID event from the binary representation. 19 | /// 20 | /// The sequence reader containing the binary data. 21 | /// The context for decoding. 22 | protected internal override void DecodeBody(ref SequenceReader reader, object context) 23 | { 24 | reader.TryReadLittleEndian(out long tarnsID); 25 | TransactionID = tarnsID; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/IReplicationClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using SuperSocket.Client; 4 | using SciSharp.MySQL.Replication.Events; 5 | using System.Collections.Generic; 6 | 7 | namespace SciSharp.MySQL.Replication 8 | { 9 | /// 10 | /// Interface for MySQL replication client that handles database event streaming. 11 | /// 12 | public interface IReplicationClient 13 | { 14 | /// 15 | /// Connects to a MySQL server with the specified credentials. 16 | /// 17 | /// The server address to connect to. 18 | /// The username for authentication. 19 | /// The password for authentication. 20 | /// The server ID to use for the replication client. 21 | /// A task that represents the asynchronous login operation and contains the login result. 22 | Task ConnectAsync(string server, string username, string password, int serverId); 23 | 24 | /// 25 | /// Connects to a MySQL server with the specified credentials and starts replication from a specific binlog position. 26 | /// 27 | /// The server address to connect to. 28 | /// The username for authentication. 29 | /// The password for authentication. 30 | /// The server ID to use for the replication client. 31 | /// The binary log position to start replicating from. 32 | /// A task that represents the asynchronous login operation and contains the login result. 33 | Task ConnectAsync(string server, string username, string password, int serverId, BinlogPosition binlogPosition); 34 | 35 | /// 36 | /// Gets the current binary log position. 37 | /// 38 | BinlogPosition CurrentPosition { get; } 39 | 40 | /// 41 | /// Event triggered when the binary log position changes. 42 | /// 43 | event EventHandler PositionChanged; 44 | 45 | /// 46 | /// Receives the next log event from the server. 47 | /// 48 | /// A task representing the asynchronous receive operation and containing the log event. 49 | ValueTask ReceiveAsync(); 50 | 51 | /// 52 | /// Asynchronously streams log events from the server. 53 | /// This method will yield log events as they are received. 54 | /// 55 | IAsyncEnumerable GetEventLogStream(); 56 | 57 | /// 58 | /// Closes the connection to the server. 59 | /// 60 | /// A task representing the asynchronous close operation. 61 | ValueTask CloseAsync(); 62 | 63 | /// 64 | /// Starts the continuous receiving of log events from the server. 65 | /// 66 | void StartReceive(); 67 | 68 | /// 69 | /// Event triggered when a log event package is received. 70 | /// 71 | event PackageHandler PackageHandler; 72 | 73 | /// 74 | /// Event triggered when the connection is closed. 75 | /// 76 | event EventHandler Closed; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/LogEventPackageDecoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Generic; 4 | using SciSharp.MySQL.Replication.Events; 5 | using SuperSocket.ProtoBase; 6 | 7 | namespace SciSharp.MySQL.Replication 8 | { 9 | /// 10 | /// Decoder for MySQL binary log events. 11 | /// See: https://dev.mysql.com/doc/internals/en/binlog-event.html 12 | /// and: https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_replication_binlog_event.html 13 | /// 14 | class LogEventPackageDecoder : IPackageDecoder 15 | { 16 | /// 17 | /// Dictionary of log event factories indexed by event type. 18 | /// 19 | private static Dictionary _logEventFactories = new Dictionary(); 20 | 21 | /// 22 | /// Default factory for events that are not implemented. 23 | /// 24 | private static ILogEventFactory _notImplementedEventFactory = new DefaultEventFactory(); 25 | 26 | /// 27 | /// Registers a log event type with a default factory. 28 | /// 29 | /// Type of log event to register. 30 | /// The event type to register. 31 | internal static void RegisterLogEventType(LogEventType eventType) 32 | where TLogEvent : LogEvent, new() 33 | { 34 | RegisterLogEventType(eventType, new DefaultEventFactory()); 35 | } 36 | 37 | /// 38 | /// Registers a log event type with a custom factory. 39 | /// 40 | /// The event type to register. 41 | /// The factory to use for creating events of this type. 42 | internal static void RegisterLogEventType(LogEventType eventType, ILogEventFactory factory) 43 | { 44 | _logEventFactories.Add(eventType, factory); 45 | } 46 | 47 | /// 48 | /// Registers multiple event types that have empty payloads. 49 | /// 50 | /// The event types to register as empty payload events. 51 | internal static void RegisterEmptyPayloadEventTypes(params LogEventType[] eventTypes) 52 | { 53 | foreach (var eventType in eventTypes) 54 | { 55 | _logEventFactories.Add(eventType, new DefaultEventFactory()); 56 | } 57 | } 58 | 59 | /// 60 | /// Creates a log event instance of the specified type. 61 | /// 62 | /// The type of log event to create. 63 | /// The context object for the event. 64 | /// A new log event instance. 65 | protected virtual LogEvent CreateLogEvent(LogEventType eventType, object context) 66 | { 67 | if (!_logEventFactories.TryGetValue(eventType, out var factory)) 68 | factory = _notImplementedEventFactory; 69 | 70 | var log = factory.Create(context); 71 | log.EventType = eventType; 72 | return log; 73 | } 74 | 75 | /// 76 | /// Decodes a binary buffer into a LogEvent object. 77 | /// 78 | /// The buffer containing binary data. 79 | /// The context object for the decoding. 80 | /// A decoded LogEvent object. 81 | public LogEvent Decode(ref ReadOnlySequence buffer, object context) 82 | { 83 | var reader = new SequenceReader(buffer); 84 | 85 | reader.Advance(4); // 3 + 1 86 | 87 | // ok byte 88 | reader.TryRead(out byte ok); 89 | 90 | if (ok == 0xFF) 91 | { 92 | var errorLogEvent = new ErrorEvent(); 93 | errorLogEvent.DecodeBody(ref reader, context); 94 | return errorLogEvent; 95 | } 96 | 97 | reader.TryReadLittleEndian(out int seconds); 98 | var timestamp = LogEvent.GetTimestampFromUnixEpoch(seconds); 99 | 100 | reader.TryRead(out byte eventTypeValue); 101 | var eventType = (LogEventType)eventTypeValue; 102 | 103 | var log = CreateLogEvent(eventType, context); 104 | 105 | log.Timestamp = timestamp; 106 | log.EventType = eventType; 107 | 108 | reader.TryReadLittleEndian(out int serverID); 109 | log.ServerID = serverID; 110 | 111 | reader.TryReadLittleEndian(out int eventSize); 112 | log.EventSize = eventSize; 113 | 114 | reader.TryReadLittleEndian(out int position); 115 | log.Position = position; 116 | 117 | reader.TryReadLittleEndian(out short flags); 118 | log.Flags = (LogEventFlag)flags; 119 | 120 | log.DecodeBody(ref reader, context); 121 | 122 | return log; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/LogEventPipelineFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using SciSharp.MySQL.Replication.Events; 4 | using SuperSocket.ProtoBase; 5 | 6 | namespace SciSharp.MySQL.Replication 7 | { 8 | /// 9 | /// Pipeline filter that processes MySQL binary log event data from a network stream. 10 | /// This class is responsible for extracting complete log events from the stream. 11 | /// https://dev.mysql.com/doc/internals/en/binlog-event.html 12 | /// https://dev.mysql.com/doc/internals/en/binlog-event-header.html 13 | /// 14 | public class LogEventPipelineFilter : FixedHeaderPipelineFilter 15 | { 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | public LogEventPipelineFilter() 20 | : base(3) 21 | { 22 | Decoder = new LogEventPackageDecoder(); 23 | Context = new ReplicationState(); 24 | } 25 | 26 | /// 27 | /// Gets the body length of a MySQL packet from its header. 28 | /// 29 | /// The buffer containing the packet header. 30 | /// The length of the packet body. 31 | protected override int GetBodyLengthFromHeader(ref ReadOnlySequence buffer) 32 | { 33 | var reader = new SequenceReader(buffer); 34 | 35 | reader.TryRead(out byte byte0); 36 | reader.TryRead(out byte byte1); 37 | reader.TryRead(out byte byte2); 38 | 39 | return byte2 * 256 * 256 + byte1 * 256 + byte0 + 1; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/LoginResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SciSharp.MySQL.Replication 4 | { 5 | /// 6 | /// Represents the result of a login attempt to a MySQL server. 7 | /// 8 | public class LoginResult 9 | { 10 | /// 11 | /// Gets or sets a value indicating whether the login was successful. 12 | /// 13 | public bool Result { get; set; } 14 | 15 | /// 16 | /// Gets or sets a message describing the login result. 17 | /// 18 | public string Message { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/ReplicationState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using SciSharp.MySQL.Replication.Events; 4 | 5 | namespace SciSharp.MySQL.Replication 6 | { 7 | /// 8 | /// Represents the state information maintained during a MySQL replication session. 9 | /// 10 | class ReplicationState 11 | { 12 | /// 13 | /// Gets or sets the dictionary mapping table IDs to their corresponding TableMapEvent objects. 14 | /// 15 | public Dictionary TableMap { get; set; } = new Dictionary(); 16 | 17 | /// 18 | /// Gets or sets the dictionary mapping table IDs to their corresponding TableSchema objects. 19 | /// 20 | public Dictionary TableSchemaMap { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/SciSharp.MySQL.Replication.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0;net7.0;net8.0;net9.0 4 | 12.0 5 | true 6 | 7 | 8 | https://github.com/SciSharp/dotnet-mysql-replication 9 | Apache-2.0 10 | true 11 | snupkg 12 | true 13 | Kerry Jiang and other contributors 14 | SciSharp STACK 15 | MySQL;replication;C#;client 16 | 17 | dotnet-mysql-replication is a C# Implementation of MySQL replication protocol client. This allows you to receive events like insert, update, delete with their data and raw SQL queries from MySQL. 18 | 19 | 20 | 21 | v$(PackageVersion).md 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/SequenceReaderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using SuperSocket.ProtoBase; 7 | 8 | namespace SciSharp.MySQL.Replication 9 | { 10 | /// 11 | /// Extension methods for SequenceReader to help parse MySQL binary log formats. 12 | /// 13 | public static class SequenceReaderExtensions 14 | { 15 | /// 16 | /// Reads a BitArray from the binary stream. 17 | /// 18 | /// The sequence reader. 19 | /// The number of bits to read. 20 | /// If true, interprets the bits in big-endian order. 21 | /// The BitArray with the read bits. 22 | internal static BitArray ReadBitArray(ref this SequenceReader reader, int length, bool bigEndian = false) 23 | { 24 | var dataLen = (length + 7) / 8; 25 | var array = new BitArray(length, false); 26 | 27 | if (!bigEndian) 28 | { 29 | for (int i = 0; i < dataLen; i++) 30 | { 31 | reader.TryRead(out byte b); 32 | SetBitArray(array, b, i, length); 33 | } 34 | } 35 | else 36 | { 37 | for (int i = dataLen - 1; i >= 0; i--) 38 | { 39 | reader.TryRead(out byte b); 40 | SetBitArray(array, b, i, length); 41 | } 42 | } 43 | 44 | return array; 45 | } 46 | 47 | private static void SetBitArray(BitArray array, byte b, int i, int length) 48 | { 49 | for (var j = i * 8; j < Math.Min((i + 1) * 8, length); j++) 50 | { 51 | if ((b & (0x01 << (j % 8))) != 0x00) 52 | array.Set(j, true); 53 | } 54 | } 55 | 56 | /// 57 | /// Reads a string from the binary stream using the specified encoding. 58 | /// 59 | /// The sequence reader. 60 | /// The encoding to use. 61 | /// The decoded string. 62 | internal static string ReadString(ref this SequenceReader reader, Encoding encoding) 63 | { 64 | return ReadString(ref reader, encoding, out long consumed); 65 | } 66 | 67 | /// 68 | /// Reads a string from the binary stream using the specified encoding and outputs the number of bytes consumed. 69 | /// 70 | /// The sequence reader. 71 | /// The encoding to use. 72 | /// The number of bytes consumed. 73 | /// The decoded string. 74 | internal static string ReadString(ref this SequenceReader reader, Encoding encoding, out long consumed) 75 | { 76 | if (encoding == null) 77 | encoding = Encoding.UTF8; 78 | 79 | if (reader.TryReadTo(out ReadOnlySequence seq, 0x00, false)) 80 | { 81 | consumed = seq.Length + 1; 82 | var result = seq.GetString(encoding); 83 | reader.Advance(1); 84 | return result; 85 | } 86 | else 87 | { 88 | consumed = reader.Remaining; 89 | seq = reader.Sequence; 90 | seq = seq.Slice(reader.Consumed); 91 | var result = seq.GetString(Encoding.UTF8); 92 | reader.Advance(consumed); 93 | return result; 94 | } 95 | } 96 | 97 | /// 98 | /// Reads a string from the binary stream with a specified length. 99 | /// 100 | /// The sequence reader. 101 | /// The length of the string in bytes. 102 | /// The decoded string. 103 | internal static string ReadString(ref this SequenceReader reader, long length = 0) 104 | { 105 | return ReadString(ref reader, Encoding.UTF8, length); 106 | } 107 | 108 | /// 109 | /// Reads a string from the binary stream using the specified encoding and length. 110 | /// 111 | /// The sequence reader. 112 | /// The encoding to use. 113 | /// The length of the string in bytes. 114 | /// The decoded string. 115 | internal static string ReadString(ref this SequenceReader reader, Encoding encoding, long length = 0) 116 | { 117 | if (length == 0 || reader.Remaining <= length) 118 | return ReadString(ref reader, encoding); 119 | 120 | // reader.Remaining > length 121 | var seq = reader.Sequence.Slice(reader.Consumed, length); 122 | var consumed = 0L; 123 | 124 | try 125 | { 126 | var subReader = new SequenceReader(seq); 127 | return ReadString(ref subReader, encoding, out consumed); 128 | } 129 | finally 130 | { 131 | reader.Advance(length); 132 | } 133 | } 134 | 135 | /// 136 | /// Reads a fixed-length unsigned integer from the binary stream. 137 | /// 138 | /// The sequence reader. 139 | /// The number of bytes to read (1-4). 140 | /// The decoded integer value. 141 | internal static int ReadInteger(ref this SequenceReader reader, int length) 142 | { 143 | if (length > 4) 144 | throw new ArgumentException("Length cannot be more than 4.", nameof(length)); 145 | 146 | var unit = 1; 147 | var value = 0; 148 | 149 | for (var i = 0; i < length; i++) 150 | { 151 | reader.TryRead(out byte thisValue); 152 | value += thisValue * unit; 153 | unit *= 256; 154 | } 155 | 156 | return value; 157 | } 158 | 159 | /// 160 | /// Reads a big-endian integer from the binary stream. 161 | /// 162 | /// The sequence reader. 163 | /// The number of bytes to read (1-4). 164 | /// The decoded integer value. 165 | internal static int ReadBigEndianInteger(ref this SequenceReader reader, int length) 166 | { 167 | if (length > 4) 168 | throw new ArgumentException("Length cannot be more than 4.", nameof(length)); 169 | 170 | var unit = (int)Math.Pow(256, length - 1); 171 | var value = 0; 172 | 173 | for (var i = 0; i < length; i++) 174 | { 175 | reader.TryRead(out byte thisValue); 176 | value += thisValue * (int)Math.Pow(256, length - i - 1); 177 | } 178 | 179 | return value; 180 | } 181 | 182 | /// 183 | /// Reads a fixed-length long integer from the binary stream. 184 | /// 185 | /// The sequence reader. 186 | /// The number of bytes to read (1-8). 187 | /// The decoded long integer value. 188 | internal static long ReadLong(ref this SequenceReader reader, int length) 189 | { 190 | var unit = 1; 191 | var value = 0L; 192 | 193 | for (var i = 0; i < length; i++) 194 | { 195 | reader.TryRead(out byte thisValue); 196 | value += thisValue * unit; 197 | unit *= 256; 198 | } 199 | 200 | return value; 201 | } 202 | 203 | internal static ushort ReadLittleEndianShort(ref this SequenceReader reader) 204 | { 205 | reader.TryRead(out byte b0); 206 | reader.TryRead(out byte b1); 207 | 208 | return (ushort) (b1 << 8 | b0); 209 | } 210 | 211 | /// 212 | /// Reads a length-encoded string from the binary stream. 213 | /// 214 | /// The sequence reader. 215 | /// The decoded string. 216 | internal static string ReadLengthEncodedString(ref this SequenceReader reader) 217 | { 218 | return ReadLengthEncodedString(ref reader, Encoding.UTF8); 219 | } 220 | 221 | /// 222 | /// Reads a length-encoded string from the binary stream using the specified encoding. 223 | /// 224 | /// The sequence reader. 225 | /// The encoding to use. 226 | /// The decoded string. 227 | internal static string ReadLengthEncodedString(ref this SequenceReader reader, Encoding encoding) 228 | { 229 | var len = reader.ReadLengthEncodedInteger(); 230 | 231 | if (len < 0) 232 | return null; 233 | 234 | if (len == 0) 235 | return string.Empty; 236 | 237 | return ReadString(ref reader, encoding, len); 238 | } 239 | 240 | /// 241 | /// Reads a length-encoded integer from the binary stream. 242 | /// 243 | /// The sequence reader. 244 | /// The decoded integer value. 245 | internal static long ReadLengthEncodedInteger(ref this SequenceReader reader) 246 | { 247 | reader.TryRead(out byte b0); 248 | 249 | if (b0 == 0xFB) // 251 250 | return -1; 251 | 252 | if (b0 == 0xFC) // 252 253 | { 254 | reader.TryReadLittleEndian(out short shortValue); 255 | return (long)shortValue; 256 | } 257 | 258 | if (b0 == 0xFD) // 253 259 | { 260 | reader.TryRead(out byte b1); 261 | reader.TryRead(out byte b2); 262 | reader.TryRead(out byte b3); 263 | 264 | return (long)(b1 + b2 * 256 + b3 * 256 * 256); 265 | } 266 | 267 | if (b0 == 0xFE) // 254 268 | { 269 | reader.TryReadLittleEndian(out long longValue); 270 | return longValue; 271 | } 272 | 273 | return (long)b0; 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/TableMetadata.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Collections; 5 | using System.Text; 6 | using SciSharp.MySQL.Replication.Types; 7 | 8 | namespace SciSharp.MySQL.Replication 9 | { 10 | /// 11 | /// Contains metadata information about a MySQL table structure. 12 | /// 13 | public class TableMetadata 14 | { 15 | /// 16 | /// Gets or sets the signedness information for numeric columns. 17 | /// Each bit corresponds to a column, indicating whether it's signed (false) or unsigned (true). 18 | /// 19 | public BitArray Signedness { get; set; } 20 | 21 | /// 22 | /// Gets or sets the default charset information for the table. 23 | /// 24 | public DefaultCharset DefaultCharset { get; set; } 25 | 26 | /// 27 | /// Gets or sets the list of charset IDs for each column in the table. 28 | /// 29 | public List ColumnCharsets { get; set; } 30 | 31 | /// 32 | /// Gets or sets the list of column names in the table. 33 | /// 34 | public List ColumnNames { get; set; } 35 | 36 | /// 37 | /// Gets or sets the possible string values for SET columns. 38 | /// 39 | public List SetStrValues { get; set; } 40 | 41 | /// 42 | /// Gets or sets the possible string values for ENUM columns. 43 | /// 44 | public List EnumStrValues { get; set; } 45 | 46 | /// 47 | /// Gets or sets the types of GEOMETRY columns. 48 | /// 49 | public List GeometryTypes { get; set; } 50 | 51 | /// 52 | /// Gets or sets the indexes of columns that are part of the primary key without a prefix. 53 | /// 54 | public List SimplePrimaryKeys { get; set; } 55 | 56 | /// 57 | /// Gets or sets a dictionary mapping column indexes to their prefix length for primary key columns with a prefix. 58 | /// 59 | public Dictionary PrimaryKeysWithPrefix { get; set; } 60 | 61 | /// 62 | /// Gets or sets the default charset information for ENUM and SET columns. 63 | /// 64 | public DefaultCharset EnumAndSetDefaultCharset { get; set; } 65 | 66 | /// 67 | /// Gets or sets the list of charset IDs for ENUM and SET columns. 68 | /// 69 | public List EnumAndSetColumnCharsets { get; set; } 70 | 71 | /// 72 | /// Gets or sets the visibility information for columns. 73 | /// Each bit corresponds to a column, indicating whether it's visible (true) or invisible (false). 74 | /// 75 | public BitArray ColumnVisibility { get; set; } 76 | 77 | /// 78 | /// Gets the list of column metadata. 79 | /// 80 | public IReadOnlyList Columns { get; private set; } 81 | 82 | /// 83 | /// Builds the list of column metadata based on the provided column types. 84 | /// This method is typically called after the table metadata has been fully populated. 85 | /// 86 | /// The column types. 87 | /// The metadata values for each column. 88 | public void BuildColumnMetadataList(IReadOnlyList columnTypes, IReadOnlyList columnMetadataValues) 89 | { 90 | var columnMetadatas = new List(ColumnNames.Count); 91 | 92 | for (int i = 0; i < columnTypes.Count; i++) 93 | { 94 | var columnType = columnTypes[i]; 95 | 96 | var columnMetadata = new ColumnMetadata 97 | { 98 | Name = ColumnNames[i], 99 | Type = columnType, 100 | CharsetId = ColumnCharsets != null ? ColumnCharsets[i] : 0, 101 | MetadataValue = columnMetadataValues[i] 102 | }; 103 | 104 | columnMetadatas.Add(columnMetadata); 105 | } 106 | 107 | Columns = columnMetadatas; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/TableSchema.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace SciSharp.MySQL.Replication 6 | { 7 | /// 8 | /// Represents the schema of a MySQL database table. 9 | /// 10 | internal class TableSchema 11 | { 12 | /// 13 | /// Gets or sets the ID of the table. 14 | /// 15 | public long TableID { get; set; } 16 | 17 | /// 18 | /// Gets or sets the name of the table. 19 | /// 20 | public string TableName { get; set; } 21 | 22 | /// 23 | /// Gets or sets the database name containing the table. 24 | /// 25 | public string DatabaseName { get; set; } 26 | 27 | /// 28 | /// Gets or sets the collection of columns in the table. 29 | /// 30 | public IReadOnlyList Columns { get; set; } 31 | } 32 | 33 | /// 34 | /// Represents information about a database column. 35 | /// 36 | internal class ColumnSchema 37 | { 38 | /// 39 | /// Gets or sets the name of the column. 40 | /// 41 | public string Name { get; set; } 42 | 43 | /// 44 | /// Gets or sets the MySQL data type of the column. 45 | /// 46 | public string DataType { get; set; } 47 | 48 | /// 49 | /// Gets or sets column size. 50 | /// 51 | public ulong ColumnSize { get; set; } 52 | } 53 | } -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/BitType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | 6 | namespace SciSharp.MySQL.Replication.Types 7 | { 8 | /// 9 | /// Represents the MySQL BIT data type. 10 | /// 11 | /// 12 | /// Handles the reading and conversion of MySQL BIT values. 13 | /// 14 | class BitType : IMySQLDataType 15 | { 16 | /// 17 | /// Reads a BIT value from the binary log. 18 | /// 19 | /// The sequence reader containing the bytes to read. 20 | /// Metadata for the column. 21 | /// An object representing the MySQL BIT value. 22 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 23 | { 24 | byte meta = columnMetadata.MetadataValue[0]; 25 | int bitArrayLength = (meta >> 8) * 8 + meta; 26 | return reader.ReadBitArray(bitArrayLength, false); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/BlobType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | 6 | namespace SciSharp.MySQL.Replication.Types 7 | { 8 | /// 9 | /// Options specific to BLOB data type. 10 | /// 11 | class BlobOptions 12 | { 13 | /// 14 | /// Gets or sets the number of bytes used to store the BLOB length (1, 2, 3, or 4). 15 | /// 16 | public int LengthBytes { get; set; } 17 | } 18 | 19 | /// 20 | /// Represents the MySQL BLOB data type. 21 | /// 22 | /// 23 | /// Handles the reading and conversion of MySQL BLOB values. 24 | /// 25 | class BlobType : IMySQLDataType, IColumnMetadataLoader 26 | { 27 | /// 28 | /// Loads metadata for BLOB type. 29 | /// 30 | /// The column metadata object. 31 | public void LoadMetadataValue(ColumnMetadata columnMetadata) 32 | { 33 | // The metadata value for BLOB is the length of the size field in bytes (1, 2, 3, or 4) 34 | columnMetadata.Options = new BlobOptions 35 | { 36 | LengthBytes = columnMetadata.MetadataValue[0] 37 | }; 38 | } 39 | 40 | /// 41 | /// Reads a BLOB value from the binary log. 42 | /// 43 | /// The sequence reader containing the bytes to read. 44 | /// Metadata for the column. 45 | /// A byte array representing the MySQL BLOB value. 46 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 47 | { 48 | // Get metadata length (bytes used for size field: 1, 2, 3, or 4) from Options 49 | int lengthBytes = columnMetadata.Options is BlobOptions options ? options.LengthBytes : 0; 50 | 51 | // Read the length of the BLOB based on metadata 52 | int blobLength = reader.ReadInteger(lengthBytes); 53 | 54 | // Validate blob length to prevent out-of-memory issues 55 | if (blobLength < 0 || blobLength > 1_000_000_000) // 1GB limit as safety check 56 | { 57 | throw new InvalidOperationException($"Invalid BLOB length: {blobLength}"); 58 | } 59 | 60 | // Handle empty BLOB 61 | if (blobLength == 0) 62 | { 63 | return Array.Empty(); 64 | } 65 | 66 | // Read the BLOB data 67 | byte[] blobData = new byte[blobLength]; 68 | 69 | if (!reader.TryCopyTo(blobData.AsSpan())) 70 | { 71 | throw new InvalidOperationException($"Failed to read complete BLOB data (expected {blobLength} bytes)"); 72 | } 73 | 74 | reader.Advance(blobLength); 75 | return blobData; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/ColumnTypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SciSharp.MySQL.Replication.Types 4 | { 5 | /// 6 | /// Extension methods for the ColumnType enumeration. 7 | /// 8 | public static class ColumnTypeExtensions 9 | { 10 | /// 11 | /// Determines if the specified column type is a numeric type. 12 | /// 13 | /// The column type. 14 | internal static bool IsNumberColumn(this ColumnType columnType) 15 | { 16 | switch (columnType) 17 | { 18 | case ColumnType.TINY: 19 | case ColumnType.SHORT: 20 | case ColumnType.INT24: 21 | case ColumnType.LONG: 22 | case ColumnType.LONGLONG: 23 | case ColumnType.NEWDECIMAL: 24 | case ColumnType.FLOAT: 25 | case ColumnType.DOUBLE: 26 | return true; 27 | default: 28 | return false; 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/DateTimeType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | 6 | namespace SciSharp.MySQL.Replication.Types 7 | { 8 | /// 9 | /// Represents the MySQL DATETIME data type. 10 | /// 11 | /// 12 | /// Handles the reading and conversion of MySQL DATETIME values. 13 | /// 14 | class DateTimeType : IMySQLDataType 15 | { 16 | /// 17 | /// Reads a DATETIME value from the binary log. 18 | /// 19 | /// The sequence reader containing the bytes to read. 20 | /// Metadata for the column. 21 | /// A DateTime object representing the MySQL DATETIME value. 22 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 23 | { 24 | var value = reader.ReadLong(8); 25 | 26 | var unit = 100; 27 | 28 | var seconds = (int) (value % unit); 29 | value /= value; 30 | 31 | var minutes = (int) (value % unit); 32 | value /= value; 33 | 34 | var hours = (int) (value % unit); 35 | value /= value; 36 | 37 | var days = (int) (value % unit); 38 | value /= value; 39 | 40 | var month = (int) (value % unit); 41 | value /= value; 42 | 43 | var year = (int)value; 44 | 45 | return new DateTime(year, month, days, hours, minutes, seconds, 0); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/DateTimeV2Type.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | 6 | namespace SciSharp.MySQL.Replication.Types 7 | { 8 | /// 9 | /// Represents the MySQL DATETIME2 data type. 10 | /// 11 | /// 12 | /// Handles the reading and conversion of MySQL DATETIME2 values with fractional seconds. 13 | /// 14 | class DateTimeV2Type : TimeBaseType, IMySQLDataType 15 | { 16 | /// 17 | /// Reads a DATETIME2 value from the binary log. 18 | /// 19 | /// The sequence reader containing the bytes to read. 20 | /// Metadata for the column defining fractional second precision. 21 | /// A DateTime object representing the MySQL DATETIME2 value. 22 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 23 | { 24 | int meta = columnMetadata.MetadataValue[0]; 25 | /* 26 | (in big endian) 27 | 1 bit sign (1= non-negative, 0= negative) 28 | 17 bits year*13+month (year 0-9999, month 0-12) 29 | 5 bits day (0-31) 30 | 5 bits hour (0-23) 31 | 6 bits minute (0-59) 32 | 6 bits second (0-59) 33 | (5 bytes in total) 34 | + fractional-seconds storage (size depends on meta) 35 | */ 36 | 37 | reader.TryReadBigEndian(out int totalValue0); 38 | reader.TryRead(out byte totalValue1); 39 | 40 | var totalValue = (long)totalValue0 * 256 + totalValue1; 41 | 42 | if (totalValue == 0) 43 | return null; 44 | 45 | var seconds = (int)(totalValue & 0x3F); 46 | 47 | totalValue = totalValue >> 6; 48 | var minutes = (int)(totalValue & 0x3F); 49 | 50 | totalValue = totalValue >> 6; 51 | var hours = (int)(totalValue & 0x1F); 52 | 53 | totalValue = totalValue >> 5; 54 | var days = (int)(totalValue & 0x1F); 55 | 56 | totalValue = totalValue >> 5; 57 | var yearMonths = (int)(totalValue & 0x01FFFF); 58 | 59 | var year = yearMonths / 13; 60 | var month = yearMonths % 13; 61 | var fsp = ReadFractionalSeconds(ref reader, meta); 62 | 63 | return new DateTime(year, month, days, hours, minutes, seconds, fsp / 1000); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/DateType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | 6 | namespace SciSharp.MySQL.Replication.Types 7 | { 8 | /// 9 | /// Represents the MySQL DATE data type. 10 | /// 11 | /// 12 | /// Handles the reading and conversion of MySQL DATE values. 13 | /// 14 | class DateType : IMySQLDataType 15 | { 16 | /// 17 | /// Reads a DATE value from the binary log. 18 | /// 19 | /// The sequence reader containing the bytes to read. 20 | /// Metadata for the column. 21 | /// A DateTime object representing the MySQL DATE value. 22 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 23 | { 24 | // 11111000 00000000 00000000 25 | // 00000100 00000000 00000000 26 | var value = reader.ReadInteger(3); 27 | var day = value % 32; 28 | value >>= 5; 29 | int month = value % 16; 30 | int year = value >> 4; 31 | 32 | return new DateTime(year, month, day, 0, 0, 0); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/DoubleType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | 6 | namespace SciSharp.MySQL.Replication.Types 7 | { 8 | /// 9 | /// Represents the MySQL DOUBLE data type. 10 | /// 11 | /// 12 | /// Handles the reading and conversion of MySQL DOUBLE precision floating-point values. 13 | /// 14 | class DoubleType : IMySQLDataType 15 | { 16 | /// 17 | /// Reads a DOUBLE value from the binary log. 18 | /// 19 | /// The sequence reader containing the bytes to read. 20 | /// Metadata for the column. 21 | /// A double value representing the MySQL DOUBLE value. 22 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 23 | { 24 | // MySQL stores DOUBLE values in IEEE 754 double-precision format (8 bytes) 25 | // Read the 8 bytes in big-endian order 26 | Span buffer = stackalloc byte[8]; 27 | reader.TryCopyTo(buffer); 28 | reader.Advance(8); 29 | 30 | return BitConverter.ToDouble(buffer); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/EnumType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | 6 | namespace SciSharp.MySQL.Replication.Types 7 | { 8 | /// 9 | /// Represents the MySQL ENUM data type. 10 | /// 11 | /// 12 | /// Handles the reading and conversion of MySQL ENUM values. 13 | /// 14 | class EnumType : IMySQLDataType, IColumnMetadataLoader 15 | { 16 | public void LoadMetadataValue(ColumnMetadata columnMetadata) 17 | { 18 | columnMetadata.MaxLength = columnMetadata.MetadataValue[1]; 19 | } 20 | 21 | /// 22 | /// Reads an ENUM value from the binary log. 23 | /// 24 | /// The sequence reader containing the bytes to read. 25 | /// Metadata for the column. 26 | /// An integer representing the index of the ENUM value. 27 | public virtual object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 28 | { 29 | var enumIndex = columnMetadata.MaxLength == 1 30 | ? reader.TryRead(out byte l) ? (int)l : 0 31 | : reader.TryReadLittleEndian(out short sl) ? (int)sl : 0; 32 | 33 | // Out of range check 34 | if (enumIndex >= columnMetadata.EnumValues.Count) 35 | { 36 | return null; 37 | } 38 | 39 | if (enumIndex == 0) 40 | { 41 | return string.Empty; 42 | } 43 | 44 | return columnMetadata.EnumValues[enumIndex - 1]; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/FloatType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | 6 | namespace SciSharp.MySQL.Replication.Types 7 | { 8 | /// 9 | /// Represents the MySQL FLOAT data type. 10 | /// 11 | /// 12 | /// Handles the reading and conversion of MySQL FLOAT values. 13 | /// 14 | class FloatType : IMySQLDataType 15 | { 16 | /// 17 | /// Reads a FLOAT value from the binary log. 18 | /// 19 | /// The sequence reader containing the bytes to read. 20 | /// Metadata for the column. 21 | /// A float value representing the MySQL FLOAT value. 22 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 23 | { 24 | byte[] bytes = BitConverter.GetBytes(reader.ReadInteger(4)); 25 | return BitConverter.ToSingle(bytes, 0); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/GeometryType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | 6 | namespace SciSharp.MySQL.Replication.Types 7 | { 8 | /// 9 | /// Represents the MySQL GEOMETRY data type. 10 | /// 11 | /// 12 | /// Handles the reading and conversion of MySQL GEOMETRY values. 13 | /// 14 | class GeometryType : IMySQLDataType 15 | { 16 | /// 17 | /// Reads a GEOMETRY value from the binary log. 18 | /// 19 | /// The sequence reader containing the bytes to read. 20 | /// Metadata for the column. 21 | /// A byte array representing the MySQL GEOMETRY value. 22 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 23 | { 24 | int meta = columnMetadata.MetadataValue[0]; 25 | int blobLength = reader.ReadInteger(meta); 26 | 27 | try 28 | { 29 | return reader.Sequence.Slice(reader.Consumed, blobLength).ToArray(); 30 | } 31 | finally 32 | { 33 | reader.Advance(blobLength); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/IColumnMetadataLoader.cs: -------------------------------------------------------------------------------- 1 | namespace SciSharp.MySQL.Replication.Types 2 | { 3 | /// 4 | /// Interface for loading metadata values to column metadata. 5 | /// 6 | public interface IColumnMetadataLoader 7 | { 8 | /// 9 | /// Loads the metadata value for a given column metadata. 10 | /// 11 | /// The column metadata. 12 | void LoadMetadataValue(ColumnMetadata columnMetadata); 13 | } 14 | } -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/IMySQLDataType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | 6 | namespace SciSharp.MySQL.Replication.Types 7 | { 8 | /// 9 | /// Interface that defines MySQL data type read operations. 10 | /// 11 | internal interface IMySQLDataType 12 | { 13 | /// 14 | /// Reads a value from the sequence reader based on the metadata. 15 | /// 16 | /// The sequence reader containing binary data. 17 | /// Metadata for the column being read. 18 | /// The deserialized object value. 19 | object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/Int24Type.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Buffers.Binary; 4 | 5 | namespace SciSharp.MySQL.Replication.Types 6 | { 7 | /// 8 | /// Represents the MySQL MEDIUMINT data type (3-byte integer). 9 | /// 10 | /// 11 | /// Handles the reading and conversion of MySQL MEDIUMINT values. 12 | /// 13 | class Int24Type : IMySQLDataType 14 | { 15 | /// 16 | /// Reads a MEDIUMINT value from the binary log. 17 | /// 18 | /// The sequence reader containing the bytes to read. 19 | /// Metadata for the column. 20 | /// An integer representing the MySQL MEDIUMINT value. 21 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 22 | { 23 | Span buffer = stackalloc byte[4]; 24 | 25 | reader.TryCopyTo(buffer.Slice(0, 3)); 26 | reader.Advance(3); 27 | 28 | buffer[3] = 0; 29 | 30 | var signalByte = buffer[2]; 31 | 32 | if (!columnMetadata.IsUnsigned && (signalByte & 0x80) == 0x80) // Negative value 33 | { 34 | buffer[3] = 0xFF; // Set the sign bit for negative values 35 | } 36 | else 37 | { 38 | buffer[3] = 0x00; // Set the sign bit for positive values 39 | } 40 | 41 | return columnMetadata.IsUnsigned 42 | ? BinaryPrimitives.ReadUInt32LittleEndian(buffer) 43 | : BinaryPrimitives.ReadInt32LittleEndian(buffer); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/JsonType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | 6 | namespace SciSharp.MySQL.Replication.Types 7 | { 8 | /// 9 | /// Represents the MySQL JSON data type. 10 | /// 11 | /// 12 | /// Handles the reading and conversion of MySQL JSON values. 13 | /// 14 | class JsonType : IMySQLDataType 15 | { 16 | /// 17 | /// Reads a JSON value from the binary log. 18 | /// 19 | /// The sequence reader containing the bytes to read. 20 | /// Metadata for the column. 21 | /// A byte array representing the MySQL JSON value. 22 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 23 | { 24 | int meta = columnMetadata.MetadataValue[0]; 25 | int blobLength = reader.ReadInteger(meta); 26 | 27 | try 28 | { 29 | return reader.Sequence.Slice(reader.Consumed, blobLength).ToArray(); 30 | } 31 | finally 32 | { 33 | reader.Advance(blobLength); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/LongLongType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | using System.Buffers.Binary; 6 | 7 | namespace SciSharp.MySQL.Replication.Types 8 | { 9 | /// 10 | /// Represents the MySQL BIGINT data type. 11 | /// 12 | /// 13 | /// Handles the reading and conversion of MySQL BIGINT values (8-byte integers). 14 | /// 15 | class LongLongType : IMySQLDataType 16 | { 17 | /// 18 | /// Reads a BIGINT value from the binary log. 19 | /// 20 | /// The sequence reader containing the bytes to read. 21 | /// Metadata for the column. 22 | /// A long value representing the MySQL BIGINT value. 23 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 24 | { 25 | Span buffer = stackalloc byte[sizeof(long)]; 26 | 27 | reader.TryCopyTo(buffer); 28 | reader.Advance(sizeof(long)); 29 | 30 | return columnMetadata.IsUnsigned 31 | ? BinaryPrimitives.ReadUInt64LittleEndian(buffer) 32 | : BinaryPrimitives.ReadInt64LittleEndian(buffer); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/LongType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | using System.Buffers.Binary; 6 | 7 | namespace SciSharp.MySQL.Replication.Types 8 | { 9 | /// 10 | /// Represents the MySQL INT data type. 11 | /// 12 | /// 13 | /// Handles the reading and conversion of MySQL INT values (4-byte integers). 14 | /// 15 | class LongType : IMySQLDataType 16 | { 17 | /// 18 | /// Reads an INT value from the binary log. 19 | /// 20 | /// The sequence reader containing the bytes to read. 21 | /// Metadata for the column. 22 | /// An integer representing the MySQL INT value. 23 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 24 | { 25 | Span buffer = stackalloc byte[sizeof(int)]; 26 | 27 | reader.TryCopyTo(buffer); 28 | reader.Advance(sizeof(int)); 29 | 30 | return columnMetadata.IsUnsigned 31 | ? BinaryPrimitives.ReadUInt32LittleEndian(buffer) 32 | : BinaryPrimitives.ReadInt32LittleEndian(buffer); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/NewDecimalType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | using System.Globalization; 6 | using System.Linq.Expressions; 7 | using System.Buffers.Binary; 8 | using System.Numerics; 9 | 10 | namespace SciSharp.MySQL.Replication.Types 11 | { 12 | /// 13 | /// Represents the MySQL DECIMAL data type. 14 | /// 15 | /// 16 | /// Handles the reading and conversion of MySQL DECIMAL values. 17 | /// 18 | class NewDecimalType : IMySQLDataType, IColumnMetadataLoader 19 | { 20 | private static readonly IReadOnlyList DIGITS_PER_INTEGER = new [] { 0, 9, 19, 28, 38 }; 21 | 22 | /// 23 | /// Loads metadata for the DECIMAL column type. 24 | /// 25 | /// the colummn metadata. 26 | public void LoadMetadataValue(ColumnMetadata columnMetadata) 27 | { 28 | var decimalOptions = new DecimalOptions 29 | { 30 | Precision = (int)columnMetadata.MetadataValue[0], 31 | Scale = (int)columnMetadata.MetadataValue[1] 32 | }; 33 | 34 | // Calculate storage size (MySQL packs 9 digits into 4 bytes) 35 | decimalOptions.IntegerBytes = CalculateByteCount(decimalOptions.Precision - decimalOptions.Scale); 36 | decimalOptions.FractionBytes = CalculateByteCount(decimalOptions.Scale); 37 | 38 | columnMetadata.Options = decimalOptions; 39 | } 40 | 41 | /// 42 | /// Reads a DECIMAL value from the binary log. 43 | /// 44 | /// The sequence reader containing the bytes to read. 45 | /// Metadata for the column. 46 | /// A decimal value representing the MySQL DECIMAL value. 47 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 48 | { 49 | var options = columnMetadata.Options as DecimalOptions; 50 | 51 | reader.TryPeek(out byte signByte); 52 | bool negative = (signByte & 0x80) == 0x00; 53 | signByte ^= 0x80; 54 | 55 | // Read integer part 56 | var intPart = ReadCompactDecimal(ref reader, options.IntegerBytes, negative, signByte); 57 | 58 | // Read integer part 59 | var fractionPart = ReadCompactDecimal(ref reader, options.FractionBytes, negative); 60 | 61 | // Convert to decimal using direct decimal operations 62 | decimal fraction = (decimal)fractionPart; 63 | 64 | if (options.Scale > 0) 65 | { 66 | // Create the appropriate scaling factor based on the scale 67 | decimal scaleFactor = (decimal)Math.Pow(10, options.Scale); 68 | fraction = fraction / scaleFactor; 69 | } 70 | 71 | var result = (decimal)intPart + fraction; 72 | 73 | // Apply sign 74 | return negative ? -result : result; 75 | } 76 | 77 | private static BigInteger ReadCompactDecimal(ref SequenceReader reader, int byteCount, bool flip, byte? signByteOverride = null) 78 | { 79 | if (byteCount == 0) 80 | return BigInteger.Zero; 81 | 82 | Span bytes = stackalloc byte[byteCount]; 83 | reader.TryCopyTo(bytes); 84 | reader.Advance(byteCount); 85 | 86 | // Handle sign bit in the integer part 87 | if (signByteOverride.HasValue) 88 | bytes[0] = signByteOverride.Value; 89 | 90 | // Process each 4-byte group 91 | BigInteger result = BigInteger.Zero; 92 | 93 | for (int i = 0; i < byteCount; i += 4) 94 | { 95 | int groupSize = Math.Min(4, byteCount - i); 96 | int value = 0; 97 | 98 | // Combine bytes in group (big-endian within the group) 99 | for (int j = 0; j < groupSize; j++) 100 | { 101 | var cellValue = bytes[i + j]; 102 | 103 | if (flip) 104 | cellValue = (byte)(~cellValue); 105 | 106 | value = (value << 8) | cellValue; 107 | } 108 | 109 | // Each group represents a specific number of decimal digits 110 | int digitCount = Math.Min(9, DIGITS_PER_INTEGER[groupSize]); 111 | result = result * BigInteger.Pow(10, digitCount) + value; 112 | } 113 | 114 | return result; 115 | } 116 | 117 | private int CalculateByteCount(int digits) 118 | { 119 | if (digits == 0) 120 | return 0; 121 | 122 | return digits / 9 * 4 + (digits % 9 / 2) + (digits % 2); 123 | } 124 | 125 | class DecimalOptions 126 | { 127 | public int Precision { get; set; } 128 | public int Scale { get; set; } 129 | public int IntegerBytes { get; set; } 130 | public int FractionBytes { get; set; } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/SetType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Generic; 4 | 5 | namespace SciSharp.MySQL.Replication.Types 6 | { 7 | /// 8 | /// Represents the MySQL SET data type. 9 | /// 10 | /// 11 | /// Handles the reading and conversion of MySQL SET values. 12 | /// 13 | class SetType : IMySQLDataType, IColumnMetadataLoader 14 | { 15 | public void LoadMetadataValue(ColumnMetadata columnMetadata) 16 | { 17 | columnMetadata.MaxLength = columnMetadata.MetadataValue[1]; 18 | } 19 | 20 | /// 21 | /// Reads a SET value from the binary log. 22 | /// 23 | /// The sequence reader containing the bytes to read. 24 | /// Metadata for the column. 25 | /// A long value representing the MySQL SET value as a bitmap. 26 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 27 | { 28 | var flags = reader.ReadLong(columnMetadata.MaxLength); 29 | 30 | if (flags == 0) 31 | { 32 | return string.Empty; 33 | } 34 | 35 | var setCellValues = new List(); 36 | 37 | for (int i = 0; i < columnMetadata.SetValues.Count; i++) 38 | { 39 | if ((flags & (1 << i)) != 0) 40 | { 41 | setCellValues.Add(columnMetadata.SetValues[i]); 42 | } 43 | } 44 | 45 | return string.Join(",", setCellValues); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/ShortType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | using System.Buffers.Binary; 6 | 7 | namespace SciSharp.MySQL.Replication.Types 8 | { 9 | /// 10 | /// Represents the MySQL SMALLINT data type. 11 | /// 12 | /// 13 | /// Handles the reading and conversion of MySQL SMALLINT values (2-byte integers). 14 | /// 15 | class ShortType : IMySQLDataType 16 | { 17 | /// 18 | /// Reads a SMALLINT value from the binary log. 19 | /// 20 | /// The sequence reader containing the bytes to read. 21 | /// Metadata for the column. 22 | /// A short value representing the MySQL SMALLINT value. 23 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 24 | { 25 | Span buffer = stackalloc byte[sizeof(short)]; 26 | 27 | reader.TryCopyTo(buffer); 28 | reader.Advance(sizeof(short)); 29 | 30 | return columnMetadata.IsUnsigned 31 | ? (ushort)BinaryPrimitives.ReadUInt16LittleEndian(buffer) 32 | : (short)BinaryPrimitives.ReadInt16LittleEndian(buffer); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/StringType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | 6 | namespace SciSharp.MySQL.Replication.Types 7 | { 8 | /// 9 | /// Represents the MySQL STRING data type. 10 | /// 11 | /// 12 | /// Handles the reading and conversion of MySQL STRING values. 13 | /// 14 | class StringType : IMySQLDataType 15 | { 16 | /// 17 | /// Reads a STRING value from the binary log. 18 | /// 19 | /// The sequence reader containing the bytes to read. 20 | /// Metadata for the column. 21 | /// A string representing the MySQL STRING value. 22 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 23 | { 24 | return reader.ReadLengthEncodedString(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/TimeBaseType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | 4 | namespace SciSharp.MySQL.Replication.Types 5 | { 6 | /// 7 | /// Base class for time-related MySQL data types. 8 | /// Provides common functionality for handling fractional seconds. 9 | /// 10 | abstract class TimeBaseType 11 | { 12 | /// 13 | /// Reads fractional seconds from the binary representation. 14 | /// 15 | /// The sequence reader containing the binary data. 16 | /// The metadata defining the length of the fractional seconds part. 17 | /// The fractional seconds value. 18 | protected int ReadFractionalSeconds(ref SequenceReader reader, int meta) 19 | { 20 | int length = (meta + 1) / 2; 21 | 22 | if (length <= 0) 23 | return 0; 24 | 25 | int fraction = reader.ReadBigEndianInteger(length); 26 | return fraction * (int) Math.Pow(100, 3 - length); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/TimeType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | 6 | namespace SciSharp.MySQL.Replication.Types 7 | { 8 | /// 9 | /// Represents the MySQL TIME data type. 10 | /// 11 | /// 12 | /// Handles the reading and conversion of MySQL TIME values from binary log. 13 | /// 14 | class TimeType : IMySQLDataType 15 | { 16 | /// 17 | /// Reads a TIME value from the binary log. 18 | /// 19 | /// The sequence reader containing the bytes to read. 20 | /// Metadata for the column. 21 | /// A TimeSpan representing the MySQL TIME value. 22 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 23 | { 24 | // In binary log format, TIME is encoded as a 3-byte integer 25 | int encodedValue = 0; 26 | 27 | reader.TryRead(out byte b0); 28 | reader.TryRead(out byte b1); 29 | reader.TryRead(out byte b2); 30 | 31 | // Combine the 3 bytes into an integer (little-endian) 32 | encodedValue = b0 | (b1 << 8) | (b2 << 16); 33 | 34 | // Check if negative (bit 23 is the sign bit) 35 | bool isNegative = (encodedValue & 0x800000) != 0; 36 | 37 | // Handle negative values 38 | if (isNegative) 39 | { 40 | // Clear the sign bit for calculations 41 | encodedValue &= 0x7FFFFF; 42 | } 43 | 44 | // Binary log format has time in a packed decimal format: 45 | // HHHHHH MMMMMM SSSSSS (each field taking 6 bits in binary log) 46 | // But we need to convert it to HHMMSS for TimeSpan 47 | 48 | int hours = encodedValue / 10000; 49 | int minutes = (encodedValue % 10000) / 100; 50 | int seconds = encodedValue % 100; 51 | 52 | // Validate time components 53 | if (hours > 838 || minutes > 59 || seconds > 59) 54 | { 55 | throw new InvalidOperationException($"Invalid TIME value: {encodedValue}"); 56 | } 57 | 58 | // Create TimeSpan (MySQL TIME can represent larger ranges than .NET TimeSpan) 59 | TimeSpan result = new TimeSpan(hours, minutes, seconds); 60 | 61 | // Apply sign if needed 62 | return isNegative ? result.Negate() : result; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/TimeV2Type.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | 6 | namespace SciSharp.MySQL.Replication.Types 7 | { 8 | /// 9 | /// Represents the MySQL TIME2 data type. 10 | /// 11 | /// 12 | /// Handles the reading and conversion of MySQL TIME values with fractional seconds. 13 | /// 14 | class TimeV2Type : IMySQLDataType, IColumnMetadataLoader 15 | { 16 | /// 17 | /// Loads the fractional seconds precision from the column metadata. 18 | /// 19 | /// The column metadata containing the precision value. 20 | public void LoadMetadataValue(ColumnMetadata columnMetadata) 21 | { 22 | // For TIME2 type, the metadata value represents the precision of fractional seconds 23 | // MySQL supports precision values from 0 to 6 (microsecond precision) 24 | columnMetadata.Options = new TimeV2Options 25 | { 26 | FractionalSecondsPrecision = columnMetadata.MetadataValue[0] 27 | }; 28 | } 29 | 30 | /// 31 | /// Reads a TIME2 value from the binary log. 32 | /// 33 | /// The sequence reader containing the bytes to read. 34 | /// Metadata for the column defining fractional second precision. 35 | /// A TimeSpan representing the MySQL TIME2 value. 36 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 37 | { 38 | // Get the fractional seconds precision from metadata (0-6) 39 | int fsp = columnMetadata.Options is TimeV2Options options ? options.FractionalSecondsPrecision : 0; 40 | 41 | // Read the integer part (3 bytes) 42 | byte intPartByte1, intPartByte2, intPartByte3; 43 | reader.TryRead(out intPartByte1); 44 | reader.TryRead(out intPartByte2); 45 | reader.TryRead(out intPartByte3); 46 | 47 | // Combine into a 24-bit integer, bigendian 48 | int intPart = (intPartByte1 << 16) | (intPartByte2 << 8) | intPartByte3; 49 | 50 | // In MySQL 5.6.4+ TIME2 format: 51 | // Bit 1 (MSB): Sign bit (1=negative, 0=positive) 52 | // Bits 2-24: Packed BCD encoding of time value 53 | bool isNegative = ((intPart & 0x800000) == 0); // In MySQL TIME2, 0 is negative, 1 is positive 54 | 55 | // If negative, apply two's complement 56 | if (isNegative) 57 | { 58 | intPart = ~intPart + 1; 59 | intPart &= 0x7FFFFF; // Keep only the 23 bits for the absolute value 60 | } 61 | 62 | // TIME2 is packed in a special format: 63 | // Bits 2-13: Hours (12 bits) 64 | // Bits 14-19: Minutes (6 bits) 65 | // Bits 20-25: Seconds (6 bits) 66 | int hours = (intPart >> 12) & 0x3FF; 67 | int minutes = (intPart >> 6) & 0x3F; 68 | int seconds = intPart & 0x3F; 69 | 70 | // Read fractional seconds if precision > 0 71 | int microseconds = 0; 72 | if (fsp > 0) 73 | { 74 | // Calculate bytes needed for the requested precision 75 | int fractionalBytes = (fsp + 1) / 2; 76 | int fraction = 0; 77 | 78 | // Read bytes for fractional seconds 79 | for (int i = 0; i < fractionalBytes; i++) 80 | { 81 | byte b; 82 | reader.TryRead(out b); 83 | fraction = (fraction << 8) | b; 84 | } 85 | 86 | // Convert to microseconds based on precision 87 | int scaleFactor = 1000000; 88 | switch (fsp) 89 | { 90 | case 1: scaleFactor = 100000; break; 91 | case 2: scaleFactor = 10000; break; 92 | case 3: scaleFactor = 1000; break; 93 | case 4: scaleFactor = 100; break; 94 | case 5: scaleFactor = 10; break; 95 | case 6: scaleFactor = 1; break; 96 | } 97 | 98 | microseconds = fraction * scaleFactor; 99 | } 100 | 101 | // Create TimeSpan 102 | TimeSpan result; 103 | 104 | if (hours >= 24) 105 | { 106 | // For large hour values, convert to days + remaining hours 107 | int days = hours / 24; 108 | int remainingHours = hours % 24; 109 | 110 | // Create TimeSpan with days, hours, minutes, seconds, ms 111 | result = new TimeSpan(days, remainingHours, minutes, seconds, microseconds / 1000); 112 | 113 | // Add remaining microseconds as ticks (1 tick = 100 nanoseconds, 1 microsecond = 10 ticks) 114 | if (microseconds % 1000 > 0) 115 | { 116 | result = result.Add(TimeSpan.FromTicks((microseconds % 1000) * 10)); 117 | } 118 | } 119 | else 120 | { 121 | // Standard case for hours < 24 122 | result = new TimeSpan(0, hours, minutes, seconds, microseconds / 1000); 123 | 124 | // Add microsecond precision as ticks 125 | if (microseconds % 1000 > 0) 126 | { 127 | result = result.Add(TimeSpan.FromTicks((microseconds % 1000) * 10)); 128 | } 129 | } 130 | 131 | // Apply sign 132 | return isNegative ? result.Negate() : result; 133 | } 134 | } 135 | 136 | class TimeV2Options 137 | { 138 | public int FractionalSecondsPrecision { get; set; } = 0; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/TimestampType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | 6 | namespace SciSharp.MySQL.Replication.Types 7 | { 8 | /// 9 | /// Represents the MySQL TIMESTAMP data type. 10 | /// 11 | /// 12 | /// Handles the reading and conversion of MySQL TIMESTAMP values. 13 | /// 14 | class TimestampType : IMySQLDataType 15 | { 16 | // Unix epoch start for MySQL TIMESTAMP (1970-01-01 00:00:00 UTC) 17 | private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); 18 | 19 | /// 20 | /// Reads a TIMESTAMP value from the binary log. 21 | /// 22 | /// The sequence reader containing the bytes to read. 23 | /// Metadata for the column. 24 | /// A DateTime object representing the MySQL TIMESTAMP value. 25 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 26 | { 27 | // MySQL TIMESTAMP is stored as a 4-byte integer 28 | // representing seconds since Unix epoch (1970-01-01 00:00:00 UTC) 29 | 30 | // Read the 4 bytes as a 32-bit unsigned integer 31 | uint secondsSinceEpoch = 0; 32 | 33 | // Read each byte individually 34 | reader.TryRead(out byte b0); 35 | reader.TryRead(out byte b1); 36 | reader.TryRead(out byte b2); 37 | reader.TryRead(out byte b3); 38 | 39 | // Combine bytes to form the 32-bit unsigned int (big-endian) 40 | secondsSinceEpoch = ((uint)b0 << 24) | 41 | ((uint)b1 << 16) | 42 | ((uint)b2 << 8) | 43 | b3; 44 | 45 | // Convert Unix timestamp to DateTime 46 | // MySQL stores TIMESTAMP in UTC, so we return it as UTC DateTime 47 | DateTime timestamp = UnixEpoch.AddSeconds(secondsSinceEpoch); 48 | 49 | return timestamp; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/TimestampV2Type.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | 6 | namespace SciSharp.MySQL.Replication.Types 7 | { 8 | /// 9 | /// Represents the MySQL TIMESTAMP2 data type. 10 | /// 11 | /// 12 | /// Handles the reading and conversion of MySQL TIMESTAMP values with fractional seconds. 13 | /// 14 | class TimestampV2Type : TimeBaseType, IMySQLDataType, IColumnMetadataLoader 15 | { 16 | // Unix epoch start for MySQL TIMESTAMP (1970-01-01 00:00:00 UTC) 17 | private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); 18 | 19 | /// 20 | /// Loads metadata for TIMESTAMP2 type. 21 | /// 22 | /// The column metadata object. 23 | public void LoadMetadataValue(ColumnMetadata columnMetadata) 24 | { 25 | // For TIMESTAMP2, the metadata value represents the fractional seconds precision (0-6) 26 | columnMetadata.Options = new TimestampV2Options 27 | { 28 | FractionalSecondsPrecision = columnMetadata.MetadataValue[0] 29 | }; 30 | } 31 | 32 | /// 33 | /// Reads a TIMESTAMP2 value from the binary log. 34 | /// 35 | /// The sequence reader containing the bytes to read. 36 | /// Metadata for the column defining fractional second precision. 37 | /// A DateTime object representing the MySQL TIMESTAMP2 value. 38 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 39 | { 40 | // Get the precision from metadata 41 | int fsp = columnMetadata.Options is TimestampV2Options options ? 42 | options.FractionalSecondsPrecision : 0; 43 | 44 | // Read the 4-byte seconds part (big-endian integer representing seconds since Unix epoch) 45 | byte b0, b1, b2, b3; 46 | reader.TryRead(out b0); 47 | reader.TryRead(out b1); 48 | reader.TryRead(out b2); 49 | reader.TryRead(out b3); 50 | 51 | uint secondsSinceEpoch = ((uint)b0 << 24) | ((uint)b1 << 16) | ((uint)b2 << 8) | b3; 52 | 53 | // Start with the base DateTime (seconds part) 54 | DateTime timestamp = UnixEpoch.AddSeconds(secondsSinceEpoch); 55 | 56 | // Read fractional seconds if precision > 0 57 | if (fsp > 0) 58 | { 59 | int fractionalValue = ReadFractionalSeconds(ref reader, fsp); 60 | 61 | // Calculate microseconds 62 | int microseconds = 0; 63 | switch (fsp) 64 | { 65 | case 1: microseconds = fractionalValue * 100000; break; 66 | case 2: microseconds = fractionalValue * 10000; break; 67 | case 3: microseconds = fractionalValue * 1000; break; 68 | case 4: microseconds = fractionalValue * 100; break; 69 | case 5: microseconds = fractionalValue * 10; break; 70 | case 6: microseconds = fractionalValue; break; 71 | } 72 | 73 | // Add microsecond precision to the DateTime 74 | // 1 tick = 100 nanoseconds, 1 microsecond = 10 ticks 75 | timestamp = timestamp.AddTicks(microseconds * 10); 76 | } 77 | 78 | return timestamp; 79 | } 80 | } 81 | 82 | /// 83 | /// Options specific to TIMESTAMP2 data type. 84 | /// 85 | class TimestampV2Options 86 | { 87 | /// 88 | /// Gets or sets the precision of fractional seconds (0-6). 89 | /// 90 | public int FractionalSecondsPrecision { get; set; } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/TinyType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | 4 | namespace SciSharp.MySQL.Replication.Types 5 | { 6 | /// 7 | /// Represents the MySQL TINYINT data type. 8 | /// 9 | /// 10 | /// Handles the reading and conversion of MySQL TINYINT values (1-byte integers). 11 | /// 12 | class TinyType : IMySQLDataType 13 | { 14 | /// 15 | /// Reads a TINYINT value from the binary log. 16 | /// 17 | /// The sequence reader containing the bytes to read. 18 | /// Metadata for the column. 19 | /// A byte value representing the MySQL TINYINT value. 20 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 21 | { 22 | reader.TryRead(out byte x); 23 | return columnMetadata.IsUnsigned 24 | ? x 25 | : (sbyte)x; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/VarCharType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Buffers; 5 | using SuperSocket.ProtoBase; 6 | using System.Text; 7 | using System.Buffers.Binary; 8 | 9 | namespace SciSharp.MySQL.Replication.Types 10 | { 11 | /// 12 | /// Represents the MySQL VARCHAR data type. 13 | /// 14 | /// 15 | /// Handles the reading and conversion of MySQL VARCHAR values. 16 | /// 17 | class VarCharType : IMySQLDataType, IColumnMetadataLoader 18 | { 19 | public void LoadMetadataValue(ColumnMetadata columnMetadata) 20 | { 21 | if (columnMetadata.MetadataValue.Length == 1) 22 | { 23 | columnMetadata.MaxLength = (int)columnMetadata.MetadataValue[0]; 24 | } 25 | else 26 | { 27 | columnMetadata.MaxLength = (int)columnMetadata.MetadataValue[1] * 256 + (int)columnMetadata.MetadataValue[0]; 28 | } 29 | } 30 | 31 | /// 32 | /// Reads a VARCHAR value from the binary log. 33 | /// 34 | /// The sequence reader containing the bytes to read. 35 | /// Metadata for the column. 36 | /// A string representing the MySQL VARCHAR value. 37 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 38 | { 39 | var lenBytes = columnMetadata.MaxLength < 256 ? 1 : 2; 40 | var length = lenBytes == 1 41 | ? reader.TryRead(out byte len1) ? len1 : 0 42 | : reader.TryReadLittleEndian(out short len2) ? len2 : 0; 43 | 44 | if (length == 0) 45 | return string.Empty; 46 | 47 | try 48 | { 49 | if (lenBytes == 1) 50 | { 51 | if (reader.TryPeek(out byte checkByte) && checkByte == 0x00) 52 | { 53 | reader.Advance(1); 54 | } 55 | } 56 | 57 | return reader.UnreadSequence.Slice(0, length).GetString(Encoding.UTF8); 58 | } 59 | finally 60 | { 61 | reader.Advance(length); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/SciSharp.MySQL.Replication/Types/YearType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | 4 | namespace SciSharp.MySQL.Replication.Types 5 | { 6 | /// 7 | /// Represents the MySQL YEAR data type. 8 | /// 9 | /// 10 | /// Handles the reading and conversion of MySQL YEAR values. 11 | /// 12 | class YearType : IMySQLDataType 13 | { 14 | /// 15 | /// Reads a YEAR value from the binary log. 16 | /// 17 | /// The sequence reader containing the bytes to read. 18 | /// Metadata for the column. 19 | /// An integer representing the MySQL YEAR value. 20 | public object ReadValue(ref SequenceReader reader, ColumnMetadata columnMetadata) 21 | { 22 | return 1900 + reader.ReadInteger(1); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Test/DataTypesTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using System.Text.Json; 6 | using MySql.Data.MySqlClient; 7 | using SciSharp.MySQL.Replication; 8 | using SciSharp.MySQL.Replication.Events; 9 | using Xunit; 10 | 11 | namespace Test 12 | { 13 | [Trait("Category", "DataTypes")] 14 | public class DataTypesTest 15 | { 16 | [Fact] 17 | public async Task TestDateTimeType() 18 | { 19 | var currentValue = DateTime.Now; 20 | currentValue = new DateTime(currentValue.Year, currentValue.Month, currentValue.Day, currentValue.Hour, currentValue.Minute, currentValue.Second); 21 | 22 | await TestDataType("datetime_table", currentValue, currentValue.AddDays(1), (reader, index) => 23 | { 24 | return reader.GetDateTime(index); 25 | }); 26 | } 27 | 28 | [Fact] 29 | public async Task TestIntType() 30 | { 31 | var currentValue = 42; 32 | await TestDataType("int_table", currentValue, currentValue + 10, (reader, index) => 33 | { 34 | return reader.GetInt32(index); 35 | }); 36 | } 37 | 38 | [Fact] 39 | public async Task TestBigIntType() 40 | { 41 | var currentValue = 9223372036854775800L; 42 | await TestDataType("bigint_table", currentValue, currentValue - 10, (reader, index) => 43 | { 44 | return reader.GetInt64(index); 45 | }); 46 | } 47 | 48 | [Fact] 49 | public async Task TestTinyIntType() 50 | { 51 | var currentValue = (sbyte)42; 52 | await TestDataType("tinyint_table", currentValue, (sbyte)(currentValue + 10), (reader, index) => 53 | { 54 | return (sbyte)reader.GetByte(index); 55 | }); 56 | } 57 | 58 | [Fact] 59 | public async Task TestSmallIntType() 60 | { 61 | var currentValue = (short)1000; 62 | await TestDataType("smallint_table", currentValue, (short)(currentValue + 10), (reader, index) => 63 | { 64 | return reader.GetInt16(index); 65 | }); 66 | } 67 | 68 | [Fact] 69 | public async Task TestMediumIntType() 70 | { 71 | var currentValue = 8388000; // Close to 2^23 limit for MEDIUMINT 72 | await TestDataType("mediumint_table", currentValue, currentValue + 10, (reader, index) => 73 | { 74 | return reader.GetInt32(index); 75 | }); 76 | } 77 | 78 | [Fact] 79 | public async Task TestVarCharType() 80 | { 81 | var currentValue = "Hello World"; 82 | await TestDataType("varchar_table", currentValue, currentValue + " Updated", (reader, index) => 83 | { 84 | return reader.GetString(index); 85 | }); 86 | } 87 | 88 | [Fact] 89 | public async Task TestDecimalType() 90 | { 91 | var currentValue = 123.45m; 92 | await TestDataType("decimal_table", currentValue, currentValue + 10.55m, (reader, index) => 93 | { 94 | return reader.GetDecimal(index); 95 | }); 96 | 97 | currentValue = -123.45m; 98 | await TestDataType("decimal_table", currentValue, currentValue + 10.55m, (reader, index) => 99 | { 100 | return reader.GetDecimal(index); 101 | }); 102 | } 103 | 104 | [Fact] 105 | public async Task TestFloatType() 106 | { 107 | var currentValue = 123.45f; 108 | await TestDataType("float_table", currentValue, currentValue + 10.55f, (reader, index) => 109 | { 110 | return reader.GetFloat(index); 111 | }); 112 | } 113 | 114 | [Fact] 115 | public async Task TestDoubleType() 116 | { 117 | var currentValue = 123456.789012; 118 | await TestDataType("double_table", currentValue, currentValue + 100.123, (reader, index) => 119 | { 120 | return reader.GetDouble(index); 121 | }); 122 | } 123 | 124 | [Fact] 125 | public async Task TestDateType() 126 | { 127 | var currentValue = DateTime.Today; 128 | await TestDataType("date_table", currentValue, currentValue.AddDays(5), (reader, index) => 129 | { 130 | return reader.GetDateTime(index).Date; 131 | }); 132 | } 133 | 134 | [Fact] 135 | public async Task TestTimeType() 136 | { 137 | var currentValue = new TimeSpan(10, 30, 45); 138 | await TestDataType("time_table", currentValue, currentValue.Add(new TimeSpan(1, 15, 30)), (reader, index) => 139 | { 140 | return reader.GetTimeSpan(index); 141 | }); 142 | } 143 | 144 | [Fact] 145 | public async Task TestTimestampType() 146 | { 147 | var currentValue = DateTime.UtcNow; 148 | currentValue = new DateTime(currentValue.Year, currentValue.Month, currentValue.Day, currentValue.Hour, currentValue.Minute, currentValue.Second, DateTimeKind.Utc); 149 | 150 | await TestDataType("timestamp_table", currentValue, currentValue.AddHours(1), (reader, index) => 151 | { 152 | return DateTime.SpecifyKind(reader.GetDateTime(index), DateTimeKind.Utc); 153 | }); 154 | } 155 | 156 | [Fact] 157 | public async Task TestBlobType() 158 | { 159 | var currentValue = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; 160 | var updateValue = new byte[] { 0x06, 0x07, 0x08, 0x09, 0x0A }; 161 | 162 | await TestDataType("blob_table", currentValue, updateValue, (reader, index) => 163 | { 164 | var length = (int)reader.GetBytes(index, 0, null, 0, 0); 165 | var buffer = new byte[length]; 166 | reader.GetBytes(index, 0, buffer, 0, length); 167 | return buffer; 168 | }); 169 | } 170 | 171 | [Fact] 172 | public async Task TestEnumType() 173 | { 174 | var currentValue = "SMALL"; 175 | await TestDataType("enum_table", currentValue, "MEDIUM", (reader, index) => 176 | { 177 | return reader.GetString(index); 178 | }); 179 | } 180 | 181 | [Fact] 182 | public async Task TestSetType() 183 | { 184 | var currentValue = "RED,GREEN"; 185 | await TestDataType("set_table", currentValue, "RED,BLUE", (reader, index) => 186 | { 187 | return reader.GetString(index); 188 | }); 189 | } 190 | 191 | //[Fact] 192 | public async Task TestJsonType() 193 | { 194 | var currentValue = @"{""name"": ""John"", ""age"": 30}"; 195 | var updateValue = @"{""name"": ""Jane"", ""age"": 25, ""city"": ""New York""}"; 196 | 197 | await TestDataType("json_table", currentValue, updateValue, (reader, index) => 198 | { 199 | return reader.GetString(index); 200 | }); 201 | } 202 | 203 | [Fact] 204 | public async Task TestYearType() 205 | { 206 | var currentValue = 2023; 207 | await TestDataType("year_table", currentValue, 2024, (reader, index) => 208 | { 209 | return reader.GetInt16(index); 210 | }); 211 | } 212 | 213 | private async Task TestDataType(string tableName, TDateType currentValue, TDateType updateValue, Func dataReader) 214 | { 215 | var mysqlFixture = MySQLFixture.CreateMySQLFixture(); 216 | 217 | // Insert a new row into the table 218 | var command = mysqlFixture.CreateCommand(); 219 | command.CommandText = $"insert into {tableName} (value) values (@value);SELECT LAST_INSERT_ID();"; 220 | command.Parameters.AddWithValue("@value", currentValue); 221 | var id = (Int32)(UInt64)await command.ExecuteScalarAsync(); 222 | 223 | // Validate the WriteRowsEvent 224 | var writeRowsEvent = await mysqlFixture.ReceiveAsync(); 225 | 226 | Assert.Equal(1, writeRowsEvent.RowSet.Rows.Count); 227 | Assert.Equal("id", writeRowsEvent.RowSet.ColumnNames[0]); 228 | Assert.Equal("value", writeRowsEvent.RowSet.ColumnNames[1]); 229 | var idFromClient = writeRowsEvent.RowSet.Rows[0][0]; 230 | var valueFromClient = writeRowsEvent.RowSet.Rows[0][1]; 231 | Assert.Equal(id, (Int32)idFromClient); 232 | Assert.NotNull(valueFromClient); 233 | Assert.Equal(currentValue, (TDateType)valueFromClient); 234 | 235 | // Validate the data in the database with query 236 | command = mysqlFixture.CreateCommand(); 237 | command.CommandText = $"select value from {tableName} where id = @id"; 238 | command.Parameters.AddWithValue("@id", id); 239 | 240 | using var reader = await command.ExecuteReaderAsync() as MySqlDataReader; 241 | 242 | Assert.True(await reader.ReadAsync()); 243 | 244 | var savedValue = dataReader(reader, 0); 245 | await reader.CloseAsync(); 246 | 247 | Assert.Equal(currentValue, savedValue); 248 | 249 | // Update the row 250 | command = mysqlFixture.CreateCommand(); 251 | command.CommandText = $"update {tableName} set value=@value where id = @id"; 252 | command.Parameters.AddWithValue("@id", id); 253 | command.Parameters.AddWithValue("@value", updateValue); 254 | 255 | Assert.Equal(1, await command.ExecuteNonQueryAsync()); 256 | 257 | // Validate the UpdateRowsEvent 258 | var updateRowsEvent = await mysqlFixture.ReceiveAsync(); 259 | Assert.Equal(1, updateRowsEvent.RowSet.Rows.Count); 260 | Assert.Equal("id", updateRowsEvent.RowSet.ColumnNames[0]); 261 | Assert.Equal("value", updateRowsEvent.RowSet.ColumnNames[1]); 262 | var idCellValue = updateRowsEvent.RowSet.Rows[0][0] as CellValue; 263 | var valueCellValue = updateRowsEvent.RowSet.Rows[0][1] as CellValue; 264 | Assert.NotNull(idCellValue); 265 | Assert.Equal(id, (Int32)idCellValue.NewValue); 266 | Assert.Equal(id, (Int32)idCellValue.OldValue); 267 | Assert.NotNull(valueCellValue); 268 | Assert.Equal(currentValue, valueCellValue.OldValue); 269 | Assert.Equal(updateValue, valueCellValue.NewValue); 270 | 271 | // Delete the row 272 | command = mysqlFixture.CreateCommand(); 273 | command.CommandText = $"delete from {tableName} where id = @id"; 274 | command.Parameters.AddWithValue("@id", id); 275 | 276 | await command.ExecuteNonQueryAsync(); 277 | 278 | // Validate the DeleteRowsEvent 279 | var deleteRowsEvent = await mysqlFixture.ReceiveAsync(); 280 | Assert.Equal(1, deleteRowsEvent.RowSet.Rows.Count); 281 | Assert.Equal("id", deleteRowsEvent.RowSet.ColumnNames[0]); 282 | Assert.Equal("value", deleteRowsEvent.RowSet.ColumnNames[1]); 283 | idFromClient = deleteRowsEvent.RowSet.Rows[0][0]; 284 | valueFromClient = deleteRowsEvent.RowSet.Rows[0][1]; 285 | Assert.Equal(id, (Int32)idFromClient); 286 | Assert.NotNull(valueFromClient); 287 | Assert.Equal(updateValue, (TDateType)valueFromClient); 288 | } 289 | } 290 | } -------------------------------------------------------------------------------- /tests/Test/MainTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using MySql.Data.MySqlClient; 6 | using SciSharp.MySQL.Replication; 7 | using Xunit; 8 | using Xunit.Abstractions; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Extensions.Logging.Console; 11 | using SciSharp.MySQL.Replication.Events; 12 | 13 | namespace Test 14 | { 15 | [Trait("Category", "Replication")] 16 | public class MainTest 17 | { 18 | protected readonly ITestOutputHelper _outputHelper; 19 | 20 | public MainTest(ITestOutputHelper outputHelper) 21 | { 22 | _outputHelper = outputHelper; 23 | } 24 | 25 | [Fact] 26 | public async Task TestReceiveEvent() 27 | { 28 | using var mysqlFixture = MySQLFixture.CreateMySQLFixture(); 29 | 30 | // insert 31 | var cmd = mysqlFixture.CreateCommand(); 32 | cmd.CommandText = "INSERT INTO pet (name, owner, species, sex, birth, death) values ('Rokie', 'Kerry', 'abc', 'F', '1982-04-20', '3000-01-01'); SELECT LAST_INSERT_ID();"; 33 | var id = (UInt64)(await cmd.ExecuteScalarAsync()); 34 | 35 | // update 36 | cmd = mysqlFixture.CreateCommand(); 37 | cmd.CommandText = "update pet set owner='Linda' where `id`=" + id; 38 | await cmd.ExecuteNonQueryAsync(); 39 | 40 | // delete 41 | cmd = mysqlFixture.CreateCommand(); 42 | cmd.CommandText = "delete from pet where `id`= " + id; 43 | await cmd.ExecuteNonQueryAsync(); 44 | 45 | RowsEvent eventLog = await mysqlFixture.ReceiveAsync(); 46 | Assert.NotNull(eventLog); 47 | _outputHelper.WriteLine(eventLog.ToString() + "\r\n"); 48 | 49 | eventLog = await mysqlFixture.ReceiveAsync(); 50 | Assert.NotNull(eventLog); 51 | _outputHelper.WriteLine(eventLog.ToString() + "\r\n"); 52 | 53 | eventLog = await mysqlFixture.ReceiveAsync(); 54 | Assert.NotNull(eventLog); 55 | _outputHelper.WriteLine(eventLog.ToString() + "\r\n"); 56 | } 57 | 58 | [Fact] 59 | public async Task TestInsertEvent() 60 | { 61 | using var mysqlFixture = MySQLFixture.CreateMySQLFixture(); 62 | 63 | // insert 64 | var cmd = mysqlFixture.CreateCommand(); 65 | cmd.CommandText = "INSERT INTO pet (name, owner, species, sex, birth, death, timeUpdated) values ('Rokie', 'Kerry', 'abc', 'F', '1992-05-20', '3000-01-01', now()); SELECT LAST_INSERT_ID();"; 66 | var id = (UInt64)(await cmd.ExecuteScalarAsync()); 67 | 68 | var eventLog = await mysqlFixture.ReceiveAsync(); 69 | 70 | Assert.NotNull(eventLog); 71 | 72 | var rows = eventLog.RowSet.ToReadableRows(); 73 | Assert.Equal(1, rows.Count); 74 | 75 | var row = rows[0]; 76 | 77 | Assert.Equal("Rokie", row["name"]); 78 | Assert.Equal("Kerry", row["owner"]); 79 | Assert.Equal("abc", row["species"]); 80 | Assert.Equal("F", row["sex"]); 81 | } 82 | 83 | [Fact] 84 | public async Task TestUpdateEvent() 85 | { 86 | using var mysqlFixture = MySQLFixture.CreateMySQLFixture(); 87 | 88 | // insert 89 | var cmd = mysqlFixture.CreateCommand(); 90 | cmd.CommandText = "INSERT INTO pet (name, owner, species, sex, birth, death, timeUpdated) values ('Rokie', 'Kerry', 'abc', 'F', '1992-05-20', '3000-01-01', now());"; 91 | await cmd.ExecuteNonQueryAsync(); 92 | 93 | // query 94 | cmd = mysqlFixture.CreateCommand(); 95 | cmd.CommandText = "select * from pet order by `id` desc limit 1;"; 96 | 97 | var oldValues = new Dictionary(); 98 | 99 | using (var reader = await cmd.ExecuteReaderAsync()) 100 | { 101 | Assert.True(await reader.ReadAsync()); 102 | 103 | for (var i = 0; i < reader.FieldCount ; i++) 104 | { 105 | oldValues.Add(reader.GetName(i), reader.GetValue(i)); 106 | } 107 | 108 | await reader.CloseAsync(); 109 | } 110 | 111 | var id = oldValues["id"]; 112 | 113 | // update 114 | cmd = mysqlFixture.CreateCommand(); 115 | cmd.CommandText = "update pet set owner='Linda', timeUpdated=now() where `id`=" + id; 116 | await cmd.ExecuteNonQueryAsync(); 117 | 118 | var eventLog = await mysqlFixture.ReceiveAsync(); 119 | 120 | _outputHelper.WriteLine(eventLog.ToString() + "\r\n"); 121 | 122 | if (eventLog.EventType == LogEventType.UPDATE_ROWS_EVENT) 123 | { 124 | Assert.NotNull(eventLog); 125 | 126 | var rows = eventLog.RowSet.ToReadableRows(); 127 | Assert.Equal(1, rows.Count); 128 | 129 | var row = rows[0]; 130 | 131 | var cellValue = row["id"] as CellValue; 132 | 133 | Assert.Equal(id, cellValue.OldValue); 134 | Assert.Equal(id, cellValue.NewValue); 135 | 136 | cellValue = row["owner"] as CellValue; 137 | 138 | Assert.Equal("Kerry", oldValues["owner"]); 139 | Assert.Equal("Kerry", cellValue.OldValue); 140 | Assert.Equal("Linda", cellValue.NewValue); 141 | } 142 | } 143 | 144 | [Fact] 145 | public async Task TestGetEventLogStream() 146 | { 147 | var mysqlFixture = MySQLFixture.CreateMySQLFixture(); 148 | 149 | // Insert a new pet 150 | var cmd = mysqlFixture.CreateCommand(); 151 | cmd.CommandText = "INSERT INTO pet (name, owner, species, sex, birth, death) values ('Buddy', 'Alex', 'dog', 'M', '2020-01-15', NULL); SELECT LAST_INSERT_ID();"; 152 | var id = (UInt64)(await cmd.ExecuteScalarAsync()); 153 | 154 | // Update the pet 155 | cmd = mysqlFixture.CreateCommand(); 156 | cmd.CommandText = $"UPDATE pet SET owner='Sarah' WHERE id={id}"; 157 | await cmd.ExecuteNonQueryAsync(); 158 | 159 | // Delete the pet 160 | cmd = mysqlFixture.CreateCommand(); 161 | cmd.CommandText = $"DELETE FROM pet WHERE id={id}"; 162 | await cmd.ExecuteNonQueryAsync(); 163 | 164 | // Use the client's GetEventLogStream to process events 165 | var eventCount = 0; 166 | var sawInsert = false; 167 | var sawUpdate = false; 168 | var sawDelete = false; 169 | 170 | // Process only the next 5 events (or fewer if we reach the end) 171 | await foreach (var logEvent in mysqlFixture.Client.GetEventLogStream()) 172 | { 173 | eventCount++; 174 | _outputHelper.WriteLine($"Event type: {logEvent.EventType}"); 175 | 176 | switch (logEvent) 177 | { 178 | case WriteRowsEvent writeEvent: 179 | _outputHelper.WriteLine($"INSERT event on table ID: {writeEvent.TableID}"); 180 | var insertRows = writeEvent.RowSet.ToReadableRows(); 181 | if (insertRows.Count > 0) 182 | { 183 | var petName = insertRows[0]["name"]?.ToString(); 184 | if (petName == "Buddy") 185 | { 186 | sawInsert = true; 187 | _outputHelper.WriteLine($"Found INSERT for pet 'Buddy'"); 188 | } 189 | } 190 | break; 191 | 192 | case UpdateRowsEvent updateEvent: 193 | _outputHelper.WriteLine($"UPDATE event on table ID: {updateEvent.TableID}"); 194 | var updateRows = updateEvent.RowSet.ToReadableRows(); 195 | if (updateRows.Count > 0) 196 | { 197 | var cellValue = updateRows[0]["owner"] as CellValue; 198 | if (cellValue?.NewValue?.ToString() == "Sarah") 199 | { 200 | sawUpdate = true; 201 | _outputHelper.WriteLine($"Found UPDATE with new owner 'Sarah'"); 202 | } 203 | } 204 | break; 205 | 206 | case DeleteRowsEvent deleteEvent: 207 | _outputHelper.WriteLine($"DELETE event on table ID: {deleteEvent.TableID}"); 208 | var deleteRows = deleteEvent.RowSet.ToReadableRows(); 209 | if (deleteRows.Count > 0) 210 | { 211 | // For DELETE events, check if this might be our deleted pet 212 | sawDelete = true; 213 | _outputHelper.WriteLine($"Found DELETE event"); 214 | } 215 | break; 216 | 217 | case QueryEvent queryEvent: 218 | _outputHelper.WriteLine($"SQL Query: {queryEvent.Query}"); 219 | break; 220 | } 221 | 222 | // Exit the loop once we've seen all three events or processed 10 events 223 | if ((sawInsert && sawUpdate && sawDelete) || eventCount >= 20) 224 | { 225 | break; 226 | } 227 | } 228 | 229 | // Assert that we saw all the events we expected 230 | Assert.True(sawInsert, "Should have seen INSERT event"); 231 | Assert.True(sawUpdate, "Should have seen UPDATE event"); 232 | Assert.True(sawDelete, "Should have seen DELETE event"); 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /tests/Test/MySQLFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using MySql.Data.MySqlClient; 5 | using SciSharp.MySQL.Replication; 6 | using SciSharp.MySQL.Replication.Events; 7 | 8 | namespace Test 9 | { 10 | public class MySQLFixture : IDisposable 11 | { 12 | internal const string Host = "localhost"; 13 | internal const string Username = "root"; 14 | internal const string Password = "root"; 15 | 16 | private readonly int _serverId; 17 | 18 | public IReplicationClient Client { get; private set; } 19 | 20 | private SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); 21 | 22 | internal static MySQLFixture CreateMySQLFixture(int serverId = 1) 23 | { 24 | return new MySQLFixture(serverId); 25 | } 26 | 27 | private MySQLFixture(int serverId) 28 | { 29 | this._serverId = serverId; 30 | Client = new ReplicationClient(); 31 | ConnectAsync().Wait(); 32 | } 33 | 34 | private async Task ConnectAsync() 35 | { 36 | await Client.ConnectAsync(Host, Username, Password, _serverId); 37 | } 38 | 39 | private MySqlConnection GetConnection() 40 | { 41 | var connection = new MySqlConnection($"Server={Host};Database=garden;Uid={Username};Pwd={Password};"); 42 | connection.OpenAsync().Wait(); 43 | return connection; 44 | } 45 | 46 | public MySqlCommand CreateCommand() 47 | { 48 | return GetConnection().CreateCommand(); 49 | } 50 | 51 | public async Task ReceiveAsync(CancellationToken cancellationToken = default) 52 | where TLogEvent : LogEvent 53 | { 54 | await _semaphore.WaitAsync(cancellationToken); 55 | 56 | try 57 | { 58 | while (!cancellationToken.IsCancellationRequested) 59 | { 60 | var logEvent = await Client.ReceiveAsync(); 61 | 62 | if (logEvent is TLogEvent requiredLogEvent) 63 | { 64 | return requiredLogEvent; 65 | } 66 | } 67 | } 68 | finally 69 | { 70 | _semaphore.Release(); 71 | } 72 | 73 | return default; 74 | } 75 | 76 | public void Dispose() 77 | { 78 | Client?.CloseAsync().AsTask().Wait(); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /tests/Test/Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | PreserveNewest 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/Test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | 5 | db: 6 | image: mysql:latest 7 | container_name: mysql-rep 8 | restart: always 9 | environment: 10 | MYSQL_ROOT_PASSWORD: root 11 | ports: 12 | - 3306:3306 13 | volumes: 14 | - ./dump.sql:/docker-entrypoint-initdb.d/dump.sql 15 | command: ["mysqld", "--log-bin=mysql-bin", "--server-id=1", "--default-authentication-plugin=mysql_native_password", "--binlog_row_metadata=full"] 16 | 17 | adminer: 18 | image: adminer 19 | restart: always 20 | ports: 21 | - 8080:8080 -------------------------------------------------------------------------------- /tests/Test/dump.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE garden; 2 | 3 | USE garden; 4 | 5 | -- Original pet table 6 | CREATE TABLE pet (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(20), owner VARCHAR(20), species VARCHAR(20), sex CHAR(1), birth DATE, death DATE, timeUpdated datetime); 7 | 8 | -- Numeric data types 9 | CREATE TABLE tinyint_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value TINYINT); 10 | CREATE TABLE smallint_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value SMALLINT); 11 | CREATE TABLE mediumint_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value MEDIUMINT); 12 | CREATE TABLE int_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value INT); 13 | CREATE TABLE bigint_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value BIGINT); 14 | CREATE TABLE decimal_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value DECIMAL(10,2)); 15 | CREATE TABLE float_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value FLOAT); 16 | CREATE TABLE double_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value DOUBLE); 17 | CREATE TABLE bit_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value BIT(8)); 18 | 19 | -- Date and Time data types 20 | CREATE TABLE date_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value DATE); 21 | CREATE TABLE time_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value TIME); 22 | CREATE TABLE datetime_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value DATETIME); 23 | CREATE TABLE timestamp_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value TIMESTAMP); 24 | CREATE TABLE year_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value YEAR); 25 | 26 | -- String data types 27 | CREATE TABLE char_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value CHAR(50)); 28 | CREATE TABLE varchar_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value VARCHAR(255)); 29 | CREATE TABLE binary_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value BINARY(50)); 30 | CREATE TABLE varbinary_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value VARBINARY(255)); 31 | CREATE TABLE tinytext_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value TINYTEXT); 32 | CREATE TABLE text_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value TEXT); 33 | CREATE TABLE mediumtext_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value MEDIUMTEXT); 34 | CREATE TABLE longtext_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value LONGTEXT); 35 | CREATE TABLE tinyblob_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value TINYBLOB); 36 | CREATE TABLE blob_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value BLOB); 37 | CREATE TABLE mediumblob_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value MEDIUMBLOB); 38 | CREATE TABLE longblob_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value LONGBLOB); 39 | CREATE TABLE enum_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value ENUM('SMALL', 'MEDIUM', 'LARGE')); 40 | CREATE TABLE set_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value SET('RED', 'BLUE', 'GREEN')); 41 | 42 | /* 43 | -- JSON data type (MySQL 5.7.8 and later) 44 | CREATE TABLE json_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value JSON); 45 | 46 | -- Spatial data types 47 | CREATE TABLE geometry_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value GEOMETRY); 48 | CREATE TABLE point_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value POINT); 49 | CREATE TABLE linestring_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value LINESTRING); 50 | CREATE TABLE polygon_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value POLYGON); 51 | CREATE TABLE multipoint_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value MULTIPOINT); 52 | CREATE TABLE multilinestring_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value MULTILINESTRING); 53 | CREATE TABLE multipolygon_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value MULTIPOLYGON); 54 | CREATE TABLE geometrycollection_table (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, value GEOMETRYCOLLECTION); 55 | */ 56 | -------------------------------------------------------------------------------- /tests/Test/mysql.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | default-authentication-plugin=mysql_native_password 3 | server-id=1 4 | log_bin_basename=mysql-bin 5 | binlog_row_metadata=full -------------------------------------------------------------------------------- /tests/Test/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://xunit.github.io/schema/current/xunit.runner.schema.json", 3 | "parallelizeAssembly": false, 4 | "parallelizeTestCollections": false 5 | } -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "1.0.0-beta.4", 4 | "publicReleaseRefSpec": [ 5 | "^refs/heads/master$", 6 | "^refs/heads/v\\d+(?:\\.\\d+)?$" 7 | ], 8 | "cloudBuild": { 9 | "buildNumber": { 10 | "enabled": true, 11 | "setVersionVariables": true 12 | } 13 | }, 14 | "nugetPackageVersion": { 15 | "semVer": 2 16 | } 17 | } --------------------------------------------------------------------------------