Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ This scenario inlines the view defined in `MyView.sql`, with unused join strippi
When a file path is specified for the main view, the tool uses the exact contents of that file. If a connection
string is also supplied, any views referenced *within* `MyView.sql` are fetched from the database.

### Adding multiple view definitions from a config file
```
sqlinliner -vp "./views/MyView.sql" -vc "./views/views.json"
```

Provide `--views-config` with a JSON file that maps view names to `.sql` files.
All listed views are loaded before inlining so nested local views can be
resolved. Example:

```json
{
"dbo.VPeople": "VPeople.sql",
"dbo.VNestedPeople": "VNestedPeople.sql"
}
```

### Disabling the CREATE OR ALTER wrapper
``sqlinliner -vp "./views/MyView.sql" --generate-create-or-alter false``

Expand All @@ -61,6 +77,7 @@ Two optional parameters can be used to control where the generated SQL and debug

* `--output-path` (`-op`) – write the resulting SQL to the specified file instead of the console.
* `--log-path` (`-lp`) – write warnings, errors and timing information to the given file. When not provided these details are written to the console.
* `--views-config` (`-vc`) – JSON file mapping additional view names to their local SQL files.

## Verifying the generated code

Expand Down
14 changes: 14 additions & 0 deletions src/SqlInliner.Tests/AdditionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,18 @@ public void ViewWithoutAliasGetsDefaultAlias()
Assert.IsNotNull(referenced.Alias);
Assert.AreEqual("VPeople", referenced.Alias!.Value);
}

[Test]
public void ParseObjectNameDefaultsSchema()
{
var name = DatabaseConnection.ParseObjectName("VTest");
Assert.AreEqual("[dbo].[VTest]", name.GetName());
}

[Test]
public void ParseObjectNameWithSchema()
{
var name = DatabaseConnection.ParseObjectName("custom.VTest");
Assert.AreEqual("[custom].[VTest]", name.GetName());
}
Comment on lines +50 to +62
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests for ParseObjectName should include edge cases such as empty strings, whitespace-only strings, strings with leading/trailing dots, and three-part names (e.g., "server.schema.name"). These edge cases are important to verify the method's behavior with invalid or unexpected input.

Copilot uses AI. Check for mistakes.
}
31 changes: 31 additions & 0 deletions src/SqlInliner/DatabaseConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,35 @@ public static SchemaObjectName ToObjectName(string schema, string name)
objectName.Identifiers.Add(new() { Value = name });
return objectName;
}

/// <summary>
/// Parses a two-part view name (<c>schema.name</c>) into a
/// <see cref="SchemaObjectName"/> instance.
/// </summary>
public static SchemaObjectName ParseObjectName(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Object name must not be null, empty, or whitespace.", nameof(name));

var parts = name.Split('.');
if (parts.Length == 1)
{
var trimmedName = parts[0].Trim();
if (string.IsNullOrEmpty(trimmedName))
throw new ArgumentException("Object name must not be empty or whitespace.", nameof(name));
return ToObjectName("dbo", trimmedName);
}
else if (parts.Length == 2)
{
var schema = parts[0].Trim();
var objName = parts[1].Trim();
if (string.IsNullOrEmpty(schema) || string.IsNullOrEmpty(objName))
throw new ArgumentException("Schema and object name must not be empty or whitespace.", nameof(name));
return ToObjectName(schema, objName);
}
else
{
throw new ArgumentException("Object name must be a valid one or two-part name (schema.name or name).", nameof(name));
}
}
}
55 changes: 53 additions & 2 deletions src/SqlInliner/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using System;
using System.CommandLine;
using System.IO;
using System.Text.Json;
using System.Collections.Generic;

namespace SqlInliner;

