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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -498,3 +498,6 @@ Reporting/Nimblesite.Reporting.React/wwwroot/js/h5.meta.js
Reporting/Nimblesite.Reporting.React/wwwroot/js/h5.min.js
Reporting/Nimblesite.Reporting.React/wwwroot/js/h5.meta.min.js
Reporting/Nimblesite.Reporting.React/wwwroot/js/index.html


.deslop-cache/
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
"titleBar.activeForeground": "#F4F7FB",
"titleBar.inactiveBackground": "#070D1F",
"titleBar.inactiveForeground": "#F4F7FBcc"
}
},
"deslop.topOffenders.splitByLanguage": true
}
139 changes: 37 additions & 102 deletions DataProvider/DataProvider/PostgresCli.cs
Original file line number Diff line number Diff line change
Expand Up @@ -595,81 +595,8 @@ string outDir
);
}

if (columns.Count == 0)
{
return new Result<string, SqlError>.Error<string, SqlError>(
new SqlError($"No columns found for table {table.Schema}.{table.Name}")
);
}

var sb = new StringBuilder();
// Use the literal table name (e.g. `gk_user`) instead of
// PascalCasing it. The SQLite.Cli emits methods like
// `Insertgk_userAsync`, and consumers reference them by that
// exact name. PascalCasing here breaks consumer call sites.
var pascalName = table.Name;

// Header
_ = sb.AppendLine("// <auto-generated />");
_ = sb.AppendLine("#nullable enable");
_ = sb.AppendLine();
_ = sb.AppendLine("using Npgsql;");
_ = sb.AppendLine("using Outcome;");
_ = sb.AppendLine("using Nimblesite.Sql.Model;");
_ = sb.AppendLine();
_ = sb.AppendLine("namespace Generated;");
_ = sb.AppendLine();

// Extension class
_ = sb.AppendLine("/// <summary>");
_ = sb.AppendLine(
CultureInfo.InvariantCulture,
$"/// Generated CRUD operations for {table.Name} table."
);
_ = sb.AppendLine("/// </summary>");
_ = sb.AppendLine(
CultureInfo.InvariantCulture,
$"public static class {pascalName}Extensions"
);
_ = sb.AppendLine("{");

// Generate INSERT method
if (table.GenerateInsert)
{
GenerateInsertMethod(sb, table, columns, pascalName);
}

// Generate UPDATE method
if (table.GenerateUpdate)
{
GenerateUpdateMethod(sb, table, columns, pascalName);
}

// Generate DELETE method
if (table.GenerateDelete)
{
GenerateDeleteMethod(sb, table, columns, pascalName);
}

// Generate bulk INSERT method
if (table.GenerateBulkInsert)
{
GenerateBulkInsertMethod(sb, table, columns, pascalName);
}

// Generate bulk UPSERT method
if (table.GenerateBulkUpsert)
{
GenerateBulkUpsertMethod(sb, table, columns, pascalName);
}

_ = sb.AppendLine("}");

var target = Path.Combine(outDir, $"{pascalName}Operations.g.cs");
await File.WriteAllTextAsync(target, sb.ToString()).ConfigureAwait(false);
Console.WriteLine($"✅ Generated {target}");

return new Result<string, SqlError>.Ok<string, SqlError>(sb.ToString());
return await AssembleAndWriteTableOperationsAsync(table, columns, outDir)
.ConfigureAwait(false);
}

private static async Task<Result<string, SqlError>> GenerateTableOperationsAsync(
Expand Down Expand Up @@ -736,6 +663,16 @@ ORDER BY c.ordinal_position
}
}

return await AssembleAndWriteTableOperationsAsync(table, columns, outDir)
.ConfigureAwait(false);
}

