├── .github └── workflows │ ├── official.yml │ └── pr.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SyncKusto.Tests ├── DictionaryExtensionTests.cs ├── Properties │ └── AssemblyInfo.cs └── SyncKusto.Tests.csproj ├── SyncKusto.sln ├── SyncKusto ├── App.config ├── ChangeModel │ ├── DictionaryDifferenceMapper.cs │ ├── Difference.cs │ ├── KustoSchemaDifferenceMapper.cs │ ├── KustoSchemaExtensions.cs │ ├── LineEndingMode.cs │ └── Schema │ │ ├── FunctionSchemaDifference.cs │ │ ├── IDatabaseSchema.cs │ │ ├── InvalidDatabaseSchema.cs │ │ ├── SchemaDifference.cs │ │ ├── TableSchemaDifference.cs │ │ └── ValidDatabaseSchema.cs ├── DropWarningForm.Designer.cs ├── DropWarningForm.cs ├── DropWarningForm.resx ├── ExtensionMethods.cs ├── Extensions │ └── DictionaryExtensions.cs ├── Functional │ ├── Either.cs │ ├── EitherAdapters.cs │ ├── EnumerableExtensions.cs │ ├── None.cs │ ├── Option.cs │ ├── OptionAdapters.cs │ ├── Reiterable.cs │ ├── ReiterableExtensions.cs │ └── Some.cs ├── Kusto │ ├── AuthenticationMode.cs │ ├── CreateOrAlterException.cs │ ├── DatabaseSchemaBuilder │ │ ├── BaseDatabaseSchemaBuilder.cs │ │ ├── EmptyDatabaseSchemaBuilder.cs │ │ ├── FileDatabaseSchemaBuilder.cs │ │ ├── IDatabaseSchemaBuilder.cs │ │ └── KustoDatabaseSchemaBuilder.cs │ ├── FormattedCslCommandGenerator.cs │ ├── Model │ │ ├── IKustoObject.cs │ │ ├── IKustoSchema.cs │ │ ├── KustoFunctionSchema.cs │ │ └── KustoTableSchema.cs │ └── QueryEngine.cs ├── MainForm.Designer.cs ├── MainForm.cs ├── MainForm.resx ├── Models │ ├── INonEmptyStringState.cs │ ├── NonEmptyString.cs │ └── UninitializedString.cs ├── Program.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings ├── SchemaPickerControl.Designer.cs ├── SchemaPickerControl.cs ├── SchemaPickerControl.resx ├── SettingsForm.Designer.cs ├── SettingsForm.cs ├── SettingsForm.resx ├── SettingsWrapper.cs ├── SyncKusto.csproj ├── SyncSources │ ├── DestinationSelections.cs │ ├── ISourceSelectionFactory.cs │ ├── SourceMode.cs │ ├── SourceSelection.cs │ └── SourceSelections.cs ├── Utilities │ └── CertificateStore.cs ├── Validation │ ├── ErrorMessages │ │ ├── DatabaseSchemaOperationError.cs │ │ ├── DefaultOperationErrorMessageResolver.cs │ │ ├── IOperationError.cs │ │ ├── IOperationErrorMessageSpecification.cs │ │ ├── NonSpecificOperationError.cs │ │ ├── OperationErrorMessageSpecification.cs │ │ └── Specifications │ │ │ ├── DefaultOperationErrorSpecification.cs │ │ │ ├── FilePathOperationErrorSpecifications.cs │ │ │ └── KustoOperationErrorSpecifications.cs │ └── Infrastructure │ │ ├── Composite.cs │ │ ├── NotNull.cs │ │ ├── NotNullOrEmptyString.cs │ │ ├── Predicate.cs │ │ ├── Property.cs │ │ ├── Spec.cs │ │ ├── Specification.cs │ │ └── Transform.cs └── icons │ ├── LibrarySetting_16x.png │ ├── SchemaCompare.ico │ ├── SyncArrow_16x.png │ └── UploadFile_16x.png └── screenshot.png /.github/workflows/official.yml: -------------------------------------------------------------------------------- 1 | name: official 2 | # Full build along with artifact publishing 3 | 4 | on: 5 | push: 6 | # Build on any new push to the master branch 7 | branches: [ master ] 8 | schedule: 9 | # * is a special character in YAML so you have to quote this string 10 | # Create one build every month even if there are no new checkins to work around expiring artifacts from older builds 11 | - cron: '0 0 1 * *' 12 | workflow_dispatch: 13 | # Allows you to run this workflow manually from the Actions tab 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: windows-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - name: Setup MSBuild 24 | uses: microsoft/setup-msbuild@v1 25 | 26 | - name: Setup NuGet 27 | uses: NuGet/setup-nuget@v1.0.5 28 | 29 | - name: Restore Packages 30 | run: nuget restore SyncKusto.sln 31 | 32 | - name: Build Solution 33 | run: msbuild.exe SyncKusto.sln /p:Configuration=Release 34 | 35 | - name: Upload Artifact 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: published 39 | path: SyncKusto/bin/Release 40 | 41 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: pullrequest 2 | # Do the same build as the official build but don't publish artifacts. 3 | 4 | on: 5 | # Trigger on pull requests or can be run manually on branches too 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: {} 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: windows-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Setup MSBuild 19 | uses: microsoft/setup-msbuild@v1 20 | 21 | - name: Setup NuGet 22 | uses: NuGet/setup-nuget@v1.0.5 23 | 24 | - name: Restore Packages 25 | run: nuget restore SyncKusto.sln 26 | 27 | - name: Build Solution 28 | run: msbuild.exe SyncKusto.sln /p:Configuration=Release 29 | 30 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. 4 | 5 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repositories using our CLA. 6 | 7 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 8 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 10 | 11 | ## Building the project 12 | Dependencies: 13 | - [Visual Studio 2017](https://visualstudio.microsoft.com/vs/) 14 | - .NET 4.7.2 - Visual Studio will suggest the download if you need it. 15 | - C# 7.3 - If you get an error saying you don't have this, upgrade to the latest version of VS2017 16 | 17 | Local development: 18 | 1. Clone the Sync Kusto repo to your local machine 19 | 2. Open the project in Visual Studio 2017 20 | 3. Build and run the project 21 | 22 | ## Maintainers 23 | Sync Kusto is lovingly maintained by: 24 | - **@benmartens** 25 | - **@nicksw1** 26 | - **@srivas15** 27 | 28 | ## Feature Ideas 29 | If you're interested in helping but don't have specific ideas for improvements, here are a couple that would make a significant difference in the user experience. 30 | ### Temporary Databases 31 | The Settings dialog asks for a temporary cluster and database to use during the comparison. Pushing all the local CSL files to a database lets us use ".show database x schema as json" to easily pull the entire schema into a normalized data structure, but it it would be much nicer if the user didn't have to specify this. It would also be a significant perf improvement since it takes a while to clean out the database each time. Some ideas are: 32 | 1) Using the management api to automatically create and destroy databases. 33 | 2) Build a DatabaseSchema object straight from the files without going through the Kusto cluster 34 | 3) Host a Kusto cluster locally in memory 35 | ### Swap Source and Target 36 | Users frequently set up a comparison between a source and a target but then they want to reverse the comparison. Instead of making them type everything in, there could be a button to swap the settings. 37 | 38 | ## Submitting Pull Requests 39 | 40 | - **DO** submit issues for features. This facilitates discussion of a feature separately from its implementation, and increases the acceptance rates for pull requests. 41 | - **DO NOT** submit large code formatting changes without discussing with the team first. 42 | 43 | These two blogs posts on contributing code to open source projects are good too: [Open Source Contribution Etiquette](http://tirania.org/blog/archive/2010/Dec-31.html) by Miguel de Icaza 44 | and [Don’t “Push” Your Pull Requests](https://www.igvita.com/2011/12/19/dont-push-your-pull-requests/) by Ilya Grigorik. 45 | 46 | ## Creating Issues 47 | 48 | - **DO** use a descriptive title that identifies the issue to be addressed or the requested feature. For example, when describing an issue where the comparison is not behaving as expected, 49 | write your bug title in terms of what the comparison should do rather than what it is doing – “Comparison should parse syntax XYZ in functions” 50 | - **DO** specify a detailed description of the issue or requested feature. 51 | - **DO** provide the following for bug reports 52 | - Describe the expected behavior and the actual behavior. If it is not self-evident such as in the case of a crash, provide an explanation for why the expected behavior is expected. 53 | - Specify any relevant exception messages and stack traces. 54 | - **DO** subscribe to notifications for the created issue in case there are any follow up questions. 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sync Kusto 2 | 3 | The Sync Kusto tool was built to help create a maintainable development process around [Kusto](https://docs.microsoft.com/en-us/azure/data-explorer/). The tool makes it easy to sync database schemas using either the local file system or a Kusto cluster as either the source of the target. It will copy full function definitions along with table schemas. Table data is not synced. 4 | 5 | ![Screenshot](/screenshot.png) 6 | 7 | ## Scope 8 | This tool is intended specifically for the developer desktop scenario. It will not help if you need to build an automated syncing process, generate change scripts, sync policies, etc. For all of that, we recommend using [Delta Kusto](https://github.com/microsoft/delta-kusto). 9 | 10 | ## Getting Started 11 | Assuming that the user already has a Kusto database with functions and tables, set the Kusto database as the source and the local file system as the target. Press the compare button and the tool will find that none of the database objects are in the target. Press the update button to download everything from Kusto to the local file system. If it's the first time this tool has been run, the settings dialog will appear. Read through the settings below to get those configured correctly. 12 | 13 | ## Recommended Team Process 14 | The general usage pattern on our team is to make changes in a Kusto dev database which starts with a schema that mirrors the production database. Once the developer is satisfied with the changes, they use Sync Kusto with the development database as the source and their local file system as the target. The changes are committed to source control and a pull request is sent out for review. After the request is approved and the code merges to master, the developer syncs the master branch and uses Sync Kusto to sync from their local file system to the production Kusto database. This is a more flexible/manual version of the [Azure Data Explorer task for Azure Dev Ops](https://docs.microsoft.com/en-us/azure/data-explorer/devops). We've had great success with this process and use the tool daily to help with our development and for deploying to production assets. 15 | 16 | ## Sync Kusto Binaries 17 | The release can be downloaded from the [GitHub Releases](https://github.com/microsoft/synckusto/releases) page. Individual builds can also be found in the [GitHub Actions](https://github.com/microsoft/synckusto/actions/workflows/official.yml) page, or the repo can be cloned and built in Visual Studio. 18 | 19 | ## Settings 20 | ### Temporary Databases 21 | When the local file system is selected as either the source or the target, Sync Kusto creates a temporary database containing all of the CSL files on the local file system. Specify a Kusto cluster and database where you have permissions to not only create new functions and tables but also to delete all the functions and tables that exist there already. This database will be completely wiped every time a comparison is run! If two users run a comparison pointing to the same temporary database at the same time, they'll get incorrect results. Ideally every user has their own temporary database. Note that you cannot specify a temporary database unless the database is empty. This is a safety check to avoid accidentally specifying a database that isn't intended to be wiped. 22 | 23 | ### AAD Authority 24 | If your username is in the form of UPN (user@contoso.com), you can hit the common AAD endpoint and your home tenant will be resolved automatically. If your tenant's automatic resolution does not work, you need to specify the AAD authority in the settings. Note that if you're planning to connect via an AAD application id and key then this setting is required regardless of tenant configuration. 25 | 26 | ### Warnings 27 | - Ask before dropping objects in the target Kusto database - This optional check is enabled by default and will prompt the user before dropping anything in the target database. 28 | 29 | ### Formatting 30 | - Place table fields on new lines - When CSL table files are written to a file target, each field is placed on a new line. This can make it easier to diff changes in a pull request. 31 | - Generated ".create-merge table" commands instead of ".create table" - When CSL table files are written to a file target, the command will be ".create-merge table" if this is checked. If it is unchecked, the command will be ".create table". 32 | 33 | ### Files 34 | - Use legacy `.csl` file extensions - Kusto originally used files with `.csl` file extensions. This extension has since been deprecated and replaced with the `.kql` file extension. This setting tells synckusto which file extension should be used for reading _and_ writing all files. The default state is checked, which means synckusto will consume and emit files with the `.csl` file extension. 35 | 36 | ## Contributing 37 | Issues, additional features, and tests are all welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for more information. 38 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SyncKusto.Tests/DictionaryExtensionTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Bogus; 5 | using NUnit.Framework; 6 | using SyncKusto.Extensions; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | 10 | namespace SyncKusto.Tests 11 | { 12 | public class DictionaryExtensionTests 13 | { 14 | private static Faker Fake => new Faker(); 15 | 16 | private static string RandomWord => Fake.Lorem.Word(); 17 | 18 | public static string CommonWord { get; } = Fake.Commerce.Color(); 19 | 20 | public static Dictionary CommonKeyCommonValue() => 21 | new Dictionary() {{CommonWord, CommonWord}}; 22 | 23 | public static Dictionary CommonKeyRandomValue() => 24 | new Dictionary() {{CommonWord, RandomWord}}; 25 | 26 | public static Dictionary RandomKeyRandomValue() => 27 | new Dictionary() {{RandomWord, RandomWord}}; 28 | 29 | public static Dictionary CommonKeyCommonValueWithRandomKeyRandomValue() => 30 | CommonKeyCommonValue().Concat(RandomKeyRandomValue()).ToDictionary(x => x.Key, x => x.Value); 31 | 32 | public static Dictionary EmptyDictionary() => new Dictionary() { }; 33 | 34 | private static IEnumerable NoDifferencesTestData() => 35 | new[] 36 | { 37 | new object[] {(source: CommonKeyCommonValue(), 38 | target: CommonKeyCommonValue())}, 39 | 40 | new object[] {(source: EmptyDictionary(), 41 | target: EmptyDictionary())} 42 | }; 43 | 44 | private static IEnumerable DifferencesTestData() => 45 | new[] 46 | { 47 | new object[] {(source: CommonKeyCommonValue(), 48 | target: CommonKeyRandomValue())}, 49 | 50 | new object[] {(source: CommonKeyCommonValue(), 51 | target: EmptyDictionary())}, 52 | 53 | new object[] {(source: EmptyDictionary(), 54 | target: CommonKeyCommonValue())}, 55 | 56 | new object[] {(source: CommonKeyRandomValue().Concat(RandomKeyRandomValue()).ToDictionary(x => x.Key, x => x.Value), 57 | target: RandomKeyRandomValue().Concat(RandomKeyRandomValue()).ToDictionary(x => x.Key, x => x.Value))} 58 | }; 59 | 60 | [Test] 61 | public void Difference_Finds_Only_In_Target_Differences() 62 | { 63 | var source = CommonKeyCommonValue(); 64 | var target = CommonKeyCommonValueWithRandomKeyRandomValue(); 65 | 66 | var results = source.DifferenceFrom(target); 67 | 68 | Assert.Multiple(() => 69 | { 70 | Assert.That(Differences(results)); 71 | Assert.That(results.onlyInTarget, Has.Exactly(1).Items); 72 | Assert.That(results.onlyInTarget.ContainsKey(results.onlyInTarget.First().Key)); 73 | }); 74 | } 75 | 76 | [Test] 77 | public void Difference_Finds_Only_In_Source_Differences() 78 | { 79 | var source = CommonKeyCommonValueWithRandomKeyRandomValue(); 80 | var target = CommonKeyCommonValue(); 81 | 82 | var results = source.DifferenceFrom(target); 83 | 84 | Assert.Multiple(() => 85 | { 86 | Assert.That(Differences(results)); 87 | Assert.That(results.onlyInSource, Has.Exactly(1).Items); 88 | Assert.That(results.onlyInSource.ContainsKey(results.onlyInSource.First().Key)); 89 | }); 90 | } 91 | 92 | [Test] 93 | public void Difference_Finds_And_Keeps_Source_Content_When_Modified_Found() 94 | { 95 | var source = new Dictionary() { { CommonWord, CommonWord } }; 96 | var target = new Dictionary() { { CommonWord, RandomWord } }; 97 | 98 | var results = source.DifferenceFrom(target); 99 | 100 | Assert.Multiple(() => 101 | { 102 | Assert.That(Differences(results)); 103 | Assert.That(results.modified, Has.Exactly(1).Items); 104 | Assert.That(results.modified, Is.EqualTo(source)); 105 | }); 106 | } 107 | 108 | [Test] 109 | [TestCaseSource(nameof(NoDifferencesTestData))] 110 | public void Difference_Finds_No_Differences( 111 | (Dictionary source, Dictionary target) data) => 112 | Assert.That(NoDifferences(data.source.DifferenceFrom(data.target))); 113 | 114 | [Test] 115 | [TestCaseSource(nameof(DifferencesTestData))] 116 | public void Difference_Finds_Differences( 117 | (Dictionary source, Dictionary target) data) => 118 | Assert.That(Differences(data.source.DifferenceFrom(data.target))); 119 | 120 | public bool Differences( 121 | (IDictionary modified, IDictionary onlyInSource, IDictionary 122 | onlyInTarget) differences) => 123 | differences.modified.Any() || differences.onlyInSource.Any() || differences.onlyInTarget.Any(); 124 | 125 | public bool NoDifferences( 126 | (IDictionary modified, IDictionary onlyInSource, IDictionary 127 | onlyInTarget) differences) => 128 | !differences.modified.Any() && !differences.onlyInSource.Any() && !differences.onlyInTarget.Any(); 129 | } 130 | } -------------------------------------------------------------------------------- /SyncKusto.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: AssemblyTitle("SyncKusto.Tests")] 6 | [assembly: AssemblyDescription("")] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("")] 9 | [assembly: AssemblyProduct("SyncKusto.Tests")] 10 | [assembly: AssemblyCopyright("Copyright © 2019")] 11 | [assembly: AssemblyTrademark("")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | [assembly: Guid("52462172-b8ca-45ce-a2c6-a189cc786f64")] 17 | 18 | // [assembly: AssemblyVersion("1.0.*")] 19 | [assembly: AssemblyVersion("1.0.0.0")] 20 | [assembly: AssemblyFileVersion("1.0.0.0")] 21 | -------------------------------------------------------------------------------- /SyncKusto.Tests/SyncKusto.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {52462172-B8CA-45CE-A2C6-A189CC786F64} 8 | Library 9 | Properties 10 | SyncKusto.Tests 11 | SyncKusto.Tests 12 | v4.7.2 13 | 512 14 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 15 | 15.0 16 | False 17 | UnitTest 18 | 19 | 20 | true 21 | full 22 | false 23 | bin\Debug\ 24 | DEBUG;TRACE 25 | prompt 26 | 4 27 | 28 | 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | 36 | 37 | bin\x64\ 38 | TRACE 39 | true 40 | pdbonly 41 | AnyCPU 42 | prompt 43 | MinimumRecommendedRules.ruleset 44 | 45 | 46 | true 47 | bin\x64\Debug\ 48 | DEBUG;TRACE 49 | full 50 | x64 51 | prompt 52 | MinimumRecommendedRules.ruleset 53 | 54 | 55 | bin\x64\Release\ 56 | TRACE 57 | true 58 | pdbonly 59 | x64 60 | prompt 61 | MinimumRecommendedRules.ruleset 62 | 63 | 64 | bin\x64\x64\ 65 | TRACE 66 | true 67 | pdbonly 68 | x64 69 | prompt 70 | MinimumRecommendedRules.ruleset 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 28.0.1 83 | 84 | 85 | 3.11.0 86 | 87 | 88 | 3.13.0 89 | 90 | 91 | 4.5.0 92 | 93 | 94 | 95 | 96 | {b8402e4f-09f8-4684-8d70-4def827e264b} 97 | SyncKusto 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /SyncKusto.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.645 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SyncKusto", "SyncKusto\SyncKusto.csproj", "{B8402E4F-09F8-4684-8D70-4DEF827E264B}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SyncKusto.Tests", "SyncKusto.Tests\SyncKusto.Tests.csproj", "{52462172-B8CA-45CE-A2C6-A189CC786F64}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|x64 = Debug|x64 14 | Release|Any CPU = Release|Any CPU 15 | Release|x64 = Release|x64 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {B8402E4F-09F8-4684-8D70-4DEF827E264B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {B8402E4F-09F8-4684-8D70-4DEF827E264B}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {B8402E4F-09F8-4684-8D70-4DEF827E264B}.Debug|x64.ActiveCfg = Debug|x64 21 | {B8402E4F-09F8-4684-8D70-4DEF827E264B}.Debug|x64.Build.0 = Debug|x64 22 | {B8402E4F-09F8-4684-8D70-4DEF827E264B}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {B8402E4F-09F8-4684-8D70-4DEF827E264B}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {B8402E4F-09F8-4684-8D70-4DEF827E264B}.Release|x64.ActiveCfg = Release|Any CPU 25 | {B8402E4F-09F8-4684-8D70-4DEF827E264B}.Release|x64.Build.0 = Release|Any CPU 26 | {52462172-B8CA-45CE-A2C6-A189CC786F64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {52462172-B8CA-45CE-A2C6-A189CC786F64}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {52462172-B8CA-45CE-A2C6-A189CC786F64}.Debug|x64.ActiveCfg = Debug|x64 29 | {52462172-B8CA-45CE-A2C6-A189CC786F64}.Debug|x64.Build.0 = Debug|x64 30 | {52462172-B8CA-45CE-A2C6-A189CC786F64}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {52462172-B8CA-45CE-A2C6-A189CC786F64}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {52462172-B8CA-45CE-A2C6-A189CC786F64}.Release|x64.ActiveCfg = Release|Any CPU 33 | {52462172-B8CA-45CE-A2C6-A189CC786F64}.Release|x64.Build.0 = Release|Any CPU 34 | EndGlobalSection 35 | GlobalSection(SolutionProperties) = preSolution 36 | HideSolutionNode = FALSE 37 | EndGlobalSection 38 | GlobalSection(ExtensibilityGlobals) = postSolution 39 | SolutionGuid = {676A25DB-E4BC-4F7E-8F23-C3B9FCCD47BB} 40 | EndGlobalSection 41 | EndGlobal 42 | -------------------------------------------------------------------------------- /SyncKusto/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | True 27 | 28 | 29 | False 30 | 31 | 32 | False 33 | 34 | 35 | True 36 | 37 | 38 | CurrentUser 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 0 51 | 52 | 53 | False 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /SyncKusto/ChangeModel/DictionaryDifferenceMapper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace SyncKusto.ChangeModel 8 | { 9 | public abstract class DictionaryDifferenceMapper 10 | { 11 | private protected DictionaryDifferenceMapper(Func<(IDictionary modified, IDictionary onlyInSource, 12 | IDictionary onlyInTarget)> difference) => 13 | DifferenceFactory = difference; 14 | 15 | private protected Func<(IDictionary modified, IDictionary onlyInSource, 16 | IDictionary onlyInTarget)> DifferenceFactory { get; } 17 | 18 | private protected Dictionary> MapDifferences() 19 | { 20 | var results = new Dictionary>(); 21 | 22 | (IDictionary modified, IDictionary onlyInSource, IDictionary onlyInTarget) = DifferenceFactory(); 23 | 24 | results.Add(Difference.Modified(), modified); 25 | results.Add(Difference.OnlyInSource(), onlyInSource); 26 | results.Add(Difference.OnlyInTarget(), onlyInTarget); 27 | 28 | return results; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /SyncKusto/ChangeModel/Difference.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace SyncKusto.ChangeModel 5 | { 6 | public class Difference 7 | { 8 | private protected Difference(){} 9 | 10 | public static Difference OnlyInTarget() => new OnlyInTarget(); 11 | public static Difference OnlyInSource() => new OnlyInSource(); 12 | public static Difference Modified() => new Modified(); 13 | } 14 | 15 | public class OnlyInTarget : Difference { } 16 | public class OnlyInSource : Difference { } 17 | public class Modified : Difference { } 18 | } -------------------------------------------------------------------------------- /SyncKusto/ChangeModel/KustoSchemaDifferenceMapper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace SyncKusto.ChangeModel 9 | { 10 | public class KustoSchemaDifferenceMapper : DictionaryDifferenceMapper 11 | { 12 | public KustoSchemaDifferenceMapper( 13 | Func<(IDictionary modified, IDictionary onlyInSource, 14 | IDictionary onlyInTarget)> difference) : base(difference) 15 | { 16 | } 17 | 18 | public IEnumerable GetDifferences() => MapDifferences() 19 | .Aggregate(new List(), (agg, item) => 20 | { 21 | agg.AddRange(item.Value.Select(x => x.Value.AsSchemaDifference(item.Key))); 22 | return agg; 23 | }); 24 | } 25 | } -------------------------------------------------------------------------------- /SyncKusto/ChangeModel/KustoSchemaExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using Kusto.Data.Common; 8 | 9 | namespace SyncKusto.ChangeModel 10 | { 11 | public static class KustoSchemaExtensions 12 | { 13 | public static IKustoSchema AsKustoSchema(this TableSchema schema) => new KustoTableSchema(schema); 14 | public static IKustoSchema AsKustoSchema(this FunctionSchema schema) => new KustoFunctionSchema(schema); 15 | 16 | public static Dictionary AsKustoSchema(this Dictionary schemas) => 17 | schemas.ToDictionary(x => x.Key, x => x.Value.AsKustoSchema()); 18 | 19 | public static Dictionary AsKustoSchema(this Dictionary schemas) => 20 | schemas.ToDictionary(x => x.Key, x => x.Value.AsKustoSchema()); 21 | 22 | public static SchemaDifference AsSchemaDifference(this IKustoSchema schema, Difference difference) 23 | { 24 | switch (schema) 25 | { 26 | case KustoTableSchema _: 27 | return new TableSchemaDifference(difference, schema); 28 | case KustoFunctionSchema _: 29 | return new FunctionSchemaDifference(difference, schema); 30 | default: 31 | throw new InvalidOperationException("Unknown type supplied."); 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /SyncKusto/ChangeModel/LineEndingMode.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace SyncKusto.ChangeModel 5 | { 6 | /// 7 | /// Line endings can be normalized based on this value. Windows style is \r\n and Unix style is \n. 8 | /// 9 | public enum LineEndingMode 10 | { 11 | LeaveAsIs = 0, 12 | WindowsStyle = 1, 13 | UnixStyle = 2 14 | }; 15 | } -------------------------------------------------------------------------------- /SyncKusto/ChangeModel/Schema/FunctionSchemaDifference.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace SyncKusto.ChangeModel 5 | { 6 | public class FunctionSchemaDifference : SchemaDifference 7 | { 8 | public FunctionSchemaDifference(Difference difference, IKustoSchema value) : base(difference) => Value = value; 9 | 10 | private IKustoSchema Value { get; } 11 | 12 | public override IKustoSchema Schema => Value; 13 | 14 | public override string Name => Value.Name; 15 | } 16 | } -------------------------------------------------------------------------------- /SyncKusto/ChangeModel/Schema/IDatabaseSchema.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Kusto.Data.Common; 5 | 6 | namespace SyncKusto 7 | { 8 | public interface IDatabaseSchema 9 | { 10 | DatabaseSchema GetSchema(); 11 | } 12 | } -------------------------------------------------------------------------------- /SyncKusto/ChangeModel/Schema/InvalidDatabaseSchema.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using Kusto.Data.Common; 6 | using SyncKusto.Validation.ErrorMessages; 7 | 8 | namespace SyncKusto 9 | { 10 | public class InvalidDatabaseSchema : IDatabaseSchema 11 | { 12 | public InvalidDatabaseSchema(IOperationError error) 13 | { 14 | Error = error; 15 | } 16 | 17 | public IOperationError Error { get; } 18 | 19 | public DatabaseSchema GetSchema() => throw new InvalidOperationException("Invalid schema operation."); 20 | 21 | public Exception Exception => Error.Exception; 22 | } 23 | } -------------------------------------------------------------------------------- /SyncKusto/ChangeModel/Schema/SchemaDifference.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace SyncKusto.ChangeModel 5 | { 6 | public abstract class SchemaDifference 7 | { 8 | protected SchemaDifference(Difference difference) => Difference = difference; 9 | 10 | public Difference Difference { get; } 11 | 12 | public abstract IKustoSchema Schema { get; } 13 | 14 | public abstract string Name { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /SyncKusto/ChangeModel/Schema/TableSchemaDifference.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace SyncKusto.ChangeModel 5 | { 6 | public class TableSchemaDifference : SchemaDifference 7 | { 8 | public TableSchemaDifference(Difference difference, IKustoSchema value) : base(difference) => Value = value; 9 | 10 | private IKustoSchema Value { get; } 11 | 12 | public override IKustoSchema Schema => Value; 13 | 14 | public override string Name => Value.Name; 15 | } 16 | } -------------------------------------------------------------------------------- /SyncKusto/ChangeModel/Schema/ValidDatabaseSchema.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using Kusto.Data.Common; 6 | 7 | namespace SyncKusto 8 | { 9 | public class ValidDatabaseSchema : IDatabaseSchema 10 | { 11 | public static implicit operator DatabaseSchema(ValidDatabaseSchema schema) => schema.GetSchema(); 12 | 13 | public ValidDatabaseSchema(Func schema) 14 | { 15 | Schema = schema(); 16 | } 17 | 18 | public DatabaseSchema GetSchema() => Schema; 19 | 20 | public DatabaseSchema Schema { get; } 21 | } 22 | } -------------------------------------------------------------------------------- /SyncKusto/DropWarningForm.Designer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace SyncKusto 5 | { 6 | partial class DropWarningForm 7 | { 8 | /// 9 | /// Required designer variable. 10 | /// 11 | private System.ComponentModel.IContainer components = null; 12 | 13 | /// 14 | /// Clean up any resources being used. 15 | /// 16 | /// true if managed resources should be disposed; otherwise, false. 17 | protected override void Dispose(bool disposing) 18 | { 19 | if (disposing && (components != null)) 20 | { 21 | components.Dispose(); 22 | } 23 | base.Dispose(disposing); 24 | } 25 | 26 | #region Windows Form Designer generated code 27 | 28 | /// 29 | /// Required method for Designer support - do not modify 30 | /// the contents of this method with the code editor. 31 | /// 32 | private void InitializeComponent() 33 | { 34 | System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(DropWarningForm)); 35 | this.btnOk = new System.Windows.Forms.Button(); 36 | this.btnCancel = new System.Windows.Forms.Button(); 37 | this.label1 = new System.Windows.Forms.Label(); 38 | this.chkNextTime = new System.Windows.Forms.CheckBox(); 39 | this.SuspendLayout(); 40 | // 41 | // btnOk 42 | // 43 | this.btnOk.Location = new System.Drawing.Point(303, 125); 44 | this.btnOk.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); 45 | this.btnOk.Name = "btnOk"; 46 | this.btnOk.Size = new System.Drawing.Size(112, 35); 47 | this.btnOk.TabIndex = 3; 48 | this.btnOk.Text = "&Yes"; 49 | this.btnOk.UseVisualStyleBackColor = true; 50 | this.btnOk.Click += new System.EventHandler(this.btnOk_Click); 51 | // 52 | // btnCancel 53 | // 54 | this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; 55 | this.btnCancel.Location = new System.Drawing.Point(424, 125); 56 | this.btnCancel.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); 57 | this.btnCancel.Name = "btnCancel"; 58 | this.btnCancel.Size = new System.Drawing.Size(112, 35); 59 | this.btnCancel.TabIndex = 4; 60 | this.btnCancel.Text = "&No"; 61 | this.btnCancel.UseVisualStyleBackColor = true; 62 | this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click); 63 | // 64 | // label1 65 | // 66 | this.label1.AutoSize = true; 67 | this.label1.Location = new System.Drawing.Point(20, 23); 68 | this.label1.Name = "label1"; 69 | this.label1.Size = new System.Drawing.Size(454, 40); 70 | this.label1.TabIndex = 5; 71 | this.label1.Text = "You have chosen to drop objects in the target Kusto database. \r\nAre you sure you " + 72 | "want to continue?"; 73 | // 74 | // chkNextTime 75 | // 76 | this.chkNextTime.AutoSize = true; 77 | this.chkNextTime.Checked = true; 78 | this.chkNextTime.CheckState = System.Windows.Forms.CheckState.Checked; 79 | this.chkNextTime.Location = new System.Drawing.Point(24, 80); 80 | this.chkNextTime.Name = "chkNextTime"; 81 | this.chkNextTime.Size = new System.Drawing.Size(489, 24); 82 | this.chkNextTime.TabIndex = 6; 83 | this.chkNextTime.Text = "Continue to show these warnings before dropping Kusto objects."; 84 | this.chkNextTime.UseVisualStyleBackColor = true; 85 | // 86 | // DropWarningForm 87 | // 88 | this.AcceptButton = this.btnOk; 89 | this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F); 90 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 91 | this.CancelButton = this.btnCancel; 92 | this.ClientSize = new System.Drawing.Size(559, 179); 93 | this.Controls.Add(this.chkNextTime); 94 | this.Controls.Add(this.label1); 95 | this.Controls.Add(this.btnCancel); 96 | this.Controls.Add(this.btnOk); 97 | this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); 98 | this.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); 99 | this.Name = "DropWarningForm"; 100 | this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide; 101 | this.Text = "Drop Warning"; 102 | this.ResumeLayout(false); 103 | this.PerformLayout(); 104 | 105 | } 106 | 107 | #endregion 108 | private System.Windows.Forms.Button btnOk; 109 | private System.Windows.Forms.Button btnCancel; 110 | private System.Windows.Forms.Label label1; 111 | private System.Windows.Forms.CheckBox chkNextTime; 112 | } 113 | } -------------------------------------------------------------------------------- /SyncKusto/DropWarningForm.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Windows.Forms; 6 | 7 | namespace SyncKusto 8 | { 9 | /// 10 | /// Collect some settings from the user 11 | /// 12 | public partial class DropWarningForm : Form 13 | { 14 | /// 15 | /// Default constructor 16 | /// 17 | public DropWarningForm() 18 | { 19 | InitializeComponent(); 20 | } 21 | 22 | ///// 23 | ///// Close the form without saving anything 24 | ///// 25 | ///// 26 | ///// 27 | private void btnCancel_Click(object sender, EventArgs e) 28 | { 29 | this.DialogResult = DialogResult.No; 30 | this.Close(); 31 | } 32 | 33 | /// 34 | /// Test out the settings before saving them. 35 | /// 36 | /// 37 | /// 38 | private void btnOk_Click(object sender, EventArgs e) 39 | { 40 | SettingsWrapper.KustoObjectDropWarning = chkNextTime.Checked; 41 | this.DialogResult = DialogResult.Yes; 42 | this.Close(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SyncKusto/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.IO; 5 | using Kusto.Data.Common; 6 | using SyncKusto.Kusto; 7 | 8 | namespace SyncKusto 9 | { 10 | public static class ExtensionMethods 11 | { 12 | /// 13 | /// Write the function to the file system. 14 | /// 15 | /// The function to write 16 | /// The root folder for all the CSL files 17 | /// 18 | public static void WriteToFile(this FunctionSchema functionSchema, string rootFolder, string fileExtension) 19 | { 20 | string filename = Path.ChangeExtension(functionSchema.Name, fileExtension); 21 | 22 | // First remove any other files with this name. In the case where you moved an object to a new folder, this will handle cleaning up the old file 23 | string[] existingFiles = Directory.GetFiles(rootFolder, filename, SearchOption.AllDirectories); 24 | if (existingFiles.Length > 0) 25 | { 26 | foreach (string file in existingFiles) 27 | { 28 | try 29 | { 30 | File.Delete(file.HandleLongFileNames()); 31 | } 32 | catch 33 | { 34 | // It's not the end of the world if this call fails 35 | } 36 | } 37 | } 38 | 39 | // Now add write the new file to the correct location. 40 | string funcFolder = Path.Combine(rootFolder, "Functions"); 41 | if (!string.IsNullOrEmpty(functionSchema.Folder)) 42 | { 43 | string cleanedFolder = string.Join("", functionSchema.Folder.Split(Path.GetInvalidPathChars())); 44 | funcFolder = Path.Combine(funcFolder, cleanedFolder); 45 | } 46 | 47 | string destinationFile = Path.Combine(funcFolder, filename); 48 | if (!Directory.Exists(funcFolder)) 49 | { 50 | Directory.CreateDirectory(funcFolder); 51 | } 52 | 53 | File.WriteAllText(destinationFile.HandleLongFileNames(), CslCommandGenerator.GenerateCreateOrAlterFunctionCommand(functionSchema, true)); 54 | } 55 | 56 | /// 57 | /// Write a function to Kusto 58 | /// 59 | /// The function to write 60 | /// An initialized query engine for issuing the Kusto command 61 | public static void WriteToKusto(this FunctionSchema functionSchema, QueryEngine kustoQueryEngine) 62 | { 63 | kustoQueryEngine.CreateOrAlterFunctionAsync(CslCommandGenerator.GenerateCreateOrAlterFunctionCommand(functionSchema, true), functionSchema.Name).Wait(); 64 | } 65 | 66 | /// 67 | /// Delete a function from the file system 68 | /// 69 | /// The function to remove 70 | /// The root folder for all the CSL files 71 | public static void DeleteFromFolder(this FunctionSchema functionSchema, string rootFolder, string fileExtension) 72 | { 73 | string funcFolder = Path.Combine(rootFolder, "Functions"); 74 | if (!string.IsNullOrEmpty(functionSchema.Folder)) 75 | { 76 | funcFolder = Path.Combine(funcFolder, functionSchema.Folder); 77 | } 78 | string destinationFile = Path.ChangeExtension(Path.Combine(funcFolder, functionSchema.Name), fileExtension); 79 | File.Delete(destinationFile.HandleLongFileNames()); 80 | } 81 | 82 | /// 83 | /// Delete a function from Kusto 84 | /// 85 | /// The function to remove 86 | /// An initialized query engine for issuing the Kusto command 87 | public static void DeleteFromKusto(this FunctionSchema functionSchema, QueryEngine kustoQueryEngine) 88 | { 89 | kustoQueryEngine.DropFunction(functionSchema); 90 | } 91 | 92 | /// 93 | /// Write a table to the file system 94 | /// 95 | /// The table to write 96 | /// The root folder for all the CSL files 97 | public static void WriteToFile(this TableSchema tableSchema, string rootFolder, string fileExtension) 98 | { 99 | string tableFolder = rootFolder; 100 | if (!string.IsNullOrEmpty(tableSchema.Folder)) 101 | { 102 | string cleanedFolder = string.Join("", tableSchema.Folder.Split(Path.GetInvalidPathChars())); 103 | tableFolder = Path.Combine(rootFolder, "Tables", cleanedFolder); 104 | } 105 | string destinationFile = Path.ChangeExtension(Path.Combine(tableFolder, tableSchema.Name), fileExtension); 106 | if (!Directory.Exists(tableFolder)) 107 | { 108 | Directory.CreateDirectory(tableFolder); 109 | } 110 | 111 | File.WriteAllText(destinationFile.HandleLongFileNames(), FormattedCslCommandGenerator.GenerateTableCreateCommand(tableSchema, true)); 112 | } 113 | 114 | /// 115 | /// Write a table to Kusto 116 | /// 117 | /// The table to write 118 | /// An initialized query engine for issuing the Kusto command 119 | public static void WriteToKusto(this TableSchema tableSchema, QueryEngine kustoQueryEngine) 120 | { 121 | kustoQueryEngine.CreateOrAlterTableAsync(FormattedCslCommandGenerator.GenerateTableCreateCommand(tableSchema, false), tableSchema.Name).Wait(); 122 | } 123 | 124 | /// 125 | /// Delete a table from the file system 126 | /// 127 | /// The table to remove 128 | /// The root folder for all the CSL files 129 | public static void DeleteFromFolder(this TableSchema tableSchema, string rootFolder, string fileExtension) 130 | { 131 | string tableFolder = rootFolder; 132 | if (!string.IsNullOrEmpty(tableSchema.Folder)) 133 | { 134 | tableFolder = Path.Combine(rootFolder, "Tables", tableSchema.Folder); 135 | } 136 | string destinationFile = Path.ChangeExtension(Path.Combine(tableFolder, tableSchema.Name), fileExtension); 137 | File.Delete(destinationFile.HandleLongFileNames()); 138 | } 139 | 140 | /// 141 | /// Delete a table from Kusto 142 | /// 143 | /// The table to remove 144 | /// An initialized query engine for issuing the Kusto command 145 | public static void DeleteFromKusto(this TableSchema tableSchema, QueryEngine kustoQueryEngine) 146 | { 147 | kustoQueryEngine.DropTable(tableSchema.Name); 148 | } 149 | 150 | /// 151 | /// Convert to long path to avoid issues with long file names 152 | // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces 153 | /// 154 | /// 155 | /// 156 | public static string HandleLongFileNames(this string filename) 157 | { 158 | const string LongPathPrefix = "\\\\?\\"; 159 | 160 | if (filename.Length > 248) 161 | { 162 | // The path is getting close to the limit so prepend the longPathPrefix. 163 | return LongPathPrefix + filename; 164 | } 165 | else if (filename.StartsWith(LongPathPrefix)) 166 | { 167 | // The path has the long path prefix but doesn't need it. 168 | return filename.Substring(LongPathPrefix.Length); 169 | } 170 | else 171 | { 172 | // The path doesn't have the long path prefix and doesn't need it. 173 | return filename; 174 | } 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /SyncKusto/Extensions/DictionaryExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using Kusto.Cloud.Platform.Utils; 7 | 8 | namespace SyncKusto.Extensions 9 | { 10 | public static class DictionaryExtensions 11 | { 12 | public static (IDictionary modified, IDictionary onlyInSource, 13 | IDictionary onlyInTarget) DifferenceFrom( 14 | this IDictionary source, IDictionary target) 15 | { 16 | var modified = new Dictionary(); 17 | var onlyInTarget = new Dictionary(); 18 | var onlyInSource = new Dictionary(); 19 | 20 | target.Concat(source) 21 | // remove common items 22 | .Except(target.Intersect(source)) 23 | // only keep source for value mismatches 24 | .Except(target.Where(tgt => source.ContainsKey(tgt.Key) && !Equals(source[tgt.Key], tgt.Value))) 25 | .ForEach(item => 26 | 27 | { 28 | if (target.ContainsKey(item.Key) && source.ContainsKey(item.Key)) 29 | { 30 | modified.Add(item.Key, item.Value); 31 | } 32 | else if (source.ContainsKey(item.Key) && target.ContainsKey(item.Key) == false) 33 | { 34 | onlyInSource.Add(item.Key, item.Value); 35 | } 36 | else 37 | { 38 | onlyInTarget.Add(item.Key, item.Value); 39 | } 40 | } 41 | ); 42 | 43 | return (modified, onlyInSource, onlyInTarget); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SyncKusto/Functional/Either.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace SyncKusto.Functional 5 | { 6 | public abstract class Either 7 | { 8 | public static implicit operator Either(TLeft obj) => 9 | new Left(obj); 10 | 11 | public static implicit operator Either(TRight obj) => 12 | new Right(obj); 13 | } 14 | 15 | public class Left : Either 16 | { 17 | private TLeft Content { get; } 18 | 19 | public Left(TLeft content) 20 | { 21 | this.Content = content; 22 | } 23 | 24 | public static implicit operator TLeft(Left obj) => 25 | obj.Content; 26 | } 27 | 28 | public class Right : Either 29 | { 30 | private TRight Content { get; } 31 | 32 | public Right(TRight content) 33 | { 34 | this.Content = content; 35 | } 36 | 37 | public static implicit operator TRight(Right obj) => 38 | obj.Content; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SyncKusto/Functional/EitherAdapters.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | 6 | namespace SyncKusto.Functional 7 | { 8 | public static class EitherAdapters 9 | { 10 | public static Either Map( 11 | this Either either, Func map) => 12 | either is Right right 13 | ? (Either)map(right) 14 | : (TLeft)(Left)either; 15 | 16 | public static Either Map( 17 | this Either either, Func> map) => 18 | either is Right right 19 | ? map(right) 20 | : (TLeft)(Left)either; 21 | 22 | public static TRight Reduce( 23 | this Either either, Func map) => 24 | either is Left left 25 | ? map(left) 26 | : (Right)either; 27 | 28 | public static Either Reduce( 29 | this Either either, Func map, 30 | Func when) => 31 | either is Left bound && when(bound) 32 | ? (Either)map(bound) 33 | : either; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SyncKusto/Functional/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace SyncKusto.Functional 9 | { 10 | public static class EnumerableExtensions 11 | { 12 | public static IEnumerable SelectOptional( 13 | this IEnumerable sequence, Func> map) => 14 | sequence.Select(map) 15 | .OfType>() 16 | .Select(some => some.Content); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SyncKusto/Functional/None.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace SyncKusto.Functional 5 | { 6 | public class None : Option 7 | { 8 | } 9 | 10 | public class None 11 | { 12 | public static None Value { get; } = new None(); 13 | private None() { } 14 | } 15 | } -------------------------------------------------------------------------------- /SyncKusto/Functional/Option.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace SyncKusto.Functional 5 | { 6 | public abstract class Option 7 | { 8 | public static implicit operator Option(T value) => 9 | new Some(value); 10 | 11 | public static implicit operator Option(None none) => 12 | new None(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /SyncKusto/Functional/OptionAdapters.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | 6 | namespace SyncKusto.Functional 7 | { 8 | public static class OptionAdapters 9 | { 10 | public static Option Map(this Option option, Func map) => 11 | option is Some some ? (Option)map(some) : None.Value; 12 | 13 | public static Option When(this T value, Func predicate) => 14 | predicate(value) ? (Option)value : None.Value; 15 | 16 | public static T Reduce(this Option option, T whenNone) => 17 | option is Some some ? (T)some : whenNone; 18 | 19 | public static T Reduce(this Option option, Func whenNone) => 20 | option is Some some ? (T)some : whenNone(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SyncKusto/Functional/Reiterable.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace SyncKusto.Functional 9 | { 10 | public class Reiterable : IEnumerable 11 | { 12 | private IEnumerable Data { get; } 13 | 14 | private Reiterable(IEnumerable data) 15 | { 16 | this.Data = data; 17 | } 18 | 19 | public IEnumerator GetEnumerator() => 20 | this.Data.GetEnumerator(); 21 | 22 | IEnumerator IEnumerable.GetEnumerator() => 23 | this.GetEnumerator(); 24 | 25 | public static implicit operator Reiterable(List data) => 26 | new Reiterable(data); 27 | 28 | public static implicit operator Reiterable(T[] data) => 29 | new Reiterable(data); 30 | 31 | public static Reiterable From(IEnumerable data) 32 | { 33 | switch (data) 34 | { 35 | case List list: return list; 36 | case T[] array: return array; 37 | case Reiterable reiterable: return reiterable; 38 | default: return new Reiterable(data.ToList()); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SyncKusto/Functional/ReiterableExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace SyncKusto.Functional 7 | { 8 | public static class ReiterableExtensions 9 | { 10 | public static Reiterable AsReiterable(this IEnumerable data) => 11 | Reiterable.From(data); 12 | } 13 | } -------------------------------------------------------------------------------- /SyncKusto/Functional/Some.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace SyncKusto.Functional 5 | { 6 | public class Some : Option 7 | { 8 | public T Content { get; } 9 | 10 | public Some(T content) 11 | { 12 | this.Content = content; 13 | } 14 | 15 | public static implicit operator T(Some value) => 16 | value.Content; 17 | } 18 | } -------------------------------------------------------------------------------- /SyncKusto/Kusto/AuthenticationMode.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace SyncKusto.Kusto 5 | { 6 | /// 7 | /// When connecting to a Kusto cluster, this enum contains the multiple methods of authentication are supported. 8 | /// 9 | public enum AuthenticationMode 10 | { 11 | AadFederated, 12 | AadApplication, 13 | AadApplicationSni 14 | }; 15 | } -------------------------------------------------------------------------------- /SyncKusto/Kusto/CreateOrAlterException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | 6 | namespace SyncKusto.Kusto 7 | { 8 | /// 9 | /// Represents an exception when attempting a CreateOrAlter Kusto command 10 | /// 11 | public class CreateOrAlterException : Exception 12 | { 13 | public string FailedEntityName { get; } 14 | 15 | public CreateOrAlterException(string message, Exception inner, string failedEntityName) 16 | : base(message, inner) 17 | { 18 | FailedEntityName = failedEntityName; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SyncKusto/Kusto/DatabaseSchemaBuilder/BaseDatabaseSchemaBuilder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using Kusto.Data.Common; 8 | 9 | namespace SyncKusto.Kusto.DatabaseSchemaBuilder 10 | { 11 | public abstract class BaseDatabaseSchemaBuilder : IDatabaseSchemaBuilder 12 | { 13 | public abstract Task Build(); 14 | 15 | private protected static List WaitAllAndGetFailedObjects(List createOrAlterTasks) 16 | { 17 | var failedObjects = new List(); 18 | try 19 | { 20 | Task.WaitAll(createOrAlterTasks.ToArray()); 21 | } 22 | catch (AggregateException ex) 23 | { 24 | AggregateException flattendedException = ex.Flatten(); 25 | foreach (Exception exception in flattendedException.InnerExceptions) 26 | { 27 | failedObjects.Add(((CreateOrAlterException)exception).FailedEntityName); 28 | } 29 | } 30 | return failedObjects; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /SyncKusto/Kusto/DatabaseSchemaBuilder/EmptyDatabaseSchemaBuilder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Threading.Tasks; 5 | using Kusto.Data.Common; 6 | 7 | namespace SyncKusto.Kusto.DatabaseSchemaBuilder 8 | { 9 | public class EmptyDatabaseSchemaBuilder : IDatabaseSchemaBuilder 10 | { 11 | private EmptyDatabaseSchemaBuilder() { } 12 | 13 | public Task Build() => throw new System.InvalidOperationException(); 14 | 15 | public static IDatabaseSchemaBuilder Value => new EmptyDatabaseSchemaBuilder(); 16 | } 17 | } -------------------------------------------------------------------------------- /SyncKusto/Kusto/DatabaseSchemaBuilder/FileDatabaseSchemaBuilder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using System.Windows.Forms; 10 | using Kusto.Data.Common; 11 | 12 | namespace SyncKusto.Kusto.DatabaseSchemaBuilder 13 | { 14 | public class FileDatabaseSchemaBuilder : BaseDatabaseSchemaBuilder 15 | { 16 | private readonly string rootFolder; 17 | private readonly string fileExtension; 18 | 19 | public FileDatabaseSchemaBuilder(string rootFolder, string fileExtension) 20 | { 21 | if (string.IsNullOrWhiteSpace(rootFolder)) 22 | { 23 | throw new ArgumentException($"'{nameof(rootFolder)}' cannot be null or whitespace.", nameof(rootFolder)); 24 | } 25 | 26 | if (string.IsNullOrWhiteSpace(fileExtension)) 27 | { 28 | throw new ArgumentException($"'{nameof(fileExtension)}' cannot be null or whitespace.", nameof(fileExtension)); 29 | } 30 | 31 | this.rootFolder = rootFolder; 32 | this.fileExtension = fileExtension; 33 | } 34 | 35 | public override Task Build() 36 | { 37 | // Find all of the table and function CSL files 38 | if (!Directory.Exists(rootFolder)) 39 | { 40 | Directory.CreateDirectory(rootFolder); 41 | } 42 | 43 | string functionFolder = Path.Combine(rootFolder, "Functions"); 44 | if (!Directory.Exists(functionFolder)) 45 | { 46 | Directory.CreateDirectory(functionFolder); 47 | } 48 | 49 | string[] functionFiles = Directory.GetFiles(functionFolder, $"*.{fileExtension}", SearchOption.AllDirectories); 50 | IEnumerable tableFiles = Directory.GetFiles(rootFolder, $"*.{fileExtension}", SearchOption.AllDirectories).Where(f => !f.Contains("\\Functions\\")); 51 | 52 | var failedObjects = new List(); 53 | 54 | // This is a bit of magic. When called from the UI thread, it would deadlock on Dispose. Running in its own thread works well. 55 | DatabaseSchema resultSchema = null; 56 | var t = Task.Run(() => 57 | { 58 | // Load Kusto Query Engine, this makes it a lot easier to deal with slightly malformed CSL files. 59 | using (var queryEngine = new QueryEngine()) 60 | { 61 | 62 | // Deploy all the tables and functions 63 | var tableTasks = new List(); 64 | foreach (string table in tableFiles) 65 | { 66 | tableTasks.Add(queryEngine.CreateOrAlterTableAsync(File.ReadAllText(table.HandleLongFileNames()), Path.GetFileName(table), true)); 67 | } 68 | failedObjects.AddRange(WaitAllAndGetFailedObjects(tableTasks)); 69 | 70 | var functionTasks = new List(); 71 | foreach (string function in functionFiles) 72 | { 73 | string csl = File.ReadAllText(function.HandleLongFileNames()); 74 | functionTasks.Add(queryEngine.CreateOrAlterFunctionAsync(csl, Path.GetFileName(function))); 75 | } 76 | failedObjects.AddRange(WaitAllAndGetFailedObjects(functionTasks)); 77 | 78 | if (failedObjects.Count > 0) 79 | { 80 | MessageBox.Show( 81 | $"The following objects could not be parsed and will be ignored:\r\n{failedObjects.Aggregate((current, next) => current + "\r\n" + next)}", 82 | "Failure", MessageBoxButtons.OK, MessageBoxIcon.Warning); 83 | } 84 | 85 | // Read the functions and tables back from the locally hosted version of Kusto. 86 | resultSchema = queryEngine.GetDatabaseSchema(); 87 | } 88 | }); 89 | t.Wait(); 90 | 91 | return Task.FromResult(resultSchema); 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /SyncKusto/Kusto/DatabaseSchemaBuilder/IDatabaseSchemaBuilder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Threading.Tasks; 5 | using Kusto.Data.Common; 6 | 7 | namespace SyncKusto.Kusto.DatabaseSchemaBuilder 8 | { 9 | public interface IDatabaseSchemaBuilder 10 | { 11 | Task Build(); 12 | } 13 | } -------------------------------------------------------------------------------- /SyncKusto/Kusto/DatabaseSchemaBuilder/KustoDatabaseSchemaBuilder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using Kusto.Data.Common; 7 | 8 | namespace SyncKusto.Kusto.DatabaseSchemaBuilder 9 | { 10 | public class KustoDatabaseSchemaBuilder : BaseDatabaseSchemaBuilder 11 | { 12 | public KustoDatabaseSchemaBuilder(QueryEngine queryEngine) 13 | { 14 | QueryEngine = queryEngine ?? throw new ArgumentNullException(nameof(queryEngine)); 15 | } 16 | 17 | private QueryEngine QueryEngine { get; } 18 | 19 | public override Task Build() => Task.FromResult(QueryEngine.GetDatabaseSchema()); 20 | } 21 | } -------------------------------------------------------------------------------- /SyncKusto/Kusto/FormattedCslCommandGenerator.cs: -------------------------------------------------------------------------------- 1 | using Kusto.Data.Common; 2 | 3 | namespace SyncKusto.Kusto 4 | { 5 | /// 6 | /// If this tool wants to save CSL files that are formatted slightly differently than the Kusto default, the Kusto 7 | /// calls to CslCommandGenerator can be wrapped in this class. 8 | /// 9 | public static class FormattedCslCommandGenerator 10 | { 11 | /// 12 | /// Wrap the call to CslCommandGenerator.GenerateTableCreateCommand and allow special formatting if the user 13 | /// has enabled the setting flag for it. Also choose between "create" and "create merge" based on setting 14 | /// 15 | /// The table schema to convert to a string 16 | /// True to force the column names to be normalized/escaped 17 | /// 18 | public static string GenerateTableCreateCommand(TableSchema table, bool forceNormalizeColumnName = false) 19 | { 20 | string result = SettingsWrapper.CreateMergeEnabled == true 21 | ? CslCommandGenerator.GenerateTableCreateMergeCommandWithExtraProperties(table, forceNormalizeColumnName) 22 | : CslCommandGenerator.GenerateTableCreateCommand(table, forceNormalizeColumnName); 23 | 24 | if (SettingsWrapper.TableFieldsOnNewLine == true) 25 | { 26 | // We have to do some kind of line break. The three options in settings are "leave as is", "windows", 27 | // or "unix" but the default is "leave as is." "Leave as is" doesn't make sense in this scenario so 28 | // we'll bucket it with "Windows" style. 29 | string lineEnding = SettingsWrapper.LineEndingMode == ChangeModel.LineEndingMode.UnixStyle ? "\n" : "\r\n"; 30 | 31 | // Add a line break between each field 32 | result = result.Replace(", ['", $",{lineEnding} ['"); 33 | 34 | // Add a line break before the first field 35 | int parameterStartIndex = result.LastIndexOf("(["); 36 | result = result.Insert(parameterStartIndex + 1, $"{lineEnding} "); 37 | } 38 | 39 | return result; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SyncKusto/Kusto/Model/IKustoObject.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | 6 | namespace SyncKusto.Kusto.Model 7 | { 8 | public interface IKustoObject : IEquatable 9 | { 10 | /// 11 | /// The name of the Kusto object 12 | /// 13 | string Name { get; set; } 14 | 15 | /// 16 | /// Write this Kusto object to the specified folder 17 | /// 18 | /// The root of all the Kusto files on disk 19 | void WriteToFile(string rootFolder); 20 | 21 | /// 22 | /// Write this Kusto object to Kusto 23 | /// 24 | /// A connection to the target Kusto datbaase 25 | void WriteToKusto(QueryEngine kustoQueryEngine); 26 | 27 | /// 28 | /// Remove this Kusto object from the file system 29 | /// 30 | /// The root of all the Kusto files on disk 31 | void DeleteFromFolder(string rootFolder); 32 | 33 | /// 34 | /// Remove this Kusto object from Kusto 35 | /// 36 | /// A connection to the target Kusto database 37 | void DeleteFromKusto(QueryEngine kustoQueryEngine); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SyncKusto/Kusto/Model/IKustoSchema.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using SyncKusto.Kusto; 5 | 6 | namespace SyncKusto.ChangeModel 7 | { 8 | public interface IKustoSchema 9 | { 10 | void WriteToFile(string rootFolder, string fileExtension); 11 | 12 | void WriteToKusto(QueryEngine kustoQueryEngine); 13 | 14 | void DeleteFromFolder(string rootFolder, string fileExtension); 15 | 16 | void DeleteFromKusto(QueryEngine kustoQueryEngine); 17 | 18 | string Name { get; } 19 | } 20 | } -------------------------------------------------------------------------------- /SyncKusto/Kusto/Model/KustoFunctionSchema.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using Kusto.Data.Common; 6 | using SyncKusto.Kusto; 7 | 8 | namespace SyncKusto.ChangeModel 9 | { 10 | public sealed class KustoFunctionSchema : IKustoSchema, IEquatable 11 | { 12 | public static implicit operator FunctionSchema(KustoFunctionSchema schema) => schema.Value; 13 | 14 | public KustoFunctionSchema(FunctionSchema value) => Value = value; 15 | 16 | private FunctionSchema Value { get; } 17 | 18 | public string Name => Value.Name; 19 | 20 | public void WriteToFile(string rootFolder, string fileExtension) => Value.WriteToFile(rootFolder, fileExtension); 21 | 22 | public void WriteToKusto(QueryEngine kustoQueryEngine) => Value.WriteToKusto(kustoQueryEngine); 23 | 24 | public void DeleteFromFolder(string rootFolder, string fileExtension) => Value.DeleteFromFolder(rootFolder, fileExtension); 25 | 26 | public void DeleteFromKusto(QueryEngine kustoQueryEngine) => Value.DeleteFromKusto(kustoQueryEngine); 27 | 28 | public bool Equals(KustoFunctionSchema other) 29 | { 30 | if (ReferenceEquals(null, other)) return false; 31 | if (ReferenceEquals(this, other)) return true; 32 | return Equals(Value, other.Value); 33 | } 34 | 35 | public override bool Equals(object obj) 36 | { 37 | if (ReferenceEquals(null, obj)) return false; 38 | if (ReferenceEquals(this, obj)) return true; 39 | if (obj.GetType() != this.GetType()) return false; 40 | return Equals((KustoFunctionSchema) obj); 41 | } 42 | 43 | public override int GetHashCode() 44 | { 45 | return (Value != null ? Value.GetHashCode() : 0); 46 | } 47 | 48 | public static bool operator ==(KustoFunctionSchema left, KustoFunctionSchema right) 49 | { 50 | return Equals(left, right); 51 | } 52 | 53 | public static bool operator !=(KustoFunctionSchema left, KustoFunctionSchema right) 54 | { 55 | return !Equals(left, right); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /SyncKusto/Kusto/Model/KustoTableSchema.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using Kusto.Data.Common; 6 | using SyncKusto.Kusto; 7 | 8 | namespace SyncKusto.ChangeModel 9 | { 10 | public sealed class KustoTableSchema : IKustoSchema, IEquatable 11 | { 12 | public static implicit operator TableSchema(KustoTableSchema schema) => schema.Value; 13 | 14 | public KustoTableSchema(TableSchema value) => Value = value; 15 | 16 | private TableSchema Value { get; } 17 | 18 | public void WriteToFile(string rootFolder, string fileExtension) => Value.WriteToFile(rootFolder, fileExtension); 19 | 20 | public void WriteToKusto(QueryEngine kustoQueryEngine) => Value.WriteToKusto(kustoQueryEngine); 21 | public void DeleteFromFolder(string rootFolder, string fileExtension) => Value.DeleteFromFolder(rootFolder, fileExtension); 22 | 23 | public void DeleteFromKusto(QueryEngine kustoQueryEngine) => Value.DeleteFromKusto(kustoQueryEngine); 24 | 25 | public string Name => Value.Name; 26 | 27 | public bool Equals(KustoTableSchema other) 28 | { 29 | if (ReferenceEquals(null, other)) return false; 30 | if (ReferenceEquals(this, other)) return true; 31 | return Equals(Value, other.Value); 32 | } 33 | 34 | public override bool Equals(object obj) 35 | { 36 | if (ReferenceEquals(null, obj)) return false; 37 | if (ReferenceEquals(this, obj)) return true; 38 | if (obj.GetType() != this.GetType()) return false; 39 | return Equals((KustoTableSchema) obj); 40 | } 41 | 42 | public override int GetHashCode() 43 | { 44 | return (Value != null ? Value.GetHashCode() : 0); 45 | } 46 | 47 | public static bool operator ==(KustoTableSchema left, KustoTableSchema right) 48 | { 49 | return Equals(left, right); 50 | } 51 | 52 | public static bool operator !=(KustoTableSchema left, KustoTableSchema right) 53 | { 54 | return !Equals(left, right); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /SyncKusto/Kusto/QueryEngine.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Data; 6 | using System.Diagnostics; 7 | using System.Linq; 8 | using System.Text.RegularExpressions; 9 | using System.Threading.Tasks; 10 | using Kusto.Data; 11 | using Kusto.Data.Common; 12 | using Kusto.Data.Net.Client; 13 | using Newtonsoft.Json; 14 | using SyncKusto.Properties; 15 | using SyncKusto.Utilities; 16 | 17 | namespace SyncKusto.Kusto 18 | { 19 | /// 20 | /// Central location for interacting with a Kusto cluster 21 | /// 22 | public class QueryEngine : IDisposable 23 | { 24 | private readonly ICslAdminProvider _adminClient; 25 | private readonly ICslQueryProvider _queryClient; 26 | private readonly string _databaseName; 27 | private readonly string _cluster; 28 | private readonly bool _tempDatabaseUsed = false; 29 | 30 | /// 31 | /// Constructor which gets ready to make queries to Kusto 32 | /// 33 | /// The connection string builder to connect to Kusto 34 | public QueryEngine(KustoConnectionStringBuilder kustoConnectionStringBuilder) 35 | { 36 | _adminClient = KustoClientFactory.CreateCslAdminProvider(kustoConnectionStringBuilder); 37 | _queryClient = KustoClientFactory.CreateCslQueryProvider(kustoConnectionStringBuilder); 38 | _databaseName = kustoConnectionStringBuilder.InitialCatalog; 39 | _cluster = kustoConnectionStringBuilder.DataSource; 40 | } 41 | 42 | /// 43 | /// Constructor which creates a connection to the workspace cluster and uses the workspace database to temporarily load the schema 44 | /// 45 | public QueryEngine() 46 | { 47 | if (string.IsNullOrEmpty(SettingsWrapper.KustoClusterForTempDatabases)) throw new ArgumentNullException(nameof(SettingsWrapper.KustoClusterForTempDatabases)); 48 | 49 | _databaseName = SettingsWrapper.TemporaryKustoDatabase; 50 | var connString = new KustoConnectionStringBuilder(SettingsWrapper.KustoClusterForTempDatabases) 51 | { 52 | FederatedSecurity = true, 53 | InitialCatalog = _databaseName, 54 | Authority = SettingsWrapper.AADAuthority 55 | }; 56 | 57 | _adminClient = KustoClientFactory.CreateCslAdminProvider(connString); 58 | _queryClient = KustoClientFactory.CreateCslQueryProvider(connString); 59 | _tempDatabaseUsed = true; 60 | _cluster = connString.DataSource; 61 | 62 | CleanDatabase(); 63 | } 64 | 65 | /// 66 | /// Remove any functions and tables that are present in the database. Note that this should only be called when 67 | /// connecting to the temporary database 68 | /// 69 | public void CleanDatabase() 70 | { 71 | if (!_tempDatabaseUsed) 72 | { 73 | throw new Exception("CleanDatabase() was called on something other than the temporary database."); 74 | } 75 | 76 | var schema = GetDatabaseSchema(); 77 | 78 | if (schema.Functions.Count > 0) 79 | { 80 | _adminClient.ExecuteControlCommand( 81 | CslCommandGenerator.GenerateFunctionsDropCommand( 82 | schema.Functions.Select(f => f.Value.Name), true)); 83 | } 84 | 85 | if (schema.Tables.Count > 0) 86 | { 87 | _adminClient.ExecuteControlCommand( 88 | CslCommandGenerator.GenerateTablesDropCommand( 89 | schema.Tables.Select(f => f.Value.Name), true)); 90 | } 91 | } 92 | 93 | /// 94 | /// Get the full database schema 95 | /// 96 | /// 97 | public DatabaseSchema GetDatabaseSchema() 98 | { 99 | DatabaseSchema result = null; 100 | string csl = $@".show database ['{_databaseName}'] schema as json"; 101 | using (IDataReader reader = _adminClient.ExecuteControlCommand(_databaseName, csl)) 102 | { 103 | reader.Read(); 104 | string json = reader[0].ToString(); 105 | ClusterSchema clusterSchema = JsonConvert.DeserializeObject(json); 106 | result = clusterSchema.Databases.First().Value; 107 | } 108 | foreach (var function in result.Functions.Values) 109 | { 110 | switch (SettingsWrapper.LineEndingMode) 111 | { 112 | case ChangeModel.LineEndingMode.WindowsStyle: 113 | function.Body = Regex.Replace(function.Body, @"\r\n|\r|\n", "\r\n"); 114 | break; 115 | case ChangeModel.LineEndingMode.UnixStyle: 116 | function.Body = Regex.Replace(function.Body, @"\r\n|\r|\n", "\n"); 117 | break; 118 | } 119 | 120 | if (function.Folder == null) 121 | { 122 | function.Folder = ""; 123 | } 124 | if (function.DocString == null) 125 | { 126 | function.DocString = ""; 127 | } 128 | } 129 | return result; 130 | } 131 | 132 | /// 133 | /// Create or alter the function definition to match what is specified in the command 134 | /// 135 | /// A create-or-alter function command 136 | /// The name of the function 137 | public Task CreateOrAlterFunctionAsync(string functionCommand, string functionName) 138 | { 139 | return Task.Run(async () => 140 | { 141 | try 142 | { 143 | // If the CSL files on disk were written with an older version of the tool and did not have the skipvalidation paramter, they will fail. 144 | // This code will insert the parameter into the script. 145 | string skipValidationRegEx = @"skipvalidation[\s]*[=]+[\s""@']*true"; 146 | if (!Regex.IsMatch(functionCommand, skipValidationRegEx)) 147 | { 148 | string searchString = "("; 149 | string replaceString = searchString + "skipvalidation = @'true',"; 150 | int firstIndexOf = functionCommand.IndexOf(searchString); 151 | functionCommand = functionCommand.Substring(0, firstIndexOf) + replaceString + functionCommand.Substring(firstIndexOf + searchString.Length); 152 | } 153 | await _adminClient.ExecuteControlCommandAsync(_databaseName, functionCommand).ConfigureAwait(false); 154 | } 155 | catch (Exception ex) 156 | { 157 | throw new CreateOrAlterException("Failed to create or alter a function", ex, functionName); 158 | } 159 | }); 160 | } 161 | 162 | /// 163 | /// Run the create table command. If the table exists, it's schema will be altered. This might result in 164 | /// data loss. 165 | /// 166 | /// A .create table command string 167 | /// The name of the table 168 | /// 169 | /// When this is true, the caller is saying that the table doesn't exist yet so we should skip the alter attempt 170 | /// 171 | public Task CreateOrAlterTableAsync(string tableCommand, string tableName, bool createOnly = false) 172 | { 173 | return Task.Run(async () => 174 | { 175 | string createCommand = tableCommand; 176 | string alterCommand = tableCommand.Replace(".create", ".alter"); 177 | 178 | try 179 | { 180 | if (createOnly) 181 | { 182 | await _adminClient.ExecuteControlCommandAsync(_databaseName, createCommand).ConfigureAwait(false); 183 | } 184 | else 185 | { 186 | try 187 | { 188 | await _adminClient.ExecuteControlCommandAsync(_databaseName, alterCommand).ConfigureAwait(false); 189 | } 190 | catch 191 | { 192 | await _adminClient.ExecuteControlCommandAsync(_databaseName, createCommand).ConfigureAwait(false); 193 | } 194 | } 195 | } 196 | catch (Exception ex) 197 | { 198 | throw new CreateOrAlterException("Failed to create or alter a table", ex, tableName); 199 | } 200 | }); 201 | } 202 | 203 | /// 204 | /// Remove the specified function 205 | /// 206 | /// The function to drop 207 | public void DropFunction(FunctionSchema functionSchema) 208 | { 209 | _adminClient.ExecuteControlCommand(_databaseName, $".drop function ['{functionSchema.Name}']"); 210 | } 211 | 212 | /// 213 | /// Remove the specified table 214 | /// 215 | /// The table to drop 216 | public void DropTable(string tableName) 217 | { 218 | _adminClient.ExecuteControlCommand(_databaseName, $".drop table ['{tableName}']"); 219 | } 220 | 221 | /// 222 | /// Dispose of all the resources we created. Note that if this hangs and you're calling from a UI thread, you might 223 | /// have better luck spinning up the engine in a spearate thread. 224 | /// 225 | public void Dispose() 226 | { 227 | _queryClient?.Dispose(); 228 | _adminClient?.Dispose(); 229 | } 230 | 231 | /// 232 | /// Create a new connection string builder based on the input parameters 233 | /// 234 | /// Either the full cluster name or the short one 235 | /// The name of the database to connect to 236 | /// Optionally connect with AAD client app 237 | /// Optional key for AAD client app 238 | /// Optional thumbprint of a certificate to use for Subject Name Issuer authentication 239 | /// A connection string for accessing Kusto 240 | public static KustoConnectionStringBuilder GetKustoConnectionStringBuilder( 241 | string cluster, 242 | string database, 243 | string aadClientId = null, 244 | string aadClientKey = null, 245 | string certificateThumbprint = null) 246 | { 247 | if (string.IsNullOrEmpty(aadClientId) != string.IsNullOrEmpty(aadClientKey) && 248 | string.IsNullOrEmpty(aadClientId) != string.IsNullOrEmpty(certificateThumbprint)) 249 | { 250 | throw new ArgumentException("If either aadClientId or aadClientKey are specified, they must both be specified."); 251 | } 252 | 253 | if (string.IsNullOrWhiteSpace(SettingsWrapper.AADAuthority)) 254 | { 255 | throw new Exception("Authority value must be specified in the Settings dialog."); 256 | } 257 | 258 | cluster = NormalizeClusterName(cluster); 259 | 260 | // User auth 261 | if (string.IsNullOrWhiteSpace(aadClientId)) 262 | { 263 | return new KustoConnectionStringBuilder(cluster) 264 | { 265 | FederatedSecurity = true, 266 | InitialCatalog = database, 267 | Authority = SettingsWrapper.AADAuthority 268 | }; 269 | } 270 | 271 | // App Key auth 272 | if (!string.IsNullOrWhiteSpace(aadClientId) && !string.IsNullOrWhiteSpace(aadClientKey)) 273 | { 274 | return new KustoConnectionStringBuilder(cluster) 275 | { 276 | FederatedSecurity = true, 277 | InitialCatalog = database, 278 | Authority = SettingsWrapper.AADAuthority, 279 | ApplicationKey = aadClientKey, 280 | ApplicationClientId = aadClientId 281 | }; 282 | } 283 | 284 | // App SNI auth 285 | if (!string.IsNullOrWhiteSpace(aadClientId) && !string.IsNullOrWhiteSpace(certificateThumbprint)) 286 | { 287 | return new KustoConnectionStringBuilder(cluster) 288 | { 289 | InitialCatalog = database, 290 | }.WithAadApplicationCertificateAuthentication( 291 | aadClientId, 292 | CertificateStore.GetCertificate(certificateThumbprint), 293 | SettingsWrapper.AADAuthority, 294 | true); 295 | } 296 | 297 | throw new Exception("Could not determine how to create a connection string from provided parameters."); 298 | } 299 | 300 | /// 301 | /// Allow users to specify cluster.eastus2, cluster.eastus2.kusto.windows.net, or https://cluster.eastus2.kusto.windows.net 302 | /// 303 | /// Input cluster name 304 | /// Normalized cluster name e.g. https://cluster.eastus2.kusto.windows.net 305 | public static string NormalizeClusterName(string cluster) 306 | { 307 | if (cluster.StartsWith("https://")) 308 | { 309 | // If it starts with https, take it verbatim and return from the function 310 | return cluster; 311 | } 312 | else 313 | { 314 | // Trim any spaces and trailing '/' 315 | cluster = cluster.TrimEnd('/').Trim(); 316 | 317 | // If it doesn't end with .com or .net then default to .kusto.windows.net 318 | if (!cluster.EndsWith(".com") && !cluster.EndsWith(".net")) 319 | { 320 | cluster = $@"https://{cluster}.kusto.windows.net"; 321 | } 322 | 323 | // Make sure it starts with https 324 | if (!cluster.StartsWith("https://")) 325 | { 326 | cluster = $"https://{cluster}"; 327 | } 328 | 329 | return cluster; 330 | } 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /SyncKusto/MainForm.Designer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace SyncKusto 5 | { 6 | partial class MainForm 7 | { 8 | /// 9 | /// Required designer variable. 10 | /// 11 | private System.ComponentModel.IContainer components = null; 12 | 13 | /// 14 | /// Clean up any resources being used. 15 | /// 16 | /// true if managed resources should be disposed; otherwise, false. 17 | protected override void Dispose(bool disposing) 18 | { 19 | if (disposing && (components != null)) 20 | { 21 | components.Dispose(); 22 | } 23 | base.Dispose(disposing); 24 | } 25 | 26 | #region Windows Form Designer generated code 27 | 28 | /// 29 | /// Required method for Designer support - do not modify 30 | /// the contents of this method with the code editor. 31 | /// 32 | private void InitializeComponent() 33 | { 34 | System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); 35 | this.toolStrip1 = new System.Windows.Forms.ToolStrip(); 36 | this.btnCompare = new System.Windows.Forms.ToolStripButton(); 37 | this.btnUpdate = new System.Windows.Forms.ToolStripButton(); 38 | this.btnSettings = new System.Windows.Forms.ToolStripButton(); 39 | this.grpComparison = new System.Windows.Forms.GroupBox(); 40 | this.rtbSourceText = new System.Windows.Forms.RichTextBox(); 41 | this.label1 = new System.Windows.Forms.Label(); 42 | this.tvComparison = new System.Windows.Forms.TreeView(); 43 | this.label2 = new System.Windows.Forms.Label(); 44 | 45 | // hack around using a parameterized constructor 46 | spcTarget = new SyncKusto.SchemaPickerControl(new DestinationSelections()); 47 | spcSource = new SyncKusto.SchemaPickerControl(new SourceSelections()); 48 | this.spcTargetHolder = new SyncKusto.SchemaPickerControl(); 49 | this.spcSourceHolder = new SyncKusto.SchemaPickerControl(); 50 | // * 51 | 52 | this.toolStrip1.SuspendLayout(); 53 | this.grpComparison.SuspendLayout(); 54 | this.SuspendLayout(); 55 | // 56 | // toolStrip1 57 | // 58 | this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { 59 | this.btnCompare, 60 | this.btnUpdate, 61 | this.btnSettings}); 62 | this.toolStrip1.Location = new System.Drawing.Point(0, 0); 63 | this.toolStrip1.Name = "toolStrip1"; 64 | this.toolStrip1.Size = new System.Drawing.Size(639, 25); 65 | this.toolStrip1.TabIndex = 2; 66 | this.toolStrip1.Text = "toolStrip1"; 67 | // 68 | // btnCompare 69 | // 70 | this.btnCompare.Image = ((System.Drawing.Image)(resources.GetObject("btnCompare.Image"))); 71 | this.btnCompare.ImageTransparentColor = System.Drawing.Color.Magenta; 72 | this.btnCompare.Name = "btnCompare"; 73 | this.btnCompare.Size = new System.Drawing.Size(76, 22); 74 | this.btnCompare.Text = "Compare"; 75 | this.btnCompare.Click += new System.EventHandler(this.btnCompare_Click); 76 | // 77 | // btnUpdate 78 | // 79 | this.btnUpdate.Enabled = false; 80 | this.btnUpdate.Image = ((System.Drawing.Image)(resources.GetObject("btnUpdate.Image"))); 81 | this.btnUpdate.ImageTransparentColor = System.Drawing.Color.Magenta; 82 | this.btnUpdate.Name = "btnUpdate"; 83 | this.btnUpdate.Size = new System.Drawing.Size(65, 22); 84 | this.btnUpdate.Text = "Update"; 85 | this.btnUpdate.Click += new System.EventHandler(this.btnUpdate_Click); 86 | // 87 | // btnSettings 88 | // 89 | this.btnSettings.Enabled = true; 90 | this.btnSettings.Image = ((System.Drawing.Image)(resources.GetObject("btnSettings.Image"))); 91 | this.btnSettings.ImageTransparentColor = System.Drawing.Color.Magenta; 92 | this.btnSettings.Name = "btnSettings"; 93 | this.btnSettings.Size = new System.Drawing.Size(69, 22); 94 | this.btnSettings.Text = "Settings"; 95 | this.btnSettings.Click += new System.EventHandler(this.btnSettings_Click); 96 | // 97 | // grpComparison 98 | // 99 | this.grpComparison.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 100 | | System.Windows.Forms.AnchorStyles.Left) 101 | | System.Windows.Forms.AnchorStyles.Right))); 102 | this.grpComparison.Controls.Add(this.rtbSourceText); 103 | this.grpComparison.Controls.Add(this.label1); 104 | this.grpComparison.Controls.Add(this.tvComparison); 105 | this.grpComparison.Location = new System.Drawing.Point(12, 270); 106 | this.grpComparison.Name = "grpComparison"; 107 | this.grpComparison.Size = new System.Drawing.Size(615, 535); 108 | this.grpComparison.TabIndex = 3; 109 | this.grpComparison.TabStop = false; 110 | this.grpComparison.Text = "Comparison"; 111 | // 112 | // rtbSourceText 113 | // 114 | this.rtbSourceText.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 115 | | System.Windows.Forms.AnchorStyles.Left) 116 | | System.Windows.Forms.AnchorStyles.Right))); 117 | this.rtbSourceText.Font = new System.Drawing.Font("Consolas", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); 118 | this.rtbSourceText.Location = new System.Drawing.Point(6, 278); 119 | this.rtbSourceText.Name = "rtbSourceText"; 120 | this.rtbSourceText.Size = new System.Drawing.Size(603, 251); 121 | this.rtbSourceText.TabIndex = 11; 122 | this.rtbSourceText.Text = ""; 123 | this.rtbSourceText.WordWrap = false; 124 | // 125 | // label1 126 | // 127 | this.label1.AutoSize = true; 128 | this.label1.Location = new System.Drawing.Point(7, 261); 129 | this.label1.Name = "label1"; 130 | this.label1.Size = new System.Drawing.Size(23, 13); 131 | this.label1.TabIndex = 9; 132 | this.label1.Text = "Diff"; 133 | // 134 | // tvComparison 135 | // 136 | this.tvComparison.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 137 | | System.Windows.Forms.AnchorStyles.Right))); 138 | this.tvComparison.CheckBoxes = true; 139 | this.tvComparison.Location = new System.Drawing.Point(6, 19); 140 | this.tvComparison.Name = "tvComparison"; 141 | this.tvComparison.Size = new System.Drawing.Size(603, 221); 142 | this.tvComparison.TabIndex = 6; 143 | this.tvComparison.AfterCheck += new System.Windows.Forms.TreeViewEventHandler(this.tvComparison_AfterCheck); 144 | this.tvComparison.NodeMouseClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.tvComparison_NodeMouseClick); 145 | // 146 | // label2 147 | // 148 | this.label2.AutoSize = true; 149 | this.label2.Location = new System.Drawing.Point(545, 9); 150 | this.label2.Name = "label2"; 151 | this.label2.Size = new System.Drawing.Size(81, 13); 152 | this.label2.TabIndex = 6; 153 | this.label2.Text = "Build 20250117"; 154 | // 155 | // spcTargetHolder 156 | // 157 | this.spcTargetHolder.Location = new System.Drawing.Point(326, 29); 158 | this.spcTargetHolder.Name = "spcTargetHolder"; 159 | this.spcTargetHolder.Size = new System.Drawing.Size(306, 235); 160 | this.spcTargetHolder.TabIndex = 5; 161 | this.spcTargetHolder.Title = "Target Schema"; 162 | this.spcTargetHolder.Visible = true; 163 | 164 | // 165 | // spcTarget 166 | // 167 | this.spcTarget.Location = this.spcTargetHolder.Location; 168 | this.spcTarget.Name = this.spcTargetHolder.Name; 169 | this.spcTarget.Size = this.spcTargetHolder.Size; 170 | this.spcTarget.TabIndex = this.spcTargetHolder.TabIndex; 171 | this.spcTarget.Title = this.spcTargetHolder.Title; 172 | 173 | // 174 | // spcSourceHolder 175 | // 176 | this.spcSourceHolder.Location = new System.Drawing.Point(12, 28); 177 | this.spcSourceHolder.Name = "spcSourceHolder"; 178 | this.spcSourceHolder.Size = new System.Drawing.Size(306, 230); 179 | this.spcSourceHolder.TabIndex = 4; 180 | this.spcSourceHolder.Title = "Source Schema"; 181 | this.spcSourceHolder.Visible = true; 182 | 183 | // 184 | // spcSource 185 | // 186 | this.spcSource.Location = this.spcSourceHolder.Location; 187 | this.spcSource.Name = this.spcSourceHolder.Name; 188 | this.spcSource.Size = this.spcSourceHolder.Size; 189 | this.spcSource.TabIndex = this.spcSourceHolder.TabIndex; 190 | this.spcSource.Title = this.spcSourceHolder.Title; 191 | 192 | // 193 | // MainForm 194 | // 195 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 196 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 197 | this.ClientSize = new System.Drawing.Size(639, 815); 198 | this.Controls.Add(this.label2); 199 | //this.Controls.Add(this.spcTargetHolder); 200 | //this.Controls.Add(this.spcSourceHolder); 201 | spcTargetHolder.Visible = false; 202 | spcSourceHolder.Visible = false; 203 | this.Controls.Add(this.spcTarget); 204 | this.Controls.Add(this.spcSource); 205 | this.Controls.Add(this.grpComparison); 206 | this.Controls.Add(this.toolStrip1); 207 | this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); 208 | this.MinimumSize = new System.Drawing.Size(655, 815); 209 | this.Name = "MainForm"; 210 | this.Text = "SyncKusto"; 211 | this.toolStrip1.ResumeLayout(false); 212 | this.toolStrip1.PerformLayout(); 213 | this.grpComparison.ResumeLayout(false); 214 | this.grpComparison.PerformLayout(); 215 | this.ResumeLayout(false); 216 | this.PerformLayout(); 217 | 218 | } 219 | 220 | #endregion 221 | 222 | private System.Windows.Forms.ToolStrip toolStrip1; 223 | private System.Windows.Forms.ToolStripButton btnCompare; 224 | private System.Windows.Forms.GroupBox grpComparison; 225 | private System.Windows.Forms.TreeView tvComparison; 226 | private System.Windows.Forms.Label label1; 227 | private System.Windows.Forms.RichTextBox rtbSourceText; 228 | private System.Windows.Forms.ToolStripButton btnUpdate; 229 | private System.Windows.Forms.ToolStripButton btnSettings; 230 | private SchemaPickerControl spcSourceHolder; 231 | private SchemaPickerControl spcTargetHolder; 232 | private SchemaPickerControl spcSource; 233 | private SchemaPickerControl spcTarget; 234 | private System.Windows.Forms.Label label2; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /SyncKusto/Models/INonEmptyStringState.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace SyncKusto.Models 5 | { 6 | public interface INonEmptyStringState 7 | { 8 | INonEmptyStringState Set(string value); 9 | string Get(); 10 | } 11 | } -------------------------------------------------------------------------------- /SyncKusto/Models/NonEmptyString.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using SyncKusto.Validation.Infrastructure; 5 | using System; 6 | 7 | namespace SyncKusto.Models 8 | { 9 | public sealed class NonEmptyString : INonEmptyStringState, IEquatable 10 | { 11 | public bool Equals(NonEmptyString other) 12 | { 13 | if (ReferenceEquals(null, other)) 14 | { 15 | return false; 16 | } 17 | 18 | return ReferenceEquals(this, other) || string.Equals(Value, other.Value); 19 | } 20 | 21 | public override bool Equals(object obj) 22 | { 23 | if (ReferenceEquals(null, obj)) 24 | { 25 | return false; 26 | } 27 | 28 | if (ReferenceEquals(this, obj)) 29 | { 30 | return true; 31 | } 32 | 33 | return obj.GetType() == GetType() && Equals((NonEmptyString)obj); 34 | } 35 | 36 | public override int GetHashCode() => Value.GetHashCode(); 37 | 38 | public static bool operator ==(NonEmptyString left, NonEmptyString right) => Equals(left, right); 39 | 40 | public static bool operator !=(NonEmptyString left, NonEmptyString right) => !Equals(left, right); 41 | 42 | public NonEmptyString(string value) 43 | { 44 | Value = Spec.NonEmptyString(s => s).IsSatisfiedBy(value) 45 | ? value 46 | : throw new ArgumentException(nameof(value)); 47 | } 48 | 49 | private string Value { get; } 50 | 51 | public INonEmptyStringState Set(string value) => new NonEmptyString(value); 52 | 53 | public string Get() => Value; 54 | } 55 | } -------------------------------------------------------------------------------- /SyncKusto/Models/UninitializedString.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | 6 | namespace SyncKusto.Models 7 | { 8 | public class UninitializedString : INonEmptyStringState 9 | { 10 | public INonEmptyStringState Set(string value) => new NonEmptyString(value); 11 | 12 | public string Get() => throw new InvalidOperationException(); 13 | } 14 | } -------------------------------------------------------------------------------- /SyncKusto/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using SyncKusto.Properties; 5 | using System; 6 | using System.Diagnostics; 7 | using System.Windows.Forms; 8 | 9 | namespace SyncKusto 10 | { 11 | class Program 12 | { 13 | [STAThread] 14 | static void Main(string[] args) 15 | { 16 | Application.EnableVisualStyles(); 17 | Application.SetCompatibleTextRenderingDefault(false); 18 | Application.Run(new MainForm()); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /SyncKusto/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Reflection; 5 | using System.Runtime.InteropServices; 6 | 7 | // General Information about an assembly is controlled through the following 8 | // set of attributes. Change these attribute values to modify the information 9 | // associated with an assembly. 10 | [assembly: AssemblyTitle("SyncKusto")] 11 | [assembly: AssemblyDescription("")] 12 | [assembly: AssemblyConfiguration("")] 13 | [assembly: AssemblyCompany("")] 14 | [assembly: AssemblyProduct("SyncKusto")] 15 | [assembly: AssemblyCopyright("Copyright © 2017")] 16 | [assembly: AssemblyTrademark("")] 17 | [assembly: AssemblyCulture("")] 18 | 19 | // Setting ComVisible to false makes the types in this assembly not visible 20 | // to COM components. If you need to access a type in this assembly from 21 | // COM, set the ComVisible attribute to true on that type. 22 | [assembly: ComVisible(false)] 23 | 24 | // The following GUID is for the ID of the typelib if this project is exposed to COM 25 | [assembly: Guid("b8402e4f-09f8-4684-8d70-4def827e264b")] 26 | 27 | // Version information for an assembly consists of the following four values: 28 | // 29 | // Major Version 30 | // Minor Version 31 | // Build Number 32 | // Revision 33 | // 34 | // You can specify all the values or you can default the Build and Revision Numbers 35 | // by using the '*' as shown below: 36 | // [assembly: AssemblyVersion("1.0.*")] 37 | [assembly: AssemblyVersion("1.0.0.0")] 38 | [assembly: AssemblyFileVersion("1.0.0.0")] 39 | -------------------------------------------------------------------------------- /SyncKusto/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace SyncKusto.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SyncKusto.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to . 65 | /// 66 | internal static string a { 67 | get { 68 | return ResourceManager.GetString("a", resourceCulture); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /SyncKusto/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /SyncKusto/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace SyncKusto.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.12.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | 26 | [global::System.Configuration.UserScopedSettingAttribute()] 27 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 28 | [global::System.Configuration.DefaultSettingValueAttribute("")] 29 | public string PreviousFilePath { 30 | get { 31 | return ((string)(this["PreviousFilePath"])); 32 | } 33 | set { 34 | this["PreviousFilePath"] = value; 35 | } 36 | } 37 | 38 | [global::System.Configuration.UserScopedSettingAttribute()] 39 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 40 | [global::System.Configuration.DefaultSettingValueAttribute("")] 41 | public string KustoClusterForTempDatabases { 42 | get { 43 | return ((string)(this["KustoClusterForTempDatabases"])); 44 | } 45 | set { 46 | this["KustoClusterForTempDatabases"] = value; 47 | } 48 | } 49 | 50 | [global::System.Configuration.UserScopedSettingAttribute()] 51 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 52 | [global::System.Configuration.DefaultSettingValueAttribute("")] 53 | public string AADAuthority { 54 | get { 55 | return ((string)(this["AADAuthority"])); 56 | } 57 | set { 58 | this["AADAuthority"] = value; 59 | } 60 | } 61 | 62 | [global::System.Configuration.UserScopedSettingAttribute()] 63 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 64 | [global::System.Configuration.DefaultSettingValueAttribute("")] 65 | public string TemporaryKustoDatabase { 66 | get { 67 | return ((string)(this["TemporaryKustoDatabase"])); 68 | } 69 | set { 70 | this["TemporaryKustoDatabase"] = value; 71 | } 72 | } 73 | 74 | [global::System.Configuration.UserScopedSettingAttribute()] 75 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 76 | [global::System.Configuration.DefaultSettingValueAttribute("True")] 77 | public bool KustoObjectDropWarning { 78 | get { 79 | return ((bool)(this["KustoObjectDropWarning"])); 80 | } 81 | set { 82 | this["KustoObjectDropWarning"] = value; 83 | } 84 | } 85 | 86 | [global::System.Configuration.UserScopedSettingAttribute()] 87 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 88 | [global::System.Configuration.DefaultSettingValueAttribute("False")] 89 | public bool TableFieldsOnNewLine { 90 | get { 91 | return ((bool)(this["TableFieldsOnNewLine"])); 92 | } 93 | set { 94 | this["TableFieldsOnNewLine"] = value; 95 | } 96 | } 97 | 98 | [global::System.Configuration.UserScopedSettingAttribute()] 99 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 100 | [global::System.Configuration.DefaultSettingValueAttribute("False")] 101 | public bool CreateMergeEnabled { 102 | get { 103 | return ((bool)(this["CreateMergeEnabled"])); 104 | } 105 | set { 106 | this["CreateMergeEnabled"] = value; 107 | } 108 | } 109 | 110 | [global::System.Configuration.UserScopedSettingAttribute()] 111 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 112 | [global::System.Configuration.DefaultSettingValueAttribute("True")] 113 | public bool UseLegacyCslExtension { 114 | get { 115 | return ((bool)(this["UseLegacyCslExtension"])); 116 | } 117 | set { 118 | this["UseLegacyCslExtension"] = value; 119 | } 120 | } 121 | 122 | [global::System.Configuration.UserScopedSettingAttribute()] 123 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 124 | [global::System.Configuration.DefaultSettingValueAttribute("CurrentUser")] 125 | public string CertificateLocation { 126 | get { 127 | return ((string)(this["CertificateLocation"])); 128 | } 129 | set { 130 | this["CertificateLocation"] = value; 131 | } 132 | } 133 | 134 | [global::System.Configuration.UserScopedSettingAttribute()] 135 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 136 | public global::System.Collections.Specialized.StringCollection RecentClusters { 137 | get { 138 | return ((global::System.Collections.Specialized.StringCollection)(this["RecentClusters"])); 139 | } 140 | set { 141 | this["RecentClusters"] = value; 142 | } 143 | } 144 | 145 | [global::System.Configuration.UserScopedSettingAttribute()] 146 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 147 | public global::System.Collections.Specialized.StringCollection RecentDatabases { 148 | get { 149 | return ((global::System.Collections.Specialized.StringCollection)(this["RecentDatabases"])); 150 | } 151 | set { 152 | this["RecentDatabases"] = value; 153 | } 154 | } 155 | 156 | [global::System.Configuration.UserScopedSettingAttribute()] 157 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 158 | public global::System.Collections.Specialized.StringCollection RecentAppIds { 159 | get { 160 | return ((global::System.Collections.Specialized.StringCollection)(this["RecentAppIds"])); 161 | } 162 | set { 163 | this["RecentAppIds"] = value; 164 | } 165 | } 166 | 167 | [global::System.Configuration.UserScopedSettingAttribute()] 168 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 169 | [global::System.Configuration.DefaultSettingValueAttribute("0")] 170 | public int LineEndingMode { 171 | get { 172 | return ((int)(this["LineEndingMode"])); 173 | } 174 | set { 175 | this["LineEndingMode"] = value; 176 | } 177 | } 178 | 179 | [global::System.Configuration.UserScopedSettingAttribute()] 180 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 181 | [global::System.Configuration.DefaultSettingValueAttribute("False")] 182 | public bool IgnoreLineEndings { 183 | get { 184 | return ((bool)(this["IgnoreLineEndings"])); 185 | } 186 | set { 187 | this["IgnoreLineEndings"] = value; 188 | } 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /SyncKusto/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | True 19 | 20 | 21 | False 22 | 23 | 24 | False 25 | 26 | 27 | True 28 | 29 | 30 | CurrentUser 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 0 43 | 44 | 45 | False 46 | 47 | 48 | -------------------------------------------------------------------------------- /SyncKusto/SchemaPickerControl.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Kusto.Data; 5 | using Kusto.Data.Common; 6 | using SyncKusto.Functional; 7 | using SyncKusto.Kusto; 8 | using SyncKusto.Kusto.DatabaseSchemaBuilder; 9 | using SyncKusto.SyncSources; 10 | using SyncKusto.Utilities; 11 | using SyncKusto.Validation.ErrorMessages; 12 | using SyncKusto.Validation.Infrastructure; 13 | using System; 14 | using System.Collections.Generic; 15 | using System.Linq; 16 | using System.Security.Cryptography.X509Certificates; 17 | using System.Threading.Tasks; 18 | using System.Windows.Forms; 19 | 20 | namespace SyncKusto 21 | { 22 | public partial class SchemaPickerControl : UserControl 23 | { 24 | private const string ENTRA_ID_USER = "Microsoft Entra ID User"; 25 | private const string ENTRA_ID_APP_KEY = "Microsoft Entra ID App (Key)"; 26 | private const string ENTRA_ID_APP_SNI = "Microsoft Entra ID App (SubjectName/Issuer)"; 27 | 28 | /// 29 | /// Default constructor to make the Windows Forms designer happy 30 | /// 31 | public SchemaPickerControl() 32 | { 33 | InitializeComponent(); 34 | 35 | this.cmbAuthentication.Items.AddRange( 36 | new object[] { 37 | ENTRA_ID_USER, 38 | ENTRA_ID_APP_KEY, 39 | ENTRA_ID_APP_SNI }); 40 | 41 | this.cmbAuthentication.SelectedIndex = 0; 42 | 43 | this.cbCluster.Items.AddRange(SettingsWrapper.RecentClusters.ToArray()); 44 | this.cbDatabase.Items.AddRange(SettingsWrapper.RecentDatabases.ToArray()); 45 | } 46 | 47 | /// 48 | /// Constructor that defaults to type of source 49 | /// 50 | /// Factory for picking a source 51 | public SchemaPickerControl(ISourceSelectionFactory sourceSelectionFactory) : this() 52 | { 53 | txtFilePath.Text = SettingsWrapper.PreviousFilePath; 54 | 55 | SourceSelectionMap = 56 | sourceSelectionFactory.Choose(ToggleFilePathSourcePanel, ToggleKustoSourcePanel); 57 | 58 | SourceValidationMap = 59 | sourceSelectionFactory.Validate(FilePathSourceSpecification, KustoSourceSpecification); 60 | 61 | SourceAllowedMap = 62 | sourceSelectionFactory.Allowed(AllowedFilePathSource, AllowedKustoSource); 63 | 64 | ProgressMessageState = new Stack<(string, SourceSelection)>(); 65 | SetDefaultControlView(); 66 | } 67 | 68 | private IReadOnlyDictionary whenAllowed)> SourceAllowedMap { get; } 69 | 70 | private IReadOnlyDictionary> SourceSelectionMap { get; } 71 | 72 | private IReadOnlyDictionary> SourceValidationMap { get; } 73 | 74 | public SourceSelection SourceSelection { get; private set; } 75 | 76 | public string Title 77 | { 78 | get => grpSourceSchema.Text; 79 | set => grpSourceSchema.Text = value; 80 | } 81 | 82 | private AuthenticationMode Authentication 83 | { 84 | get 85 | { 86 | switch (cmbAuthentication.SelectedItem) 87 | { 88 | case ENTRA_ID_USER: 89 | return AuthenticationMode.AadFederated; 90 | 91 | case ENTRA_ID_APP_KEY: 92 | return AuthenticationMode.AadApplication; 93 | 94 | case ENTRA_ID_APP_SNI: 95 | return AuthenticationMode.AadApplicationSni; 96 | 97 | default: 98 | throw new Exception("Unknown authentication type"); 99 | } 100 | } 101 | } 102 | 103 | public string SourceFilePath => txtFilePath.Text.HandleLongFileNames(); 104 | 105 | public KustoConnectionStringBuilder KustoConnection 106 | { 107 | get 108 | { 109 | switch (Authentication) 110 | { 111 | case AuthenticationMode.AadFederated: 112 | return QueryEngine.GetKustoConnectionStringBuilder(cbCluster.Text, cbDatabase.Text); 113 | 114 | case AuthenticationMode.AadApplication: 115 | return QueryEngine.GetKustoConnectionStringBuilder( 116 | cbCluster.Text, 117 | cbDatabase.Text, 118 | aadClientId: cbAppId.Text, 119 | aadClientKey: txtAppKey.Text); 120 | 121 | case AuthenticationMode.AadApplicationSni: 122 | return QueryEngine.GetKustoConnectionStringBuilder( 123 | cbCluster.Text, 124 | cbDatabase.Text, 125 | aadClientId: cbAppIdSni.Text, 126 | certificateThumbprint: txtCertificate.Text); 127 | 128 | default: 129 | throw new Exception("Unknown authentication type"); 130 | } 131 | } 132 | } 133 | 134 | private Stack<(string message, SourceSelection source)> ProgressMessageState { get; } 135 | 136 | private Func IsConfigured => (input) => Spec.NonEmptyString(s => s).IsSatisfiedBy(input); 137 | 138 | public Action ResetMainFormValueHolders { private get; set; } 139 | 140 | private static Func WhenSelectedItemAndEmptyValues 141 | => 142 | (box) => Spec.IsTrue(c => c.Items.Count == 0) 143 | .And(Spec.NonEmptyString(c => c.Text)) 144 | .IsSatisfiedBy(box); 145 | 146 | private void SetDefaultControlView() 147 | { 148 | EnableSourceSelections(); 149 | SourceSelection = SourceSelection.FilePath(); 150 | ToggleSourceSelections(); 151 | } 152 | 153 | private void AllowedKustoSource(bool predicate) => rbKusto.Enabled = predicate; 154 | 155 | private void AllowedFilePathSource(bool predicate) => rbFilePath.Enabled = predicate; 156 | 157 | private bool FilePathSourceSpecification() => 158 | Spec 159 | .IsTrue(s => s.SourceSelection == SourceSelection.FilePath()) 160 | .And(Spec.NonEmptyString(s => s.SourceFilePath)) 161 | .IsSatisfiedBy(this); 162 | 163 | private bool KustoSourceSpecification() => 164 | Spec 165 | .IsTrue(s => s.SourceSelection == SourceSelection.Kusto()) 166 | .And(Spec.NonEmptyString(s => s.cbCluster.Text)) 167 | .And(Spec.NonEmptyString(s => s.cbDatabase.Text)) 168 | .And( 169 | Spec.IsTrue(s => s.Authentication == AuthenticationMode.AadFederated) 170 | .Or(Spec 171 | .IsTrue(s => s.Authentication == AuthenticationMode.AadApplication) 172 | .And(Spec.NonEmptyString(s => s.cbAppId.Text) 173 | .And(Spec.NonEmptyString(s => s.txtAppKey.Text)))) 174 | .Or(Spec 175 | .IsTrue(s => s.Authentication == AuthenticationMode.AadApplicationSni) 176 | .And(Spec.NonEmptyString(s => s.cbAppIdSni.Text) 177 | .And(Spec.NonEmptyString(s => s.txtCertificate.Text))))) 178 | .IsSatisfiedBy(this); 179 | 180 | private void ToggleFilePathSourcePanel(bool predicate) => pnlFilePath.Visible = predicate; 181 | 182 | private void ToggleKustoSourcePanel(bool predicate) => pnlKusto.Visible = predicate; 183 | 184 | private void EnableSourceSelections() 185 | { 186 | foreach ((bool enabled, Action whenAllowed) value in SourceAllowedMap.Values) 187 | { 188 | value.whenAllowed.Invoke(value.enabled); 189 | } 190 | } 191 | 192 | private void ToggleSourceSelections() 193 | { 194 | foreach (SourceSelection source in SourceSelectionMap.Keys) 195 | { 196 | SourceSelectionMap[source].Invoke(source == SourceSelection); 197 | } 198 | } 199 | 200 | private void cmbAuthentication_SelectedValueChanged(object sender, EventArgs e) 201 | { 202 | pnlApplicationAuthentication.Visible = Authentication == AuthenticationMode.AadApplication; 203 | pnlApplicationSniAuthentication.Visible = Authentication == AuthenticationMode.AadApplicationSni; 204 | } 205 | 206 | private void rbKusto_CheckedChanged(object sender, EventArgs e) => 207 | SourceButtonCheckChange(sender, SourceSelection.Kusto()); 208 | 209 | private void rbFilePath_CheckedChanged(object sender, EventArgs e) => 210 | SourceButtonCheckChange(sender, SourceSelection.FilePath()); 211 | 212 | private void SourceButtonCheckChange(object sender, SourceSelection source) 213 | { 214 | if (((RadioButton)sender).Checked) 215 | { 216 | if (Spec.NonEmptyString(s => s).IsSatisfiedBy(txtOperationProgress.Text)) 217 | { 218 | ProgressMessageState.Push((txtOperationProgress.Text, SourceSelection)); 219 | } 220 | 221 | SourceSelection = source; 222 | (string message, SourceSelection _) = ProgressMessageState.FirstOrDefault(x => x.source == SourceSelection); 223 | ReportProgress(message ?? string.Empty); 224 | } 225 | 226 | ToggleSourceSelections(); 227 | } 228 | 229 | private void btnChooseDirectory_Click(object sender, EventArgs e) 230 | { 231 | var fbd = new FolderBrowserDialog 232 | { 233 | ShowNewFolderButton = false, 234 | Description = "Select the directory that contains the full schema for the Kusto database.", 235 | SelectedPath = txtFilePath.Text 236 | }; 237 | 238 | if (fbd.ShowDialog() == DialogResult.OK) 239 | { 240 | txtFilePath.Text = fbd.SelectedPath; 241 | } 242 | } 243 | 244 | /// 245 | /// Check if the user has correctly specified a schema source 246 | /// 247 | /// True if it is correctly set, false otherwise. 248 | public bool IsValid() => SourceValidationMap[SourceSelection].Invoke(); 249 | 250 | /// 251 | /// Attempt to load the schema specified in the control 252 | /// 253 | /// Either an error or the schema that was loaded 254 | public Either TryLoadSchema() 255 | { 256 | IDatabaseSchemaBuilder schemaBuilder = EmptyDatabaseSchemaBuilder.Value; 257 | 258 | try 259 | { 260 | if (SourceSelection == SourceSelection.Kusto()) 261 | { 262 | schemaBuilder = new KustoDatabaseSchemaBuilder(new QueryEngine(KustoConnection)); 263 | } 264 | 265 | if (SourceSelection == SourceSelection.FilePath()) 266 | { 267 | SettingsWrapper.PreviousFilePath = SourceFilePath; 268 | schemaBuilder = new FileDatabaseSchemaBuilder(SourceFilePath, SettingsWrapper.FileExtension); 269 | } 270 | 271 | switch (schemaBuilder) 272 | { 273 | case KustoDatabaseSchemaBuilder _: 274 | case FileDatabaseSchemaBuilder _: 275 | ReportProgress($@"Constructing schema..."); 276 | return Task.Run(async () => 277 | await schemaBuilder.Build().ConfigureAwait(false)).Result; 278 | 279 | default: 280 | return new DatabaseSchemaOperationError(new InvalidOperationException("An unknown type was supplied.")); 281 | } 282 | } 283 | catch (Exception e) 284 | { 285 | return new DatabaseSchemaOperationError(e); 286 | } 287 | } 288 | 289 | /// 290 | /// Update the UI with the status message 291 | /// 292 | /// The message to display 293 | public void ReportProgress(string message) 294 | { 295 | if (Spec.NotNull(s => s).IsSatisfiedBy(message)) 296 | txtOperationProgress.Text = message; 297 | } 298 | 299 | private void btnCertificate_Click(object sender, EventArgs e) 300 | { 301 | // Show the certificate selection dialog 302 | var selectedCertificateCollection = X509Certificate2UI.SelectFromCollection( 303 | CertificateStore.GetAllCertificates(SettingsWrapper.CertificateLocation), 304 | "Select a certificate", 305 | "Choose a certificate for authentication", 306 | X509SelectionFlag.SingleSelection); 307 | 308 | if (selectedCertificateCollection != null && 309 | selectedCertificateCollection.Count == 1) 310 | { 311 | txtCertificate.Text = selectedCertificateCollection[0].Thumbprint; 312 | } 313 | } 314 | 315 | /// 316 | /// For some of the inputs, we save the most recent values that were used. This method 317 | /// updates the storage behind all those settings to include the most recently used values. 318 | /// 319 | public void SaveRecentValues() 320 | { 321 | SettingsWrapper.AddRecentCluster(this.cbCluster.Text); 322 | SettingsWrapper.AddRecentDatabase(this.cbDatabase.Text); 323 | SettingsWrapper.AddRecentAppId(this.cbAppId.Text); 324 | SettingsWrapper.AddRecentAppId(this.cbAppIdSni.Text); 325 | } 326 | 327 | /// 328 | /// For the inputs where we store recents, reload them all with the latest values. 329 | /// 330 | public void ReloadRecentValues() 331 | { 332 | this.cbCluster.Items.Clear(); 333 | this.cbCluster.Items.AddRange(SettingsWrapper.RecentClusters.ToArray()); 334 | this.cbDatabase.Items.Clear(); 335 | this.cbDatabase.Items.AddRange(SettingsWrapper.RecentDatabases.ToArray()); 336 | this.cbAppId.Items.Clear(); 337 | this.cbAppId.Items.AddRange(SettingsWrapper.RecentAppIds.ToArray()); 338 | this.cbAppIdSni.Items.Clear(); 339 | this.cbAppIdSni.Items.AddRange(SettingsWrapper.RecentAppIds.ToArray()); 340 | } 341 | } 342 | } -------------------------------------------------------------------------------- /SyncKusto/SchemaPickerControl.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | -------------------------------------------------------------------------------- /SyncKusto/SettingsForm.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Kusto.Data; 5 | using Kusto.Data.Common; 6 | using Kusto.Data.Net.Client; 7 | using SyncKusto.ChangeModel; 8 | using SyncKusto.Kusto; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Drawing; 12 | using System.Linq; 13 | using System.Security.Cryptography.X509Certificates; 14 | using System.Windows.Forms; 15 | 16 | namespace SyncKusto 17 | { 18 | /// 19 | /// Collect some settings from the user 20 | /// 21 | public partial class SettingsForm : Form 22 | { 23 | private RadioButton[] lineEndingRadioButtons; 24 | 25 | /// 26 | /// Default constructor 27 | /// 28 | public SettingsForm() 29 | { 30 | InitializeComponent(); 31 | 32 | cbCertLocation.DataSource = Enum.GetValues(typeof(StoreLocation)); 33 | 34 | // Set the radiobutton tag fields to each of the corresponding LineEndingMode enum values 35 | rbLineEndingsLeave.Tag = LineEndingMode.LeaveAsIs; 36 | rbLineEndingsWindows.Tag = LineEndingMode.WindowsStyle; 37 | rbLineEndingsUnix.Tag = LineEndingMode.UnixStyle; 38 | 39 | lineEndingRadioButtons = new[] 40 | { 41 | rbLineEndingsLeave, 42 | rbLineEndingsWindows, 43 | rbLineEndingsUnix 44 | }; 45 | } 46 | 47 | /// 48 | /// Populate the existing settings into the text boxes 49 | /// 50 | /// 51 | /// 52 | private void SettingsForm_Load(object sender, EventArgs e) 53 | { 54 | txtKustoCluster.Text = SettingsWrapper.KustoClusterForTempDatabases; 55 | txtKustoDatabase.Text = SettingsWrapper.TemporaryKustoDatabase; 56 | txtAuthority.Text = SettingsWrapper.AADAuthority; 57 | chkTableDropWarning.Checked = SettingsWrapper.KustoObjectDropWarning; 58 | cbTableFieldsOnNewLine.Checked = SettingsWrapper.TableFieldsOnNewLine ?? false; 59 | cbCreateMerge.Checked = SettingsWrapper.CreateMergeEnabled ?? false; 60 | cbUseLegacyCslExtension.Checked = SettingsWrapper.UseLegacyCslExtension ?? false; 61 | cbCertLocation.SelectedItem = SettingsWrapper.CertificateLocation; 62 | 63 | foreach (var radioButton in lineEndingRadioButtons) 64 | { 65 | radioButton.Checked = (LineEndingMode)radioButton.Tag == SettingsWrapper.LineEndingMode; 66 | } 67 | } 68 | 69 | /// 70 | /// Test out the settings before saving them. 71 | /// 72 | /// 73 | /// 74 | private void btnOk_Click(object sender, System.EventArgs e) 75 | { 76 | Cursor lastCursor = Cursor.Current; 77 | Cursor.Current = Cursors.WaitCursor; 78 | 79 | SettingsWrapper.TableFieldsOnNewLine = cbTableFieldsOnNewLine.Checked; 80 | SettingsWrapper.CreateMergeEnabled = cbCreateMerge.Checked; 81 | SettingsWrapper.KustoObjectDropWarning = chkTableDropWarning.Checked; 82 | SettingsWrapper.AADAuthority = txtAuthority.Text; 83 | SettingsWrapper.UseLegacyCslExtension = cbUseLegacyCslExtension.Checked; 84 | SettingsWrapper.LineEndingMode = (LineEndingMode)lineEndingRadioButtons.Where(b => b.Checked).Single().Tag; 85 | SettingsWrapper.CertificateLocation = (StoreLocation)cbCertLocation.SelectedItem; 86 | 87 | // Only check the Kusto settings if they changed 88 | if (SettingsWrapper.KustoClusterForTempDatabases != txtKustoCluster.Text || 89 | SettingsWrapper.TemporaryKustoDatabase != txtKustoDatabase.Text) 90 | { 91 | // Allow for multiple ways of specifying a cluster name 92 | if (string.IsNullOrEmpty(txtKustoCluster.Text)) 93 | { 94 | MessageBox.Show($"No Kusto cluster was specified.", "Missing Info", MessageBoxButtons.OK, MessageBoxIcon.Error); 95 | return; 96 | } 97 | string clusterName = QueryEngine.NormalizeClusterName(txtKustoCluster.Text); 98 | 99 | string databaseName = txtKustoDatabase.Text; 100 | if (string.IsNullOrEmpty(databaseName)) 101 | { 102 | MessageBox.Show($"No Kusto database was specified.", "Missing Info", MessageBoxButtons.OK, MessageBoxIcon.Error); 103 | return; 104 | } 105 | 106 | // If the required info is present, update the cluster textbox with the modified cluster url 107 | txtKustoCluster.Text = clusterName; 108 | 109 | // Verify connection and permissions by creating and removing a function 110 | var connString = new KustoConnectionStringBuilder(clusterName) 111 | { 112 | FederatedSecurity = true, 113 | InitialCatalog = databaseName, 114 | Authority = txtAuthority.Text 115 | }; 116 | var adminClient = KustoClientFactory.CreateCslAdminProvider(connString); 117 | 118 | try 119 | { 120 | string functionName = "SyncKustoPermissionsTest" + Guid.NewGuid(); 121 | adminClient.ExecuteControlCommand( 122 | CslCommandGenerator.GenerateCreateOrAlterFunctionCommand( 123 | functionName, 124 | "", 125 | "", 126 | new Dictionary(), 127 | "{print now()}")); 128 | adminClient.ExecuteControlCommand(CslCommandGenerator.GenerateFunctionDropCommand(functionName)); 129 | } 130 | catch (Exception ex) 131 | { 132 | if (ex.Message.Contains("403-Forbidden")) 133 | { 134 | MessageBox.Show($"The current user does not have permission to create a function on cluster('{clusterName}').database('{databaseName}')", "Error Validating Permissions", MessageBoxButtons.OK, MessageBoxIcon.Error); 135 | } 136 | else if (ex.Message.Contains("failed to resolve the service name")) 137 | { 138 | MessageBox.Show($"Cluster {clusterName} could not be found.", "Error Validating Permissions", MessageBoxButtons.OK, MessageBoxIcon.Error); 139 | } 140 | else if (ex.Message.Contains("Kusto client failed to perform authentication")) 141 | { 142 | MessageBox.Show($"Could not authenticate with Microsoft Entra ID. Please verify that the Microsoft Entra ID Authority is specified correctly.", "Error Authenticating", MessageBoxButtons.OK, MessageBoxIcon.Error); 143 | } 144 | else 145 | { 146 | MessageBox.Show($"Unknown error: {ex.Message}", "Error Validating Permissions", MessageBoxButtons.OK, MessageBoxIcon.Error); 147 | } 148 | return; 149 | } 150 | 151 | // Verify that the scratch database is empty 152 | try 153 | { 154 | long functionCount = 0; 155 | long tableCount = 0; 156 | 157 | using (var functionReader = adminClient.ExecuteControlCommand(".show functions | count")) 158 | { 159 | functionReader.Read(); 160 | functionCount = functionReader.GetInt64(0); 161 | } 162 | 163 | using (var tableReader = adminClient.ExecuteControlCommand(".show tables | count")) 164 | { 165 | tableReader.Read(); 166 | tableCount = tableReader.GetInt64(0); 167 | } 168 | 169 | if (functionCount != 0 || tableCount != 0) 170 | { 171 | var wipeDialogResult = MessageBox.Show($"WARNING! There are existing functions and tables in the {txtKustoDatabase.Text} database" + 172 | $" on the {txtKustoCluster.Text} cluster. If you proceed, everything will be dropped from that database every time a comparison " + 173 | $"is run. Do you wish to DROP EVERYTHING in the '{txtKustoDatabase.Text}' database?", 174 | "Non-Empty Database", 175 | MessageBoxButtons.YesNo, 176 | MessageBoxIcon.Warning); 177 | if (wipeDialogResult != DialogResult.Yes) 178 | { 179 | return; 180 | } 181 | 182 | // Note that we don't actually need to clean the database here. We've gotten 183 | // permission to do so and it will happen automatically as needed during 184 | // schema comparison operations. 185 | } 186 | } 187 | catch (Exception ex) 188 | { 189 | MessageBox.Show($"Unknown error: {ex.Message}", "Error Validating Empty Database", MessageBoxButtons.OK, MessageBoxIcon.Error); 190 | return; 191 | } 192 | // Store the settings now that we know they work 193 | SettingsWrapper.KustoClusterForTempDatabases = clusterName; 194 | SettingsWrapper.TemporaryKustoDatabase = databaseName; 195 | } 196 | 197 | this.Close(); 198 | 199 | Cursor.Current = lastCursor; 200 | } 201 | 202 | /// 203 | /// Close the form without saving anything 204 | /// 205 | /// 206 | /// 207 | private void btnCancel_Click(object sender, EventArgs e) 208 | { 209 | this.Close(); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /SyncKusto/SettingsWrapper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using SyncKusto.ChangeModel; 5 | using SyncKusto.Properties; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Collections.Specialized; 9 | using System.Linq; 10 | using System.Security.Cryptography.X509Certificates; 11 | 12 | namespace SyncKusto 13 | { 14 | /// 15 | /// Settings are saved in a config file. This class wraps all of them to provide type-checking. 16 | /// 17 | public static class SettingsWrapper 18 | { 19 | /// 20 | /// The location of Kusto files that was used previously 21 | /// 22 | public static string PreviousFilePath 23 | { 24 | get => Settings.Default["PreviousFilePath"] as string; 25 | set 26 | { 27 | Settings.Default["PreviousFilePath"] = value; 28 | Settings.Default.Save(); 29 | } 30 | } 31 | 32 | /// 33 | /// The Kusto cluster that should be used to create temporary databases for schema comparison 34 | /// 35 | public static string KustoClusterForTempDatabases 36 | { 37 | get => Settings.Default["KustoClusterForTempDatabases"] as string; 38 | set 39 | { 40 | Settings.Default["KustoClusterForTempDatabases"] = value; 41 | Settings.Default.Save(); 42 | } 43 | } 44 | 45 | /// 46 | /// The temporary database to use to load a schema 47 | /// 48 | public static string TemporaryKustoDatabase 49 | { 50 | get => Settings.Default["TemporaryKustoDatabase"] as string; 51 | set 52 | { 53 | Settings.Default["TemporaryKustoDatabase"] = value; 54 | Settings.Default.Save(); 55 | } 56 | } 57 | 58 | /// 59 | /// The AAD Authority to use to authenticate a user. For user auth, this might work when it's empty depending on the tenant configuration, 60 | /// but it's always required for AAD application auth. 61 | /// 62 | public static string AADAuthority 63 | { 64 | get => Settings.Default["AADAuthority"] as string; 65 | set 66 | { 67 | Settings.Default["AADAuthority"] = value; 68 | Settings.Default.Save(); 69 | } 70 | } 71 | 72 | /// 73 | /// Prompt the user before dropping any tables from the target as part of an Update operation 74 | /// 75 | public static bool KustoObjectDropWarning 76 | { 77 | get 78 | { 79 | bool? currentSetting = Settings.Default["KustoObjectDropWarning"] as bool?; 80 | return currentSetting ?? false; 81 | } 82 | set 83 | { 84 | Settings.Default["KustoObjectDropWarning"] = value; 85 | Settings.Default.Save(); 86 | } 87 | } 88 | 89 | /// 90 | /// If true, every table field will get it's own line in the resulting CSL file 91 | /// 92 | public static bool? TableFieldsOnNewLine 93 | { 94 | get => Settings.Default["TableFieldsOnNewLine"] as bool?; 95 | set 96 | { 97 | Settings.Default["TableFieldsOnNewLine"] = value; 98 | Settings.Default.Save(); 99 | } 100 | } 101 | 102 | /// 103 | /// If true, table create commands will be ".create-merge table" instead of ".create table" 104 | /// 105 | public static bool? CreateMergeEnabled 106 | { 107 | get => Settings.Default["CreateMergeEnabled"] as bool?; 108 | set 109 | { 110 | Settings.Default["CreateMergeEnabled"] = value; 111 | Settings.Default.Save(); 112 | } 113 | } 114 | 115 | /// 116 | /// If true, files will be read and written using the legacy ".csl" extension instead of ".kql" 117 | /// 118 | public static bool? UseLegacyCslExtension 119 | { 120 | get => Settings.Default[nameof(UseLegacyCslExtension)] as bool?; 121 | set 122 | { 123 | Settings.Default[nameof(UseLegacyCslExtension)] = value; 124 | Settings.Default.Save(); 125 | } 126 | } 127 | 128 | /// 129 | /// Specifies how to handle line endings in the files. They can be left as they are or converted 130 | /// to Windows or Unix style when they are written. 131 | /// 132 | public static LineEndingMode LineEndingMode 133 | { 134 | get 135 | { 136 | var currentValue = Settings.Default[nameof(LineEndingMode)] as int?; 137 | if (currentValue.HasValue && Enum.IsDefined(typeof(LineEndingMode), currentValue.Value)) 138 | { 139 | return (LineEndingMode)currentValue.Value; 140 | } 141 | return LineEndingMode.LeaveAsIs; 142 | } 143 | set 144 | { 145 | Settings.Default[nameof(LineEndingMode)] = (int)value; 146 | Settings.Default.Save(); 147 | } 148 | } 149 | 150 | /// 151 | /// Gets the file extension to use throughout the application when reading and writing Kusto files 152 | /// 153 | public static string FileExtension 154 | { 155 | get => SettingsWrapper.UseLegacyCslExtension.GetValueOrDefault() ? "csl" : "kql"; 156 | } 157 | 158 | /// 159 | /// Get or set the certificate location to search use when displaing certs in the Subject Name Issuer cert picker. 160 | /// 161 | public static StoreLocation CertificateLocation 162 | { 163 | get 164 | { 165 | var currentValue = Settings.Default["CertificateLocation"] as string; 166 | if (string.IsNullOrWhiteSpace(currentValue)) 167 | { 168 | return StoreLocation.CurrentUser; 169 | } 170 | 171 | if (Enum.TryParse(currentValue, out StoreLocation result)) 172 | { 173 | return result; 174 | } 175 | 176 | throw new Exception($"Could not map {currentValue} to StoreLocation enum type."); 177 | } 178 | set 179 | { 180 | Settings.Default["CertificateLocation"] = value.ToString(); 181 | Settings.Default.Save(); 182 | } 183 | } 184 | 185 | /// 186 | /// Get or set the most recently used clusters 187 | /// 188 | public static List RecentClusters 189 | { 190 | get 191 | { 192 | var currentValue = Settings.Default["RecentClusters"] as StringCollection; 193 | if (currentValue == null) 194 | { 195 | return new List(); 196 | } 197 | 198 | return currentValue.Cast().ToList(); 199 | } 200 | set 201 | { 202 | if (!(value is IList)) 203 | { 204 | throw new ArgumentException("Value must be of type IList"); 205 | } 206 | 207 | var sc = new StringCollection(); 208 | sc.AddRange(value.ToArray()); 209 | Settings.Default["RecentClusters"] = sc; 210 | Settings.Default.Save(); 211 | } 212 | } 213 | 214 | /// 215 | /// Get or set the most recently used databases 216 | /// 217 | public static List RecentDatabases 218 | { 219 | get 220 | { 221 | var currentValue = Settings.Default["RecentDatabases"] as StringCollection; 222 | if (currentValue == null) 223 | { 224 | return new List(); 225 | } 226 | 227 | return currentValue.Cast().ToList(); 228 | } 229 | set 230 | { 231 | if (!(value is IList)) 232 | { 233 | throw new ArgumentException("Value must be of type IList"); 234 | } 235 | 236 | var sc = new StringCollection(); 237 | sc.AddRange(value.ToArray()); 238 | Settings.Default["RecentDatabases"] = sc; 239 | Settings.Default.Save(); 240 | } 241 | } 242 | 243 | /// 244 | /// Get or set the most recently used application ids 245 | /// 246 | public static List RecentAppIds 247 | { 248 | get 249 | { 250 | var currentValue = Settings.Default["RecentAppIds"] as StringCollection; 251 | if (currentValue == null) 252 | { 253 | return new List(); 254 | } 255 | 256 | return currentValue.Cast().ToList(); 257 | } 258 | set 259 | { 260 | if (!(value is IList)) 261 | { 262 | throw new ArgumentException("Value must be of type IList"); 263 | } 264 | 265 | var sc = new StringCollection(); 266 | sc.AddRange(value.ToArray()); 267 | Settings.Default["RecentAppIds"] = sc; 268 | Settings.Default.Save(); 269 | } 270 | } 271 | 272 | /// 273 | /// Include a cluster in the recent history list. If it's already in the list, it will be 274 | /// moved to the top of the list. 275 | /// 276 | /// The cluster to include 277 | public static void AddRecentCluster(string cluster) 278 | { 279 | RecentClusters = AddRecentItem(RecentClusters, cluster); 280 | } 281 | 282 | /// 283 | /// Include a database in the recent history list. If it's already in the list, it will be 284 | /// moved to the top of the list. 285 | /// 286 | /// The database to include 287 | public static void AddRecentDatabase(string database) 288 | { 289 | RecentDatabases = AddRecentItem(RecentDatabases, database); 290 | } 291 | 292 | /// 293 | /// Include an application id in the recent history list. If it's already in the list, it 294 | /// will be moved to the top of the list. 295 | /// 296 | /// The application id to include 297 | public static void AddRecentAppId(string applicationId) 298 | { 299 | RecentAppIds = AddRecentItem(RecentAppIds, applicationId); 300 | } 301 | 302 | /// 303 | /// Make sure that an item is included in a list. If it's a new item, it gets added at the 304 | /// top of the list. If it's an existing item, that item gets moved to the top of the list. 305 | /// If the list is longer than 10 items, the least recently used items are truncated to get 306 | /// back to 10. 307 | /// 308 | /// The list of items to update. 309 | /// The item to include in the list. 310 | /// The updated list of items. 311 | private static List AddRecentItem(List itemList, string item) 312 | { 313 | if (string.IsNullOrWhiteSpace(item)) 314 | { 315 | return itemList; 316 | } 317 | 318 | item = item.Trim(); 319 | 320 | if (itemList.Contains(item)) 321 | { 322 | // To bubble this to the top we'll first remove it from the list and then the next 323 | // code block will add it back in. 324 | itemList.Remove(item); 325 | } 326 | 327 | // Add it to the bottom of the list 328 | itemList.Insert(0, item); 329 | while (itemList.Count > 10) 330 | { 331 | itemList.RemoveAt(itemList.Count - 1); 332 | } 333 | 334 | return itemList; 335 | } 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /SyncKusto/SyncKusto.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {B8402E4F-09F8-4684-8D70-4DEF827E264B} 8 | WinExe 9 | Properties 10 | SyncKusto 11 | SyncKusto 12 | v4.7.2 13 | 512 14 | true 15 | 16 | 17 | 18 | AnyCPU 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 7.3 27 | true 28 | 29 | 30 | AnyCPU 31 | pdbonly 32 | true 33 | bin\Release\ 34 | TRACE 35 | prompt 36 | 4 37 | 7.3 38 | 39 | 40 | bin\x64\ 41 | TRACE 42 | true 43 | pdbonly 44 | AnyCPU 45 | 7.3 46 | prompt 47 | MinimumRecommendedRules.ruleset 48 | true 49 | 50 | 51 | true 52 | bin\x64\Debug\ 53 | DEBUG;TRACE 54 | true 55 | full 56 | x64 57 | 7.3 58 | prompt 59 | MinimumRecommendedRules.ruleset 60 | true 61 | 62 | 63 | bin\x64\Release\ 64 | TRACE 65 | true 66 | pdbonly 67 | x64 68 | 7.3 69 | prompt 70 | MinimumRecommendedRules.ruleset 71 | true 72 | 73 | 74 | bin\x64\x64\ 75 | TRACE 76 | true 77 | pdbonly 78 | x64 79 | 7.3 80 | prompt 81 | MinimumRecommendedRules.ruleset 82 | true 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | Form 110 | 111 | 112 | DropWarningForm.cs 113 | 114 | 115 | 116 | Form 117 | 118 | 119 | SettingsForm.cs 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | Form 165 | 166 | 167 | MainForm.cs 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | UserControl 183 | 184 | 185 | SchemaPickerControl.cs 186 | 187 | 188 | 189 | 190 | 191 | DropWarningForm.cs 192 | 193 | 194 | SettingsForm.cs 195 | 196 | 197 | MainForm.cs 198 | 199 | 200 | ResXFileCodeGenerator 201 | Resources.Designer.cs 202 | Designer 203 | 204 | 205 | True 206 | Resources.resx 207 | True 208 | 209 | 210 | SchemaPickerControl.cs 211 | 212 | 213 | SettingsSingleFileGenerator 214 | Settings.Designer.cs 215 | 216 | 217 | True 218 | Settings.settings 219 | True 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 1.7.2 234 | 235 | 236 | 12.2.7 237 | 238 | 239 | 13.0.3 240 | 241 | 242 | 4.5.0 243 | 244 | 245 | 246 | 247 | 254 | -------------------------------------------------------------------------------- /SyncKusto/SyncSources/DestinationSelections.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using SyncKusto.SyncSources; 7 | 8 | namespace SyncKusto 9 | { 10 | public class DestinationSelections : ISourceSelectionFactory 11 | { 12 | public IReadOnlyDictionary> Choose(Action chooseFilePath, Action chooseKusto) => 13 | new Dictionary>() 14 | { 15 | [SourceSelection.FilePath()] = chooseFilePath, 16 | [SourceSelection.Kusto()] = chooseKusto, 17 | }; 18 | 19 | public IReadOnlyDictionary whenAllowed)> Allowed(Action allowFilePath, 20 | Action allowKusto) => 21 | new Dictionary whenEnabled)>() 22 | { 23 | [SourceSelection.FilePath()] = (true, allowFilePath), 24 | [SourceSelection.Kusto()] = (true, allowKusto), 25 | }; 26 | 27 | public IReadOnlyDictionary> Validate(Func validateFilePath, Func validateKusto) => 28 | new Dictionary>() 29 | { 30 | [SourceSelection.FilePath()] = validateFilePath, 31 | [SourceSelection.Kusto()] = validateKusto, 32 | }; 33 | } 34 | } -------------------------------------------------------------------------------- /SyncKusto/SyncSources/ISourceSelectionFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using SyncKusto.SyncSources; 7 | 8 | namespace SyncKusto 9 | { 10 | public interface ISourceSelectionFactory 11 | { 12 | IReadOnlyDictionary> Choose( 13 | Action chooseFilePath, 14 | Action chooseKusto); 15 | 16 | IReadOnlyDictionary whenAllowed)> Allowed( 17 | Action allowFilePath, 18 | Action allowKusto); 19 | 20 | IReadOnlyDictionary> Validate( 21 | Func validateFilePath, 22 | Func validateKusto); 23 | 24 | } 25 | } -------------------------------------------------------------------------------- /SyncKusto/SyncSources/SourceMode.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace SyncKusto 5 | { 6 | public enum SourceMode 7 | { 8 | FilePath, 9 | Kusto 10 | }; 11 | } -------------------------------------------------------------------------------- /SyncKusto/SyncSources/SourceSelection.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | 6 | namespace SyncKusto.SyncSources 7 | { 8 | public sealed class SourceSelection : IEquatable 9 | { 10 | public static implicit operator string(SourceSelection selection) => selection.SourceMode.ToString(); 11 | 12 | public override string ToString() => Enum.GetName(typeof(SourceModeRepresentation), SourceMode) ?? throw new InvalidOperationException(); 13 | 14 | private SourceSelection(SourceModeRepresentation source) => SourceMode = source; 15 | 16 | private enum SourceModeRepresentation 17 | { 18 | FilePath, 19 | Kusto 20 | }; 21 | 22 | private SourceModeRepresentation SourceMode { get; } 23 | 24 | public static SourceSelection Kusto() => new SourceSelection(SourceModeRepresentation.Kusto); 25 | 26 | public static SourceSelection FilePath() => new SourceSelection(SourceModeRepresentation.FilePath); 27 | 28 | public bool Equals(SourceSelection other) 29 | { 30 | if (other is null) return false; 31 | if (ReferenceEquals(this, other)) return true; 32 | return SourceMode == other.SourceMode; 33 | } 34 | 35 | public override bool Equals(object obj) 36 | { 37 | if (obj is null) return false; 38 | if (ReferenceEquals(this, obj)) return true; 39 | return obj.GetType() == this.GetType() && Equals((SourceSelection) obj); 40 | } 41 | 42 | public override int GetHashCode() => (int) SourceMode; 43 | 44 | public static bool operator ==(SourceSelection left, SourceSelection right) => Equals(left, right); 45 | 46 | public static bool operator !=(SourceSelection left, SourceSelection right) => !Equals(left, right); 47 | } 48 | } -------------------------------------------------------------------------------- /SyncKusto/SyncSources/SourceSelections.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using SyncKusto.SyncSources; 7 | 8 | namespace SyncKusto 9 | { 10 | public class SourceSelections : ISourceSelectionFactory 11 | { 12 | public IReadOnlyDictionary> Choose(Action chooseFilePath, Action chooseKusto) => 13 | new Dictionary>() 14 | { 15 | [SourceSelection.FilePath()] = chooseFilePath, 16 | [SourceSelection.Kusto()] = chooseKusto 17 | }; 18 | 19 | public IReadOnlyDictionary whenAllowed)> Allowed(Action allowFilePath, 20 | Action allowKusto) => 21 | new Dictionary whenEnabled)>() 22 | { 23 | [SourceSelection.FilePath()] = (true, allowFilePath), 24 | [SourceSelection.Kusto()] = (true, allowKusto) 25 | }; 26 | 27 | public IReadOnlyDictionary> Validate(Func validateFilePath, Func validateKusto) => 28 | new Dictionary>() 29 | { 30 | [SourceSelection.FilePath()] = validateFilePath, 31 | [SourceSelection.Kusto()] = validateKusto 32 | }; 33 | } 34 | } -------------------------------------------------------------------------------- /SyncKusto/Utilities/CertificateStore.cs: -------------------------------------------------------------------------------- 1 | namespace SyncKusto.Utilities 2 | { 3 | using System; 4 | using System.Security.Cryptography.X509Certificates; 5 | 6 | internal class CertificateStore 7 | { 8 | /// 9 | /// Find a certificate by thumbprint in the My store of either CurrentUser or LocalMachine. 10 | /// 11 | /// The thumbprint of the certificate to retrieve. 12 | /// The requested certificate 13 | public static X509Certificate2 GetCertificate(string thumbprint) 14 | { 15 | var certificate = GetCertificate(StoreLocation.CurrentUser, StoreName.My, thumbprint); 16 | if (certificate == null) 17 | { 18 | certificate = GetCertificate(StoreLocation.LocalMachine, StoreName.My, thumbprint); 19 | } 20 | 21 | if (certificate == null) 22 | { 23 | throw new Exception($"Cannot find certificate with thumbprint: {thumbprint}"); 24 | } 25 | 26 | return certificate; 27 | } 28 | 29 | /// 30 | /// Get all the certificates in both the Current User and Local Machine locations. 31 | /// 32 | /// A collection of certificates 33 | public static X509Certificate2Collection GetAllCertificates(StoreLocation storeLocation) 34 | { 35 | var store = new X509Store(StoreName.My, storeLocation); 36 | store.Open(OpenFlags.ReadOnly); 37 | var certificates = store.Certificates; 38 | store.Close(); 39 | 40 | return certificates; 41 | } 42 | 43 | /// 44 | /// Find a certificate by thumbprint in the specified store and location. 45 | /// 46 | /// The location of the store to search for the certificate. 47 | /// The name of the store to search for the certificate. 48 | /// The thumbprint of the certificate to retrieve. 49 | /// The requested certificate or null if it is not found. 50 | private static X509Certificate2 GetCertificate(StoreLocation storeLocation, StoreName storeName, string thumbprint) 51 | { 52 | var certStore = new X509Store(storeName, storeLocation); 53 | certStore.Open(OpenFlags.ReadOnly); 54 | var certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); 55 | certStore.Close(); 56 | 57 | if (certCollection.Count == 0) 58 | { 59 | return null; 60 | } 61 | 62 | return certCollection[0]; 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /SyncKusto/Validation/ErrorMessages/DatabaseSchemaOperationError.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | 6 | namespace SyncKusto.Validation.ErrorMessages 7 | { 8 | public class DatabaseSchemaOperationError : IOperationError 9 | { 10 | public DatabaseSchemaOperationError(Exception exception) 11 | { 12 | Exception = exception; 13 | } 14 | 15 | public Exception Exception { get; } 16 | } 17 | } -------------------------------------------------------------------------------- /SyncKusto/Validation/ErrorMessages/DefaultOperationErrorMessageResolver.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using SyncKusto.Functional; 6 | using SyncKusto.Models; 7 | using SyncKusto.Validation.ErrorMessages.Specifications; 8 | 9 | namespace SyncKusto.Validation.ErrorMessages 10 | { 11 | public class DefaultOperationErrorMessageResolver 12 | { 13 | private DefaultOperationErrorMessageResolver(Reiterable errorSpecifications) 14 | { 15 | ErrorSpecifications = errorSpecifications; 16 | } 17 | 18 | private Reiterable ErrorSpecifications { get; } 19 | 20 | public static Func Using( 21 | Func> specifications) => 22 | () => new DefaultOperationErrorMessageResolver(specifications()); 23 | 24 | public INonEmptyStringState ResolveFor(IOperationError error) 25 | { 26 | foreach (IOperationErrorMessageSpecification operationErrorMessageSpecification in ErrorSpecifications) 27 | { 28 | switch (error.Exception) 29 | { 30 | case AggregateException ae: 31 | foreach (Exception innerException in ae.InnerExceptions) 32 | { 33 | if (innerException.InnerException == null && 34 | operationErrorMessageSpecification.Match(innerException) is NonEmptyString leaf) 35 | return leaf; 36 | 37 | // do one level of nesting 38 | if (operationErrorMessageSpecification.Match(innerException.InnerException) is NonEmptyString nodeParent) 39 | return nodeParent; 40 | } 41 | break; 42 | case Exception exception 43 | when operationErrorMessageSpecification.Match(exception) is NonEmptyString matched: 44 | return matched; 45 | } 46 | } 47 | 48 | return new DefaultOperationErrorSpecification().Match(error.Exception); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /SyncKusto/Validation/ErrorMessages/IOperationError.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | 6 | namespace SyncKusto.Validation.ErrorMessages 7 | { 8 | public interface IOperationError 9 | { 10 | Exception Exception { get; } 11 | } 12 | } -------------------------------------------------------------------------------- /SyncKusto/Validation/ErrorMessages/IOperationErrorMessageSpecification.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using SyncKusto.Models; 6 | 7 | namespace SyncKusto.Validation.ErrorMessages 8 | { 9 | public interface IOperationErrorMessageSpecification 10 | { 11 | INonEmptyStringState Match(Exception exception); 12 | } 13 | } -------------------------------------------------------------------------------- /SyncKusto/Validation/ErrorMessages/NonSpecificOperationError.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | 6 | namespace SyncKusto.Validation.ErrorMessages 7 | { 8 | public class NonSpecificOperationError : IOperationError 9 | { 10 | public NonSpecificOperationError(Exception exception) 11 | { 12 | Exception = exception; 13 | } 14 | 15 | public Exception Exception { get; } 16 | } 17 | } -------------------------------------------------------------------------------- /SyncKusto/Validation/ErrorMessages/OperationErrorMessageSpecification.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using SyncKusto.Models; 6 | using SyncKusto.Validation.Infrastructure; 7 | 8 | namespace SyncKusto.Validation.ErrorMessages 9 | { 10 | public class OperationErrorMessageSpecification : IOperationErrorMessageSpecification 11 | { 12 | public OperationErrorMessageSpecification(Specification specification, string displayMessage) 13 | { 14 | Specification = specification; 15 | DisplayMessage = displayMessage; 16 | } 17 | 18 | private string DisplayMessage { get; } 19 | 20 | private Specification Specification { get; } 21 | 22 | public INonEmptyStringState Match(Exception exception) => 23 | Specification.IsSatisfiedBy(exception) 24 | ? (INonEmptyStringState) new NonEmptyString(DisplayMessage) 25 | : new UninitializedString(); 26 | } 27 | } -------------------------------------------------------------------------------- /SyncKusto/Validation/ErrorMessages/Specifications/DefaultOperationErrorSpecification.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using SyncKusto.Models; 6 | 7 | namespace SyncKusto.Validation.ErrorMessages.Specifications 8 | { 9 | public class DefaultOperationErrorSpecification : IOperationErrorMessageSpecification 10 | { 11 | public INonEmptyStringState Match(Exception exception) 12 | { 13 | switch (exception) 14 | { 15 | case AggregateException ae: 16 | foreach (Exception innerException in ae.InnerExceptions) 17 | { 18 | return Match(innerException); 19 | } 20 | break; 21 | default: 22 | return new NonEmptyString(exception.Message); 23 | } 24 | 25 | return new UninitializedString(); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /SyncKusto/Validation/ErrorMessages/Specifications/FilePathOperationErrorSpecifications.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.IO; 6 | using SyncKusto.Validation.Infrastructure; 7 | 8 | namespace SyncKusto.Validation.ErrorMessages.Specifications 9 | { 10 | public static class FilePathOperationErrorSpecifications 11 | { 12 | public static IOperationErrorMessageSpecification FolderNotFound() => 13 | new OperationErrorMessageSpecification(Spec 14 | .IsTrue(ex => ex is DirectoryNotFoundException _), 15 | "The folder path provided could not be found."); 16 | } 17 | } -------------------------------------------------------------------------------- /SyncKusto/Validation/ErrorMessages/Specifications/KustoOperationErrorSpecifications.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using Kusto.Data.Exceptions; 6 | using SyncKusto.Validation.Infrastructure; 7 | 8 | namespace SyncKusto.Validation.ErrorMessages.Specifications 9 | { 10 | public static class KustoOperationErrorSpecifications 11 | { 12 | public static IOperationErrorMessageSpecification ClusterNotFound() => 13 | new OperationErrorMessageSpecification(Spec 14 | .IsTrue(ex => ex is KustoClientNameResolutionFailureException _), 15 | "The Kusto cluster could not be located."); 16 | 17 | public static IOperationErrorMessageSpecification DatabaseNotFound() => 18 | new OperationErrorMessageSpecification(Spec 19 | .IsTrue(ex => ex is KustoBadRequestException request 20 | && request.Message.Contains("'Database' was not found")), 21 | "The Kusto database could not be found."); 22 | 23 | public static IOperationErrorMessageSpecification CannotAuthenticate() => 24 | new OperationErrorMessageSpecification(Spec 25 | .IsTrue(ex => ex is KustoClientAuthenticationException), 26 | "Could not authenticate with AAD. Check the Entra ID authority in the Settings."); 27 | 28 | public static IOperationErrorMessageSpecification NoPermissions() => 29 | new OperationErrorMessageSpecification(Spec 30 | .IsTrue(ex => ex is InvalidOperationException request 31 | && request.Message.Contains("Sequence contains no elements")), 32 | "No schema found. Check database permissions."); 33 | } 34 | } -------------------------------------------------------------------------------- /SyncKusto/Validation/Infrastructure/Composite.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace SyncKusto.Validation.Infrastructure 9 | { 10 | internal class Composite : Specification 11 | { 12 | public Composite(Func, bool> compositionFunction, params Specification[] subspecifications) 13 | { 14 | CompositionFunction = compositionFunction; 15 | Subspecifications = subspecifications; 16 | } 17 | 18 | private Func, bool> CompositionFunction { get; } 19 | private IEnumerable> Subspecifications { get; } 20 | 21 | public override bool IsSatisfiedBy(T obj) => CompositionFunction(Subspecifications.Select(spec => spec.IsSatisfiedBy(obj))); 22 | } 23 | } -------------------------------------------------------------------------------- /SyncKusto/Validation/Infrastructure/NotNull.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace SyncKusto.Validation.Infrastructure 5 | { 6 | internal class NotNull : Specification 7 | { 8 | public override bool IsSatisfiedBy(T obj) => !object.ReferenceEquals(obj, null); 9 | } 10 | } -------------------------------------------------------------------------------- /SyncKusto/Validation/Infrastructure/NotNullOrEmptyString.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace SyncKusto.Validation.Infrastructure 5 | { 6 | internal class NotNullOrEmptyString : Specification 7 | { 8 | public override bool IsSatisfiedBy(string obj) => !string.IsNullOrEmpty(obj); 9 | } 10 | 11 | internal class NullOrEmptyString : Specification 12 | { 13 | public override bool IsSatisfiedBy(string obj) => string.IsNullOrEmpty(obj); 14 | } 15 | } -------------------------------------------------------------------------------- /SyncKusto/Validation/Infrastructure/Predicate.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | 6 | namespace SyncKusto.Validation.Infrastructure 7 | { 8 | internal class Predicate : Specification 9 | { 10 | public Predicate(Func predicate) => Delegate = predicate; 11 | 12 | private Func Delegate { get; } 13 | 14 | public override bool IsSatisfiedBy(T obj) => this.Delegate(obj); 15 | } 16 | } -------------------------------------------------------------------------------- /SyncKusto/Validation/Infrastructure/Property.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | 6 | namespace SyncKusto.Validation.Infrastructure 7 | { 8 | internal class Property : Specification 9 | { 10 | private Func PropertyGetter { get; } 11 | private Specification Subspecification { get; } 12 | 13 | public Property(Func propertyGetter, Specification subspecification) 14 | { 15 | PropertyGetter = propertyGetter; 16 | Subspecification = subspecification; 17 | } 18 | 19 | public override bool IsSatisfiedBy(TType obj) => Subspecification.IsSatisfiedBy(PropertyGetter(obj)); 20 | } 21 | } -------------------------------------------------------------------------------- /SyncKusto/Validation/Infrastructure/Spec.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | 6 | namespace SyncKusto.Validation.Infrastructure 7 | { 8 | public static class Spec 9 | { 10 | public static Specification NotNull(Func propertyGetter) => 11 | new Property(propertyGetter, new NotNull()); 12 | 13 | public static Specification Null(Func propertyGetter) => 14 | NotNull(propertyGetter).Not(); 15 | 16 | public static Specification IsTrue(Func predicate) => 17 | new Predicate(predicate); 18 | 19 | public static Specification NonEmptyString(Func propertyGetter) => 20 | new Property(propertyGetter, new NotNullOrEmptyString()); 21 | 22 | public static Specification EmptyString(Func propertyGetter) => 23 | new Property(propertyGetter, new NullOrEmptyString()); 24 | } 25 | } -------------------------------------------------------------------------------- /SyncKusto/Validation/Infrastructure/Specification.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Linq; 5 | 6 | namespace SyncKusto.Validation.Infrastructure 7 | { 8 | public abstract class Specification 9 | { 10 | public abstract bool IsSatisfiedBy(T obj); 11 | 12 | public Specification And(Specification other) => 13 | new Composite(results => results.All(result => result == true), this, other); 14 | 15 | public Specification Or(Specification other) => 16 | new Composite(results => results.Any(result => result == true), this, other); 17 | 18 | public Specification Not() => 19 | new Transform(result => !result, this); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SyncKusto/Validation/Infrastructure/Transform.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | 6 | namespace SyncKusto.Validation.Infrastructure 7 | { 8 | internal class Transform : Specification 9 | { 10 | private Func Transformation { get; } 11 | private Specification Subspecification { get; } 12 | 13 | public Transform(Func transformation, Specification specification) 14 | { 15 | Transformation = transformation; 16 | Subspecification = specification; 17 | } 18 | 19 | public override bool IsSatisfiedBy(T obj) => Transformation(Subspecification.IsSatisfiedBy(obj)); 20 | } 21 | } -------------------------------------------------------------------------------- /SyncKusto/icons/LibrarySetting_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/synckusto/e344fe0a3cf83559bd1395882342f15c593a4c1d/SyncKusto/icons/LibrarySetting_16x.png -------------------------------------------------------------------------------- /SyncKusto/icons/SchemaCompare.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/synckusto/e344fe0a3cf83559bd1395882342f15c593a4c1d/SyncKusto/icons/SchemaCompare.ico -------------------------------------------------------------------------------- /SyncKusto/icons/SyncArrow_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/synckusto/e344fe0a3cf83559bd1395882342f15c593a4c1d/SyncKusto/icons/SyncArrow_16x.png -------------------------------------------------------------------------------- /SyncKusto/icons/UploadFile_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/synckusto/e344fe0a3cf83559bd1395882342f15c593a4c1d/SyncKusto/icons/UploadFile_16x.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/synckusto/e344fe0a3cf83559bd1395882342f15c593a4c1d/screenshot.png --------------------------------------------------------------------------------