This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
sql-inliner is a .NET CLI tool that optimizes SQL Server views by inlining nested views into a single flattened query, optionally stripping unused columns and joins. It parses SQL using Microsoft's ScriptDom (TSql150Parser) and uses the visitor pattern to analyze and transform the AST.
# Build
dotnet build src/SqlInliner/SqlInliner.csproj
# Run all tests
dotnet test src/SqlInliner.Tests/SqlInliner.Tests.csproj
# Run a single test by name
dotnet test src/SqlInliner.Tests/SqlInliner.Tests.csproj --filter "InlineSimpleView"
# Run tests in a specific class
dotnet test src/SqlInliner.Tests/SqlInliner.Tests.csproj --filter "FullyQualifiedName~SimpleTests"
# Run the tool locally
dotnet run --project src/SqlInliner/SqlInliner.csproj -- -vp "./path/to/view.sql" --strip-unused-joinsThe inlining pipeline flows through these core classes:
-
Program.cs — CLI entry point using System.CommandLine. Conditionally compiled out (
#if !RELEASELIBRARY) when building as a library. LoadsInlinerConfigfrom--configor auto-discoveredsqlinliner.json, merges CLI overrides (CLI > config > default). -
DatabaseConnection — Wraps
IDbConnection(Dapper) to querysys.viewsfor non-indexed views. Has a parameterless constructor for testing/file-only workflows that accepts mock view definitions viaAddViewDefinition().ParseObjectName(string)parses"schema.name"or"name"strings intoSchemaObjectName. -
DatabaseView — Parses SQL with
TSql150Parser, extracts the AST tree and aReferencesVisitor. HandlesCREATE OR ALTER VIEWconversion via regex. Embeds original SQL betweenBeginOriginal/EndOriginalmarkers so previously-inlined views can be re-inlined from their original source. -
ReferencesVisitor (
TSqlFragmentVisitor) — Walks the AST to collect view references, table references, and column references. UsesDatabaseConnection.IsView()to distinguish views from tables. Auto-assigns aliases to unaliased view references. -
DatabaseViewInliner — Core orchestrator. Recursively resolves nested views, optionally strips unused columns/joins based on
InlinerOptions, then replaces view references withQueryDerivedTable(subqueries) viaTableInlineVisitor. HandlesBinaryQueryExpression(UNION/EXCEPT/INTERSECT) by stripping columns across all branches. -
TableInlineVisitor (
TSqlFragmentVisitor) — Performs the actual AST replacement: swapsNamedTableReferencenodes forQueryDerivedTableand removes unused table references fromFromClauseandQualifiedJoin. -
DerivedTableStripper — Post-processing step that strips unused columns and LEFT JOINs inside nested
QueryDerivedTablenodes produced by inlining. Runs afterDatabaseViewInliner.Inline()and beforeDerivedTableFlattenerwhenStripUnusedColumnsorStripUnusedJoinsis enabled. Iterates until no more stripping occurs (handles cascading effects across nesting levels). Skips derived tables with SELECT *, DISTINCT, TOP, GROUP BY, or HAVING. Only strips LEFT OUTER JOINs toQueryDerivedTablenodes (notNamedTableReferencewhich were already evaluated by the inlining logic with join hints). Uses scope-awareOuterScopeColumnReferenceCollectorthat stops at derived table boundaries. -
DerivedTableFlattener — Post-processing step that flattens derived tables (subqueries) produced by inlining. Runs after
DerivedTableStripperwhenFlattenDerivedTablesis enabled. Walks the AST to findQueryDerivedTablenodes withinQuerySpecificationFROM/JOIN trees and replaces eligible ones with their inner table references. Handles single-table and multi-table (JOIN) inner queries, alias collision resolution, column reference rewriting, and WHERE clause merging. Uses scope-aware visitors (OuterScopeColumnReferenceCollector) that stop at derived table boundaries to avoid corrupting inner-scope AST nodes. -
InlinerResult — Formats the final output with a metadata comment containing original SQL, referenced views list, and strip statistics.
Conditionally compiled (#if !RELEASELIBRARY) and excluded from the library build via <Compile Remove="Optimize\**" />.
-
OptimizeCommand — System.CommandLine subcommand (
optimize) with--connection-stringand--view-nameoptions. Accepts a sharedconfigOptionfrom Program.cs; connection string can come from CLI or config. Registered inProgram.csviarootCommand.Add(OptimizeCommand.Create(configOption)). -
OptimizeSession — Orchestrates the 9-step interactive workflow (connect → select → inline → review → deploy → validate → iterate → benchmark → summary). All I/O goes through
IConsoleWizardfor testability. -
ConsoleWizard / IConsoleWizard — Abstraction for interactive console I/O (prompts, colored output, tables). Tests use a
MockWizardwith queued answers. -
SessionDirectory — Manages a session folder (
optimize-{name}-{timestamp}/), saves iteration files, execution plans (.sqlplan), a self-contained HTML benchmark report (benchmark.html), computes SHA256 hashes for edit detection, and writes a session log. -
QueryRunner — Executes validation queries (COUNT, EXCEPT) and benchmarks (SET STATISTICS TIME/IO/XML via
SqlConnection.InfoMessage) with configurable command timeouts. Parses per-table IO statistics (TableIOStats) and captures actual execution plans as XML. Results are returned inBenchmarkResult.
Conditionally compiled (#if !RELEASELIBRARY) alongside the Optimize subsystem.
-
ValidateCommand — System.CommandLine subcommand (
validate) with--connection-string,--deploy,--deploy-only,--output-dir,--stop-on-error,--filter,--timeout, and inliner boolean flags. Accepts a sharedconfigOptionfrom Program.cs; boolean flags resolved viaProgram.ResolveOption(CLI > config > default). Registered inProgram.csviarootCommand.Add(ValidateCommand.Create(configOption)). -
ValidateSession — Batch-validates all views using a two-phase approach. Phase 1 (inline): iterates
connection.Viewsalphabetically, inlines each withDatabaseViewInliner, tracks per-viewViewValidateResult. Phase 2 (deploy, if--deployor--deploy-only): deploys only views that passed inlining, with a progress counter reflecting the deployable count.--deployruns COUNT + EXCEPT viaQueryRunnerwith per-query timeout handling;--deploy-onlydeploys and drops immediately (fast SQL error checking without comparison overhead). Supports--filter(exact or SQL LIKE%wildcard via regex),--output-dir(saves inlined SQL),--stop-on-error,--timeout(default 90s). Reopens broken connections between views. Status enum:Pass,PassWithWarnings,Skipped,InlineError,ParseError,DeployError,ValidationFail,Timeout,Exception.
Conditionally compiled (#if !RELEASELIBRARY) alongside the Optimize subsystem.
-
VerifyCommand — System.CommandLine subcommand (
verify) with--connection-string,--filter,--stop-on-error, and--timeout. Accepts a sharedconfigOptionfrom Program.cs; connection string can come from CLI or config. Registered inProgram.csviarootCommand.Add(VerifyCommand.Create(configOption)). -
VerifySession — Auto-detects deployed inlined views by checking for
BeginOriginal/EndOriginalmarkers in raw view definitions. For each candidate: extracts the original SQL from markers, deploys it as[schema].[{name}_Original], runs COUNT + EXCEPT comparisons viaQueryRunner, always drops_Originalin finally. Skips_Inlinedcompanion views when the base view also exists. Supports--filter(reusesValidateSession.BuildFilterRegexandStripBrackets),--stop-on-error(timeouts don't halt), and--timeout(configurable query timeout). Status enum:Pass,Skipped,DeployError,ValidationFail,Timeout,Exception.
Conditionally compiled (#if !RELEASELIBRARY) alongside the Optimize subsystem.
-
AnalyzeCommand — System.CommandLine subcommand (
analyze) with--connection-string,--filter,--days,--min-executions,--top,--generate-script,--from-file, and--output-path. Three-way mode branching:--generate-scriptemits a SQL Server stored procedure for data extraction,--from-fileruns offline analysis from exported JSON, default runs live against a database. Mutual exclusion:--generate-script+--from-file= error,--from-file+--connection-string= error. Accepts a sharedconfigOptionfrom Program.cs; connection string can come from CLI or config. Registered inProgram.csviarootCommand.Add(AnalyzeCommand.Create(configOption)). -
AnalyzeSession — Identifies inlining candidates via three phases. Phase 1 (dependencies): recursive CTE on
sys.sql_expression_dependenciescomputes per-viewNestingDepth,DirectViewRefs,TransitiveViewCount. Phase 2 (Query Store): checkssys.database_query_store_optionsfor enabled state, pulls aggregated stats filtered by--daysand--min-executions, matches view names via compiled regex. Phase 3 (inlined status): checks forBeginOriginal/EndOriginalmarkers. Scoring:3*log2(1+depth)*log2(1+transitiveViews) + 2*log10(1+executions) + 1*log10(1+totalCpuMs+totalLogicalReadsK). Output: two tables (candidates sorted by score, already inlined) plus skipped-views summary. Supports two constructors:AnalyzeSession(connection, wizard)for live mode,AnalyzeSession(wizard)for offline mode. Key methods:Run()(live),RunFromFile()(offline, deserializesAnalyzeDataExportJSON),GenerateScript()(static, returns SP SQL). Shared logic extracted intoMatchQueryStoreToViews()(regex-based view name matching) andScoreAndDisplay()(scoring + table output). Data types:AnalyzeSessionOptions,ViewAnalyzeResult,AnalyzeDataExport(JSON envelope with version, metadata, views, query store stats),AnalyzeDataParameters,AnalyzeDataView,AnalyzeDataQueryStoreRow.
- InlinerConfig — Conditionally compiled (
#if !RELEASELIBRARY). Deserializessqlinliner.jsonviaSystem.Text.Json(camelCase, comments/trailing commas allowed). Properties are all nullable to distinguish "not set" from defaults.TryLoad(explicitPath)checks explicit path then auto-discoverssqlinliner.jsonin CWD.RegisterViews(connection)reads.sqlfiles (paths relative to config directory) and callsconnection.AddViewDefinition().
- ScriptDom AST manipulation: The tool modifies the parsed AST in-place rather than doing string manipulation. SQL is regenerated via
Sql150ScriptGenerator. - Recursive inlining:
DatabaseViewInliner.Inline()recurses into each referenced view before replacing it, so deeply nested view chains are fully flattened. - Column stripping: When
StripUnusedColumnsis enabled, columns are removed by index across all branches of a UNION/EXCEPT/INTERSECT to keep them aligned. - Join stripping: Views contributing only 0-1 columns are candidates for removal when
StripUnusedJoinsis enabled. - Derived table stripping: When
StripUnusedColumnsorStripUnusedJoinsis enabled,DerivedTableStripperruns as a post-processing step after inlining (before flattening). It strips unused columns and LEFT OUTER JOINs inside nestedQueryDerivedTablenodes. Skips derived tables with SELECT *, DISTINCT, TOP, GROUP BY, or HAVING. Only strips LEFT JOINs toQueryDerivedTablenodes (notNamedTableReferencewhich were already evaluated with join hints). Iterates until no more stripping occurs. - Derived table flattening: When
FlattenDerivedTablesis enabled,DerivedTableFlattenerruns as a post-processing step after stripping. It replaces eligibleQueryDerivedTablenodes with their innerFROMtree (single table or JOIN tree), rewrites column references, and merges WHERE clauses. UsesOuterScopeColumnReferenceCollectorthat stops atQueryDerivedTableboundaries to prevent corrupting shared AST object references. ParametersToIgnore: Maps SQL functions (e.g., DATEADD) to parameter indexes that should be excluded from column reference analysis.
Conditionally compiled (#if !RELEASELIBRARY) alongside the Optimize subsystem.
-
ICredentialStore — Interface for platform-specific credential storage. Defines
Store(),Retrieve(),Remove(), andList().StoredCredentialholds username + password.CredentialStoreFactory.Create()returns the platform-appropriate implementation (or null with a warning).CredentialStoreFactory.BuildKey()normalizes server/database into a"server\database"key (lowercase, trimmed). -
WindowsCredentialStore — P/Invoke to
advapi32.dll(Credential Manager). UsesCRED_TYPE_GENERICwith target prefix"sqlinliner:". Username inCREDENTIAL.UserName, password as UTF-16 bytes inCredentialBlob.List()usesCredEnumerate("sqlinliner:*"). -
MacCredentialStore — Wraps macOS
securityCLI (Keychain). Uses a JSON index file (~/.sqlinliner/credentials.json) for username lookup and listing (no passwords in index). -
LinuxCredentialStore — Wraps
secret-toolCLI (libsecret). Uses a JSON index file (~/.sqlinliner/credentials.json) for username lookup and listing. Constructor checks forsecret-toolavailability with install instructions. -
ConnectionStringHelper — Static
Resolve(connectionString, store)method that replaces the duplicatedSqlConnectionStringBuildernormalization blocks across all 5 entry points. SetsApplicationName, injects stored credentials if no explicit credentials or Integrated Security, falls back to Integrated Security. -
CredentialsCommand — System.CommandLine subcommand (
credentials) withadd,list, andremovesub-commands.addprompts for username/password with masked input.listdisplays server/database/username table (never shows passwords).removedeletes from the OS credential store. Does not takeconfigOption.
Tests use NUnit with Shouldly assertions. The standard pattern:
- Create a
DatabaseConnectionwith no DB (parameterless constructor) - Register mock views via
connection.AddViewDefinition(DatabaseConnection.ToObjectName("dbo", "VName"), sqlString) - Construct
DatabaseViewInlinerwith the connection, view SQL, andInlinerOptions.Recommended() - Assert on
inliner.Errors,inliner.Result, andresult.ConvertedSql
- Debug — Standard development build
- Release — Single-file, trimmed, ReadyToRun publish (for CLI distribution)
- ReleaseLibrary — Multi-target library output (net472, netstandard2.0, net8.0, net9.0, net10.0), excludes Program.cs, System.CommandLine, and
Optimize\**
When adding or changing user-facing features, always update both README.md and CLAUDE.md to reflect the changes. README.md is the public documentation for users; CLAUDE.md is the architecture reference for AI-assisted development. Keep them in sync.
This repository includes a STRIDE threat model (STRIDE.md) for security analysis.
When to update STRIDE.md:
- Adding new authentication/authorization mechanisms
- Changing data storage, encryption, or secrets handling
- Adding new external integrations or API endpoints
- Modifying trust boundaries (new external connections, database access)
- After security incidents or penetration test findings
- When addressing security recommendations from the document
How to update:
- Add new threats to the relevant STRIDE category (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege)
- Assess likelihood (Very Low → High) and impact (Low → Critical)
- Document existing mitigations or add recommendations
- Link GitHub issues for unresolved findings
- Update the Review History table
- Update version if using frontmatter
Tracking critical findings:
- Critical/High risk findings should have a linked GitHub issue with
securitylabel - Review STRIDE.md annually or after major releases