private static async Task<Result<string, SqlError>> AssembleAndWriteTableOperationsAsync(
TableConfigItem table,
List<DatabaseColumn> columns,
string outDir
)
{
if (columns.Count == 0)
{
return new Result<string, SqlError>.Error<string, SqlError>(
Expand All @@ -750,6 +687,23 @@ ORDER BY c.ordinal_position
// exact name. PascalCasing here breaks consumer call sites.
var pascalName = table.Name;

AppendExtensionsHeader(sb, table, pascalName);
AppendCrudMethods(sb, table, columns, pascalName);
_ = sb.AppendLine("}");

var target = Path.Combine(outDir, $"{pascalName}Operations.g.cs");
await File.WriteAllTextAsync(target, sb.ToString()).ConfigureAwait(false);
Console.WriteLine($"✅ Generated {target}");

return new Result<string, SqlError>.Ok<string, SqlError>(sb.ToString());
}

private static void AppendExtensionsHeader(
StringBuilder sb,
TableConfigItem table,
string pascalName
)
{
// Header
_ = sb.AppendLine("// <auto-generated />");
_ = sb.AppendLine("#nullable enable");
Expand All @@ -773,7 +727,15 @@ ORDER BY c.ordinal_position
$"public static class {pascalName}Extensions"
);
_ = sb.AppendLine("{");
}

private static void AppendCrudMethods(
StringBuilder sb,
TableConfigItem table,
List<DatabaseColumn> columns,
string pascalName
)
{
// Generate INSERT method
if (table.GenerateInsert)
{
Expand Down Expand Up @@ -803,14 +765,6 @@ ORDER BY c.ordinal_position
{
GenerateBulkUpsertMethod(sb, table, columns, pascalName);
}

_ = sb.AppendLine("}");

var target = Path.Combine(outDir, $"{pascalName}Operations.g.cs");
await File.WriteAllTextAsync(target, sb.ToString()).ConfigureAwait(false);
Console.WriteLine($"✅ Generated {target}");

return new Result<string, SqlError>.Ok<string, SqlError>(sb.ToString());
}

private static void GenerateInsertMethod(
Expand Down Expand Up @@ -2302,25 +2256,6 @@ private static string ToPascalCase(string name)
return sb.ToString();
}

private static string ToCamelCase(string name)
{
if (string.IsNullOrEmpty(name))
return name;

// Preserve existing camel/Pascal input, just lowercase the first
// letter. For snake_case input, fall through to PascalCase then
// lowercase the leading letter.
if (!name.Contains('_', StringComparison.Ordinal))
{
return char.ToLowerInvariant(name[0]) + name[1..];
}

var pascal = ToPascalCase(name);
if (string.IsNullOrEmpty(pascal))
return pascal;
return char.ToLowerInvariant(pascal[0]) + pascal[1..];
}

private static string EscapeForPreprocessor(string message)
{
if (string.IsNullOrEmpty(message))
Expand Down
86 changes: 42 additions & 44 deletions DataProvider/DataProvider/SqliteProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,14 @@ as Result<IReadOnlyList<DatabaseColumn>, SqlError>.Error<
// Also emit an MSBuild-formatted error so IDE Problem Matchers pick it up
Console.Error.WriteLine($"{sqlPath}(1,1): error DP0001: {prettyMeta}");
// Emit a compile-time error file so MSBuild surfaces the exact problem
var errorFile = Path.Combine(outDir.FullName, baseName + ".g.cs");
var content =
$"// Auto-generated due to SQL error in {sqlPath}\n#error {EscapeForPreprocessor(prettyMeta)}\n";
await File.WriteAllTextAsync(errorFile, content).ConfigureAwait(false);
await WriteCompileTimeErrorFileAsync(
outDir,
baseName,
sqlPath,
reason: "SQL error",
message: prettyMeta
)
.ConfigureAwait(false);
hadErrors = true;
continue;
}
Expand Down Expand Up @@ -304,10 +308,14 @@ as Result<IReadOnlyList<DatabaseColumn>, SqlError>.Error<
// Also emit an MSBuild-formatted error so IDE Problem Matchers pick it up
Console.Error.WriteLine($"{sqlPath}(1,1): error DL0002: {prettyGen}");
// Emit a compile-time error file so MSBuild surfaces the exact problem
var errorFile = Path.Combine(outDir.FullName, baseName + ".g.cs");
var content =
$"// Auto-generated due to SQL generation error in {sqlPath}\n#error {EscapeForPreprocessor(prettyGen)}\n";
await File.WriteAllTextAsync(errorFile, content).ConfigureAwait(false);
await WriteCompileTimeErrorFileAsync(
outDir,
baseName,
sqlPath,
reason: "SQL generation error",
message: prettyGen
)
.ConfigureAwait(false);
hadErrors = true;
}
}
Expand All @@ -320,11 +328,15 @@ as Result<IReadOnlyList<DatabaseColumn>, SqlError>.Error<
{
baseName = baseName[..^".generated".Length];
}
var errorFile = Path.Combine(outDir.FullName, baseName + ".g.cs");
var content =
$"// Auto-generated due to unexpected error in {sqlPath}\n#error {EscapeForPreprocessor(ex.Message)}\n";
Console.Error.WriteLine($"{sqlPath}(1,1): error DL0003: {ex.Message}");
await File.WriteAllTextAsync(errorFile, content).ConfigureAwait(false);
await WriteCompileTimeErrorFileAsync(
outDir,
baseName,
sqlPath,
reason: "unexpected error",
message: ex.Message
)
.ConfigureAwait(false);
hadErrors = true;
}
}
Expand Down Expand Up @@ -361,7 +373,10 @@ as Result<IReadOnlyList<DatabaseColumn>, SqlError>.Error<
var notNull = reader.GetInt32(3) == 1; // notnull column
var isPrimaryKey = reader.GetInt32(5) > 0; // pk column