Expand All @@ -16,6 +18,8 @@ private static int Main(string[] args)
var viewPathOption = new Option<FileInfo>(new[] { "--view-path", "-vp" }, "The path of the view as a .sql file (including create statement)");
var stripUnusedColumnsOption = new Option<bool>(new[] { "--strip-unused-columns", "-suc" }, () => true);
var stripUnusedJoinsOption = new Option<bool>(new[] { "--strip-unused-joins", "-suj" });
var viewsConfigOption = new Option<FileInfo?>(new[] { "--views-config", "-vc" },
"Optional path to a JSON file describing additional view definitions");
var generateCreateOrAlterOption = new Option<bool>("--generate-create-or-alter", () => true);
var outputPathOption = new Option<FileInfo?>(new[] { "--output-path", "-op" }, "Optional path of the file to write the resulting SQL to");
var logPathOption = new Option<FileInfo?>(new[] { "--log-path", "-lp" }, "Optional path of the file to write debug information to");
Expand All @@ -29,11 +33,24 @@ private static int Main(string[] args)
generateCreateOrAlterOption,
outputPathOption,
logPathOption,
viewsConfigOption,
// TODO: DatabaseView.parser (hardcoded to TSql150Parser)
};

rootCommand.SetHandler((connectionString, viewName, viewPath, stripUnusedColumns, stripUnusedJoins, generateCreateOrAlter, outputPath, logPath) =>
rootCommand.SetHandler(context =>
{
var parse = context.ParseResult;

var connectionString = parse.GetValueForOption(connectionStringOption)!;
var viewName = parse.GetValueForOption(viewNameOption);
var viewPath = parse.GetValueForOption(viewPathOption);
var stripUnusedColumns = parse.GetValueForOption(stripUnusedColumnsOption);
var stripUnusedJoins = parse.GetValueForOption(stripUnusedJoinsOption);
var generateCreateOrAlter = parse.GetValueForOption(generateCreateOrAlterOption);
var outputPath = parse.GetValueForOption(outputPathOption);
var logPath = parse.GetValueForOption(logPathOption);
var viewsConfig = parse.GetValueForOption(viewsConfigOption);

var cs = new SqlConnectionStringBuilder(connectionString);
if (!cs.ContainsKey(nameof(cs.ApplicationName)))
{
Expand All @@ -43,6 +60,40 @@ private static int Main(string[] args)

var connection = new DatabaseConnection(new SqlConnection(connectionString));

if (viewsConfig != null)
{
var baseDir = viewsConfig.Directory?.FullName ?? Environment.CurrentDirectory;
var data = File.ReadAllText(viewsConfig.FullName);
Dictionary<string, string> views;
try
{
views = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(data) ?? new();
}
catch (System.Text.Json.JsonException ex)
{
Console.Error.WriteLine($"Error: The config file '{viewsConfig.FullName}' contains invalid JSON: {ex.Message}");
return;
}
foreach (var kvp in views)
{
var path = kvp.Value;
if (!Path.IsPathRooted(path))
path = Path.Combine(baseDir, path);

string sql;
try
{
sql = File.ReadAllText(path);
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error reading view definition file '{path}': {ex.Message}");
return -1;
}
connection.AddViewDefinition(DatabaseConnection.ParseObjectName(kvp.Key), sql);
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The kvp.Key (view name from JSON config) is parsed using ParseObjectName, which expects a two-part name format, but the existing GetViewDefinition at line 81 uses viewName directly as a string. This inconsistency means that views loaded from config must use a different naming format (schema.name) than views specified via --view-name. Consider documenting this requirement clearly or standardizing the view name handling.

Copilot uses AI. Check for mistakes.
}
}

string viewSql;
if (!string.IsNullOrEmpty(viewName))
viewSql = connection.GetViewDefinition(viewName);
Expand Down Expand Up @@ -74,7 +125,7 @@ private static int Main(string[] args)
File.WriteAllText(logPath.FullName, log);
}
//return inliner.Errors.Count > 0 ? -1 : 0;
}, connectionStringOption, viewNameOption, viewPathOption, stripUnusedColumnsOption, stripUnusedJoinsOption, generateCreateOrAlterOption, outputPathOption, logPathOption);
});

return rootCommand.Invoke(args);
}
Expand Down
Loading