var csharpType = MapSqliteTypeToCSharpType(sqliteType, !notNull);
var csharpType = SqliteTypeMapper.MapSqliteTypeToCSharpType(
sqliteType,
!notNull
);

columns.Add(
new DatabaseColumn
Expand Down Expand Up @@ -464,6 +479,20 @@ is Result<string, SqlError>.Error<string, SqlError> operationsFailure
}
}

private static async Task WriteCompileTimeErrorFileAsync(
DirectoryInfo outDir,
string baseName,
string sqlPath,
string reason,
string message
)
{
var errorFile = Path.Combine(outDir.FullName, baseName + ".g.cs");
var content =
$"// Auto-generated due to {reason} in {sqlPath}\n#error {EscapeForPreprocessor(message)}\n";
await File.WriteAllTextAsync(errorFile, content).ConfigureAwait(false);
}

/// <summary>
/// Generates a source file with the correct using statement for the specified connection type.
/// </summary>
Expand Down Expand Up @@ -615,35 +644,4 @@ private static string MakeConnectionStringAbsolute(string connectionString, stri
var suffix = semicolonIdx >= 0 ? connectionString[semicolonIdx..] : string.Empty;
return $"{prefix}{dataSourcePrefix}{absolutePath}{suffix}";
}

/// <summary>
/// Maps SQLite types to C# types
/// </summary>
private static string MapSqliteTypeToCSharpType(string sqliteType, bool isNullable)
{
var baseType = sqliteType.ToUpperInvariant() switch
{
var t when t.Contains("INT", StringComparison.OrdinalIgnoreCase) => "long",
var t
when t.Contains("REAL", StringComparison.OrdinalIgnoreCase)
|| t.Contains("FLOAT", StringComparison.OrdinalIgnoreCase)
|| t.Contains("DOUBLE", StringComparison.OrdinalIgnoreCase) => "double",
var t
when t.Contains("DECIMAL", StringComparison.OrdinalIgnoreCase)
|| t.Contains("NUMERIC", StringComparison.OrdinalIgnoreCase) => "double",
var t when t.Contains("BOOL", StringComparison.OrdinalIgnoreCase) => "bool",
var t
when t.Contains("DATE", StringComparison.OrdinalIgnoreCase)
|| t.Contains("TIME", StringComparison.OrdinalIgnoreCase) => "string", // SQLite stores dates as text
var t when t.Contains("BLOB", StringComparison.OrdinalIgnoreCase) => "byte[]",
_ => "string",
};

if (isNullable && baseType != "string" && baseType != "byte[]")
{
return baseType + "?";
}

return baseType;
}
}
Loading
Loading