diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..37b1101 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,139 @@ +[*.cs] + +# CA2254: Template should be a static expression +dotnet_diagnostic.CA2254.severity = none +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:error +csharp_prefer_simple_using_statement = true:error +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = file_scoped:error +csharp_style_prefer_method_group_conversion = true:suggestion +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_indent_braces = false +insert_final_newline = true +csharp_space_around_binary_operators = before_and_after +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:error +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_deconstructed_variable_declaration = true:error +csharp_style_unused_value_assignment_preference = discard_variable:error +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_prefer_static_local_function = true:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:suggestion +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_prefer_parameter_null_checking = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = false:silent +csharp_style_var_elsewhere = false:silent +csharp_style_prefer_top_level_statements = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_primary_constructors = true:suggestion +csharp_prefer_system_threading_lock = true:suggestion + +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = error +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = error +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = error +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:error +dotnet_style_prefer_auto_properties = true:error +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_readonly_field = true:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_allow_multiple_blank_lines_experimental = true:silent +dotnet_style_allow_statement_immediately_after_block_experimental = false:suggestion +dotnet_code_quality_unused_parameters = all:error +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion +dotnet_style_qualification_for_field = true:error +dotnet_style_qualification_for_property = true:error +dotnet_style_qualification_for_method = true:error +dotnet_style_qualification_for_event = true:error +dotnet_diagnostic.CA1000.severity = error + +[*.vb] +dotnet_diagnostic.CA1047.severity = error \ No newline at end of file diff --git a/ColumnNameAttributeGenerator.cs b/ColumnNameAttributeGenerator.cs new file mode 100644 index 0000000..c02763e --- /dev/null +++ b/ColumnNameAttributeGenerator.cs @@ -0,0 +1,38 @@ +using Microsoft.CodeAnalysis; +using Sybil; +using System; + +namespace Squealify; + +[Generator(LanguageNames.CSharp)] +public sealed class ColumnNameAttributeGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(GenerateAttribute); + } + + private static void GenerateAttribute(IncrementalGeneratorPostInitializationContext context) + { + var builder = SyntaxBuilder.CreateCompilationUnit() + .WithNamespace(SyntaxBuilder.CreateNamespace(Constants.Namespace) + .WithClass(SyntaxBuilder.CreateClass(Constants.ColumnNameAttributeName) + .WithBaseClass(nameof(Attribute)) + .WithModifiers($"{Constants.Public} {Constants.Sealed}") + .WithConstructor(SyntaxBuilder.CreateConstructor(Constants.ColumnNameAttributeName) + .WithModifier(Constants.Public) + .WithParameter(Constants.StringType, Constants.ColumnNameArgumentName) + .WithBody($"this.{Constants.ColumnNameProperty} = {Constants.ColumnNameArgumentName};")) + .WithAttribute(SyntaxBuilder.CreateAttribute("AttributeUsage") + .WithArgument(AttributeTargets.Property) + .WithArgument("Inherited", false) + .WithArgument("AllowMultiple", false)) + .WithProperty(SyntaxBuilder.CreateProperty(Constants.StringType, Constants.ColumnNameProperty) + .WithModifier(Constants.Public) + .WithAccessor(SyntaxBuilder.CreateGetter())))); + + var syntax = builder.Build(); + var source = syntax.ToFullString(); + context.AddSource($"{Constants.ColumnNameAttributeName}.g", source); + } +} diff --git a/Constants.cs b/Constants.cs new file mode 100644 index 0000000..d93b0c8 --- /dev/null +++ b/Constants.cs @@ -0,0 +1,96 @@ +namespace Squealify; +public static class Constants +{ + public const string Private = "private"; + public const string Protected = "protected"; + public const string Readonly = "readonly"; + public const string Public = "public"; + public const string Sealed = "sealed"; + public const string Partial = "partial"; + public const string Abstract = "abstract"; + public const string Async = "async"; + public const string Params = "params"; + + public const string StringType = "string"; + public const string ByteType = "byte"; + public const string ValueTaskType = "ValueTask"; + public const string CancellationTokenType = "CancellationToken"; + public const string StringArrayType = "string[]"; + public const string DbConnectionType = "DbConnection"; + public const string DbParameterType = "DbParameter"; + public const string DbCommandType = "DbCommand"; + public const string FuncType = "Func"; + public const string NullableObject = "object?"; + + public const string PrimaryKey = "PRIMARY KEY"; + public const string ForeignKey = "FOREIGN KEY"; + public const string References = "REFERENCES"; + public const string NotNull = "NOT NULL"; + public const string Unique = "UNIQUE"; + + public const string ArrayName = "Array"; + + public const string Namespace = "Squealify"; + + public const string UsingSystemDataCommon = "System.Data.Common"; + + public const string CancellationTokenArgument = "cancellationToken"; + + public const string DbSetName = "DbSet"; + public const string DbSetTypeName = "T"; + + public const string TableAttributeName = "TableAttribute"; + public const string TableAttributeShortName = "Table"; + public const string TableNameProperty = "Name"; + public const string TableNameArgumentName = "name"; + + public const string PrimaryKeyAttributeName = "PrimaryKeyAttribute"; + public const string PrimaryKeyAttributeShortName = "PrimaryKey"; + + public const string ForeignKeyAttributeName = "ForeignKeyAttribute"; + public const string ForeignKeyAttributeShortName = "ForeignKey"; + public const string ForeignKeyReferenceTableProperty = "ReferenceTable"; + public const string ForeignKeyReferenceFieldProperty = "ReferenceField"; + public const string ForeignKeyArgumentReferenceTableName = "referenceTable"; + public const string ForeignKeyArgumentReferenceFieldName = "field"; + + public const string ColumnNameAttributeName = "ColumnNameAttribute"; + public const string ColumnNameAttributeShortName = "ColumnName"; + public const string ColumnNameProperty = "Name"; + public const string ColumnNameArgumentName = "name"; + + public const string VarcharAttributeName = "VarcharAttribute"; + public const string VarcharAttributeShortName = "Varchar"; + public const string LengthProperty = "Length"; + public const string LengthArgumentName = "length"; + + public const string ColumnUniqueAttributeName = "ColumnUniqueAttribute"; + public const string ColumnUniqueAttributeShortName = "ColumnUnique"; + + public const string TableUniqueAttributeName = "TableUniqueAttribute"; + public const string TableUniqueAttributeShortName = "TableUnique"; + public const string TableUniqueProperty = "Uniques"; + public const string TableUniqueArgumentName = "uniqueFields"; + + public const string DbConnectionArgumentName = "connection"; + public const string DbConnectionPropertyName = "Connection"; + + public const string CreateTableStatement = "CREATE TABLE"; + public const string CreateTableIfNotExistsStatement = "CREATE TABLE IF NOT EXISTS"; + public const string CreateTableMethod = "CreateTable"; + public const string CreateTableIfNotExistsMethod = "CreateTableIfNotExists"; + + public const string CreateParameterMethod = "CreateParameter"; + public const string CreateParameterCommandArgument = "command"; + public const string CreateParameterNameArgument = "name"; + public const string CreateParameterValueArgument = "value"; + public const string ParameterTypeName = "TParameter"; + + public const string DboArgumentName = "dbo"; + public const string PrimaryKeyArgumentName = "primaryKey"; + public const string InsertMethodName = "Insert"; + public const string UpsertMethodName = "Upsert"; + public const string UpdateMethodName = "Update"; + public const string DeleteMethodName = "Delete"; + public const string FindMethodName = "Find"; +} diff --git a/Contexts/FieldContext.cs b/Contexts/FieldContext.cs new file mode 100644 index 0000000..905a871 --- /dev/null +++ b/Contexts/FieldContext.cs @@ -0,0 +1,31 @@ +namespace Squealify.Contexts; + +public readonly struct FieldContext( + string name, + string propertyName, + string propertyType, + DataTypes type, + bool isUnique, + bool isPrimaryKey, + bool isForeignKey, + string? referenceTable, + string? referenceField, + byte? varcharLength, + bool nullable, + bool isEnum, + bool requiresConversion) +{ + public readonly string Name = name; + public readonly string PropertyName = propertyName; + public readonly string PropertyType = propertyType; + public readonly DataTypes Type = type; + public readonly bool IsUnique = isUnique; + public readonly bool IsPrimaryKey = isPrimaryKey; + public readonly bool IsForeignKey = isForeignKey; + public readonly bool IsNullable = nullable; + public readonly bool IsEnum = isEnum; + public readonly string? ReferenceTable = referenceTable; + public readonly string? ReferenceField = referenceField; + public readonly byte? VarcharLength = varcharLength; + public readonly bool RequiresConversion = requiresConversion; +} diff --git a/Contexts/MethodWithSqlStatement.cs b/Contexts/MethodWithSqlStatement.cs new file mode 100644 index 0000000..8def806 --- /dev/null +++ b/Contexts/MethodWithSqlStatement.cs @@ -0,0 +1,9 @@ +using Sybil; + +namespace Squealify.Contexts; +public readonly struct MethodWithSqlStatement(MethodBuilder methodBuilder, string placeholder, string commandText) +{ + public readonly MethodBuilder MethodBuilder = methodBuilder; + public readonly string Placeholder = placeholder; + public readonly string CommandText = commandText; +} diff --git a/Contexts/TableContext.cs b/Contexts/TableContext.cs new file mode 100644 index 0000000..dcd3d4d --- /dev/null +++ b/Contexts/TableContext.cs @@ -0,0 +1,11 @@ +using System.Collections.Immutable; + +namespace Squealify.Contexts; +public readonly struct TableContext(string tableName, string dboType, FieldContext primaryKey, ImmutableArray createFieldContexts, ImmutableArray tableUniques) +{ + public readonly string TableName = tableName; + public readonly string DboType = dboType; + public readonly FieldContext PrimaryKey = primaryKey; + public readonly ImmutableArray Fields = createFieldContexts; + public readonly ImmutableArray TableUniques = tableUniques; +} diff --git a/Contexts/TableUniqueContext.cs b/Contexts/TableUniqueContext.cs new file mode 100644 index 0000000..a07ae0c --- /dev/null +++ b/Contexts/TableUniqueContext.cs @@ -0,0 +1,7 @@ +using System.Collections.Immutable; + +namespace Squealify.Contexts; +public readonly struct TableUniqueContext(ImmutableArray columnNames) +{ + public readonly ImmutableArray ColumnNames = columnNames; +} diff --git a/DataTypeMappings.cs b/DataTypeMappings.cs new file mode 100644 index 0000000..a5e69a8 --- /dev/null +++ b/DataTypeMappings.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; + +namespace Squealify; +public static class DataTypeMappings +{ + public static readonly Dictionary StringToDataType = new() { + { "int", DataTypes.INTEGER }, + { "system.int32", DataTypes.INTEGER }, + { "uint", DataTypes.INTEGER }, + { "system.uint32", DataTypes.INTEGER }, + + { "short", DataTypes.SMALLINT }, + { "system.int16", DataTypes.SMALLINT }, + { "ushort", DataTypes.SMALLINT }, + { "system.uint16", DataTypes.SMALLINT }, + + { "long", DataTypes.BIGINT }, + { "system.int64", DataTypes.BIGINT }, + { "ulong", DataTypes.BIGINT }, + { "system.uint64", DataTypes.BIGINT }, + + { "float", DataTypes.REAL }, + { "system.single", DataTypes.REAL }, + + { "double", DataTypes.DOUBLE_PRECISION }, + { "system.double", DataTypes.DOUBLE_PRECISION }, + + { "decimal", DataTypes.DECIMAL }, + { "system.decimal", DataTypes.DECIMAL }, + + { "string", DataTypes.TEXT }, + { "system.string", DataTypes.TEXT }, + + { "char", DataTypes.CHAR }, + { "system.char", DataTypes.CHAR }, + + { "bool", DataTypes.BOOLEAN }, + { "system.boolean", DataTypes.BOOLEAN }, + { "boolean", DataTypes.BOOLEAN }, + + { "datetime", DataTypes.TIMESTAMP }, + { "system.datetime", DataTypes.TIMESTAMP }, + { "datetimeoffset", DataTypes.TIMESTAMP }, + { "system.datetimeoffset", DataTypes.TIMESTAMP }, + + { "dateonly", DataTypes.DATE }, + { "system.dateonly", DataTypes.DATE }, + + { "timeonly", DataTypes.TIME }, + { "system.timeonly", DataTypes.TIME }, + + { "byte[]", DataTypes.BLOB }, + { "system.byte[]", DataTypes.BLOB }, + }; + + public static readonly Dictionary DataTypeToString = new() + { + { DataTypes.SMALLINT, "short" }, + { DataTypes.INTEGER, "int" }, + { DataTypes.BIGINT, "long" }, + { DataTypes.REAL, "float" }, + { DataTypes.DOUBLE_PRECISION, "double" }, + { DataTypes.DECIMAL, "decimal" }, + { DataTypes.CHAR, "char" }, + { DataTypes.VARCHAR, "string" }, + { DataTypes.TEXT, "string" }, + { DataTypes.DATE, "DateOnly" }, + { DataTypes.TIME, "TimeOnly" }, + { DataTypes.TIMESTAMP, "DateTimeOffset" }, + { DataTypes.BOOLEAN, "bool" }, + { DataTypes.BLOB, "byte[]" } + }; +} diff --git a/DataTypes.cs b/DataTypes.cs new file mode 100644 index 0000000..61e3467 --- /dev/null +++ b/DataTypes.cs @@ -0,0 +1,18 @@ +namespace Squealify; +public enum DataTypes +{ + SMALLINT, + INTEGER, + BIGINT, + REAL, + DOUBLE_PRECISION, + DECIMAL, + CHAR, + VARCHAR, + TEXT, + DATE, + TIME, + TIMESTAMP, + BOOLEAN, + BLOB +} diff --git a/ForeignKeyAttributeGenerator.cs b/ForeignKeyAttributeGenerator.cs new file mode 100644 index 0000000..c1e90ae --- /dev/null +++ b/ForeignKeyAttributeGenerator.cs @@ -0,0 +1,41 @@ +using Microsoft.CodeAnalysis; +using Sybil; +using System; + +namespace Squealify; + +[Generator(LanguageNames.CSharp)] +public sealed class ForeignKeyAttributeGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(GenerateAttribute); + } + + private static void GenerateAttribute(IncrementalGeneratorPostInitializationContext context) + { + var builder = SyntaxBuilder.CreateCompilationUnit() + .WithNamespace(SyntaxBuilder.CreateNamespace(Constants.Namespace) + .WithClass(SyntaxBuilder.CreateClass(Constants.ForeignKeyAttributeName) + .WithBaseClass(nameof(Attribute)) + .WithModifiers($"{Constants.Public} {Constants.Sealed}") + .WithConstructor(SyntaxBuilder.CreateConstructor(Constants.ForeignKeyAttributeName) + .WithModifier(Constants.Public) + .WithParameter(Constants.StringType, Constants.ForeignKeyArgumentReferenceTableName) + .WithParameter(Constants.StringType, Constants.ForeignKeyArgumentReferenceFieldName) + .WithBody($"this.{Constants.ForeignKeyReferenceTableProperty} = {Constants.ForeignKeyArgumentReferenceTableName}; this.{Constants.ForeignKeyReferenceFieldProperty} = {Constants.ForeignKeyArgumentReferenceFieldName};")) + .WithAttribute(SyntaxBuilder.CreateAttribute("AttributeUsage") + .WithArgument(AttributeTargets.Property) + .WithArgument("Inherited", false) + .WithArgument("AllowMultiple", false)) + .WithProperty(SyntaxBuilder.CreateProperty(Constants.StringType, Constants.ForeignKeyReferenceTableProperty) + .WithModifier(Constants.Public) + .WithAccessor(SyntaxBuilder.CreateGetter())) + .WithProperty(SyntaxBuilder.CreateProperty(Constants.StringType, Constants.ForeignKeyReferenceFieldProperty) + .WithModifier(Constants.Public) + .WithAccessor(SyntaxBuilder.CreateGetter())))); + var syntax = builder.Build(); + var source = syntax.ToFullString(); + context.AddSource($"{Constants.ForeignKeyAttributeName}.g.cs", source); + } +} diff --git a/PrimaryKeyAttributeGenerator.cs b/PrimaryKeyAttributeGenerator.cs new file mode 100644 index 0000000..14145db --- /dev/null +++ b/PrimaryKeyAttributeGenerator.cs @@ -0,0 +1,32 @@ +using Microsoft.CodeAnalysis; +using Sybil; +using System; + +namespace Squealify; + +[Generator(LanguageNames.CSharp)] +public sealed class PrimaryKeyAttributeGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(GenerateAttribute); + } + + private static void GenerateAttribute(IncrementalGeneratorPostInitializationContext context) + { + var builder = SyntaxBuilder.CreateCompilationUnit() + .WithNamespace(SyntaxBuilder.CreateNamespace(Constants.Namespace) + .WithClass(SyntaxBuilder.CreateClass(Constants.PrimaryKeyAttributeName) + .WithBaseClass(nameof(Attribute)) + .WithModifiers($"{Constants.Public} {Constants.Sealed}") + .WithConstructor(SyntaxBuilder.CreateConstructor(Constants.PrimaryKeyAttributeName) + .WithModifier(Constants.Public)) + .WithAttribute(SyntaxBuilder.CreateAttribute("AttributeUsage") + .WithArgument(AttributeTargets.Property) + .WithArgument("Inherited", false) + .WithArgument("AllowMultiple", false)))); + var syntax = builder.Build(); + var source = syntax.ToFullString(); + context.AddSource($"{Constants.PrimaryKeyAttributeName}.g.cs", source); + } +} diff --git a/Squealify.csproj b/Squealify.csproj new file mode 100644 index 0000000..1bad9d1 --- /dev/null +++ b/Squealify.csproj @@ -0,0 +1,34 @@ + + + + netstandard2.0 + 0.1.75 + latest + true + Alexandru Macocian + Source generators for ADO.Net that generate simple sql statements from classes. + true + true + true + true + true + Enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + \ No newline at end of file diff --git a/Squealify.sln b/Squealify.sln new file mode 100644 index 0000000..c46b2dd --- /dev/null +++ b/Squealify.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35919.96 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squealify", "Squealify.csproj", "{E0610C08-63C3-4577-9B66-9B64200C57E3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E0610C08-63C3-4577-9B66-9B64200C57E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0610C08-63C3-4577-9B66-9B64200C57E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0610C08-63C3-4577-9B66-9B64200C57E3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0610C08-63C3-4577-9B66-9B64200C57E3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6013BB17-9CF4-4C52-8B8B-090413AA87AB} + EndGlobalSection +EndGlobal diff --git a/SyntaxParsers.cs b/SyntaxParsers.cs new file mode 100644 index 0000000..d0ab8c8 --- /dev/null +++ b/SyntaxParsers.cs @@ -0,0 +1,285 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Squealify.Contexts; +using System.Collections.Immutable; +using System.Linq; +using System.Text; + +namespace Squealify; +public static class SyntaxParsers +{ + public static (string ConvertToName, string ConvertFromName) GetConversionNames(string propertyType, string convertedType) + { + var convertedTypeSafeNameBuilder = new StringBuilder(convertedType); + convertedTypeSafeNameBuilder[0] = char.ToUpper(convertedTypeSafeNameBuilder[0]); + if (convertedType.Contains("[]")) + { + convertedTypeSafeNameBuilder.Replace("[]", Constants.ArrayName); + } + + convertedTypeSafeNameBuilder.Replace("?", ""); + var convertedTypeSafeName = convertedTypeSafeNameBuilder.ToString(); + var propertyTypeSafeName = propertyType.Replace("?", "").Replace("[]", Constants.ArrayName); + + var convertToName = $"{propertyTypeSafeName}To{convertedTypeSafeName}Converter"; + var convertFromName = $"{convertedTypeSafeName}To{propertyTypeSafeName}Converter"; + return (convertToName, convertFromName); + } + + public static string GetTableName(ClassDeclarationSyntax classDeclarationSyntax) + { + var tableAttribute = classDeclarationSyntax.AttributeLists + .SelectMany(n => n.Attributes) + .FirstOrDefault(a => a.Name.ToString() == Constants.TableAttributeName || + a.Name.ToString() == Constants.TableAttributeShortName); + + if (tableAttribute is null) + { + return classDeclarationSyntax.Identifier.ValueText; + } + + if (tableAttribute.ArgumentList is not null && tableAttribute.ArgumentList.Arguments.Count > 0) + { + foreach (var arg in tableAttribute.ArgumentList.Arguments) + { + if (arg.NameEquals != null && arg.NameEquals.Name.Identifier.ValueText == Constants.TableNameArgumentName) + { + return arg.Expression.ToString().Trim('"'); + } + } + + return tableAttribute.ArgumentList.Arguments[0].Expression.ToString().Trim('"'); + } + + return classDeclarationSyntax.Identifier.ValueText; + } + + public static ImmutableArray GetTableUniques(ClassDeclarationSyntax classDeclarationSyntax) + { + var uniquesBuilder = ImmutableArray.CreateBuilder(); + var classAttributes = classDeclarationSyntax.AttributeLists.SelectMany(a => a.Attributes); + foreach (var attribute in classAttributes) + { + var name = attribute.Name.ToString(); + if (name == Constants.TableUniqueAttributeName || name == Constants.TableUniqueAttributeShortName) + { + if (attribute.ArgumentList is not null) + { + var columnNamesBuilder = ImmutableArray.CreateBuilder(); + foreach (var arg in attribute.ArgumentList.Arguments) + { + if (arg.Expression is InvocationExpressionSyntax invocation && + invocation.Expression.ToString() == "nameof") + { + // e.g. nameof(PropertyName) + // The argument's expression typically is an IdentifierNameSyntax + if (invocation.ArgumentList.Arguments.Count == 1) + { + var nameofArg = invocation.ArgumentList.Arguments[0].Expression.ToString(); + columnNamesBuilder.Add(nameofArg); + } + } + else + { + var text = arg.Expression.ToString().Trim('"'); + columnNamesBuilder.Add(text); + } + } + + uniquesBuilder.Add(new TableUniqueContext(columnNamesBuilder.ToImmutable())); + } + } + } + + return uniquesBuilder.ToImmutable(); + } + + public static ImmutableArray GetFieldsFromClass(ClassDeclarationSyntax classDeclarationSyntax, Compilation compilation) + { + var fieldsBuilder = ImmutableArray.CreateBuilder(); + foreach (var member in classDeclarationSyntax.Members) + { + if (member is PropertyDeclarationSyntax propertyDeclaration) + { + // Ensure the property is public. + if (!propertyDeclaration.Modifiers.Any(SyntaxKind.PublicKeyword)) + { + continue; + } + + // Check that the property has an accessor list containing a getter and a setter or init. + if (propertyDeclaration.AccessorList is not { } accessorList) + { + continue; + } + + bool hasGetter = false; + bool hasSetOrInit = false; + foreach (var accessor in accessorList.Accessors) + { + if (accessor.IsKind(SyntaxKind.GetAccessorDeclaration)) + { + hasGetter = true; + } + else if (accessor.IsKind(SyntaxKind.SetAccessorDeclaration) || accessor.IsKind(SyntaxKind.InitAccessorDeclaration)) + { + hasSetOrInit = true; + } + } + + if (!hasGetter || !hasSetOrInit) + { + continue; + } + + // Default field name to property name. + var fieldName = propertyDeclaration.Identifier.ValueText; + var originalName = fieldName; + var isUnique = false; + var isPrimaryKey = false; + var isForeignKey = false; + string? referenceTable = null; + string? referenceField = null; + byte? varCharSize = null; + + // Process attributes. + foreach (var attributeList in propertyDeclaration.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { + var attrName = attribute.Name.ToString(); + // Check for Unique attribute. + if (attrName == Constants.ColumnUniqueAttributeName || attrName == Constants.ColumnUniqueAttributeShortName) + { + isUnique = true; + continue; + } + + // Check for PrimaryKey attribute. + if (attrName == Constants.PrimaryKeyAttributeName || attrName == Constants.PrimaryKeyAttributeShortName) + { + isPrimaryKey = true; + continue; + } + + // Check for Varchar attribute. + if (attrName == Constants.VarcharAttributeName || attrName == Constants.VarcharAttributeShortName) + { + if (attribute.ArgumentList is not null && attribute.ArgumentList.Arguments.Count >= 1) + { + // Extract first two arguments as referenceTable and referenceField. + varCharSize = byte.Parse(attribute.ArgumentList.Arguments[0].ToString().Trim('"')); + continue; + } + } + + // Check for ForeignKey attribute. + if (attrName == Constants.ForeignKeyAttributeName || attrName == Constants.ForeignKeyAttributeShortName) + { + isForeignKey = true; + if (attribute.ArgumentList is not null && attribute.ArgumentList.Arguments.Count >= 2) + { + // Extract first two arguments as referenceTable and referenceField. + referenceTable = attribute.ArgumentList.Arguments[0].ToString().Trim('"'); + referenceField = attribute.ArgumentList.Arguments[1].ToString().Trim('"'); + continue; + } + } + + // Check for Name attribute. + if (attrName == Constants.ColumnNameAttributeName || attrName == Constants.ColumnNameAttributeShortName) + { + if (attribute.ArgumentList is not null && attribute.ArgumentList.Arguments.Count >= 1) + { + fieldName = attribute.ArgumentList.Arguments[0].ToString().Trim('"'); + continue; + } + } + } + } + + var semanticModel = compilation.GetSemanticModel(propertyDeclaration.SyntaxTree); + var typeSymbol = semanticModel.GetTypeInfo(propertyDeclaration.Type).Type; + var propertyTypeString = propertyDeclaration.Type.ToString(); + var nullable = propertyTypeString.EndsWith("?"); + (var isEnum, var enumUnderlyingType) = IsEnumType(typeSymbol); + (var propertyMapping, var requiresConversion) = MapPropertyType(propertyTypeString); + requiresConversion = !isEnum && requiresConversion; + var dataType = isEnum + ? enumUnderlyingType ?? DataTypes.INTEGER + : varCharSize is null + ? propertyMapping + : DataTypes.VARCHAR; + fieldsBuilder.Add(new FieldContext( + fieldName, + originalName, + propertyTypeString, + dataType, + isUnique, + isPrimaryKey, + isForeignKey, + referenceTable, + referenceField, + varCharSize, + nullable, + isEnum, + requiresConversion)); + } + } + + return fieldsBuilder.ToImmutable(); + } + + private static (bool, DataTypes? UnderlyingType) IsEnumType(ITypeSymbol? typeSymbol) + { + if (typeSymbol is null) + { + return (false, default); + } + + if (typeSymbol.TypeKind == TypeKind.Enum && typeSymbol is INamedTypeSymbol enumType) + { + return (true, enumType.EnumUnderlyingType?.ToString().ToLowerInvariant() switch + { + "byte" or "system.byte" => DataTypes.SMALLINT, + "int" or "system.int32" or "uint" or "system.uint32" => DataTypes.INTEGER, + "short" or "system.int16" or "ushort" or "system.uint16" => DataTypes.SMALLINT, + "long" or "system.int64" or "ulong" or "system.uint64" => DataTypes.BIGINT, + _ => DataTypes.INTEGER + }); + } + + if (typeSymbol is INamedTypeSymbol namedType && + namedType.IsGenericType && + namedType.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T && + namedType.TypeArguments.Length == 1 && + namedType.TypeArguments[0].TypeKind == TypeKind.Enum) + { + var enumTypeSymbol = (INamedTypeSymbol)namedType.TypeArguments[0]; + return (true, enumTypeSymbol.EnumUnderlyingType?.ToString().ToLowerInvariant() switch + { + "byte" or "system.byte" => DataTypes.SMALLINT, + "int" or "system.int32" or "uint" or "system.uint32" => DataTypes.INTEGER, + "short" or "system.int16" or "ushort" or "system.uint16" => DataTypes.SMALLINT, + "long" or "system.int64" or "ulong" or "system.uint64" => DataTypes.BIGINT, + _ => DataTypes.INTEGER + }); + } + + return (false, default); + } + + private static (DataTypes Type, bool RequiresConversion) MapPropertyType(string propertyType) + { + // Normalize the type name for matching. + var typeName = propertyType.Trim(); + var normalizedName = typeName.ToLowerInvariant().TrimEnd('?'); + if (DataTypeMappings.StringToDataType.TryGetValue(normalizedName, out var dataType)) + { + return (dataType, false); + } + + return (DataTypes.TEXT, true); + } +} diff --git a/Table/BasicQueryMethodGenerator.cs b/Table/BasicQueryMethodGenerator.cs new file mode 100644 index 0000000..8e08458 --- /dev/null +++ b/Table/BasicQueryMethodGenerator.cs @@ -0,0 +1,264 @@ +using Squealify.Contexts; +using Sybil; +using System; +using System.Linq; +using System.Text; + +namespace Squealify.Table; +public static class BasicQueryMethodGenerator +{ + public static MethodWithSqlStatement CreateInsertStatement(TableContext context) + { + var placeHolder = $"Insert{context.DboType}"; + var sql = new StringBuilder(); + sql.AppendLine() + .AppendLine($"\t\t\tINSERT INTO {context.TableName}") + .AppendLine($"\t\t\t\t({string.Join(", ", context.Fields.Select(f => f.Name))})") + .AppendLine("\t\t\tVALUES") + .AppendLine($"\t\t\t\t({string.Join(", ", context.Fields.Select(GetPlaceholderName))})"); + + var methodBuilder = SyntaxBuilder.CreateMethod(Constants.ValueTaskType, Constants.InsertMethodName) + .WithModifiers($"{Constants.Public} {Constants.Async}") + .WithParameter(context.DboType, Constants.DboArgumentName) + .WithParameter(Constants.CancellationTokenType, Constants.CancellationTokenArgument) + .WithBody(GenerateFullBody(placeHolder, context)); + + return new MethodWithSqlStatement(methodBuilder, placeHolder, sql.ToString()); + } + + public static MethodWithSqlStatement CreateUpsertStatement(TableContext context) + { + var placeHolder = $"Upsert{context.DboType}"; + var sql = new StringBuilder(); + sql.AppendLine() + .AppendLine($"\t\t\tINSERT INTO {context.TableName}") + .AppendLine($"\t\t\t\t({string.Join(", ", context.Fields.Select(f => f.Name))})") + .AppendLine("\t\t\tVALUES") + .AppendLine($"\t\t\t\t({string.Join(", ", context.Fields.Select(GetPlaceholderName))})") + .AppendLine($"\t\t\tON CONFLICT({context.PrimaryKey.Name}) DO UPDATE SET"); + + for(var i = 0; i < context.Fields.Length; i++) + { + var fieldContext = context.Fields[i]; + if (fieldContext.Name == context.PrimaryKey.Name) + { + continue; + } + + sql.Append($"\t\t\t\t{fieldContext.Name} = excluded.{fieldContext.Name}"); + if (i < context.Fields.Length - 1) + { + sql.AppendLine(","); + } + } + + sql.AppendLine(";"); + var methodBuilder = SyntaxBuilder.CreateMethod(Constants.ValueTaskType, Constants.UpsertMethodName) + .WithModifiers($"{Constants.Public} {Constants.Async}") + .WithParameter(context.DboType, Constants.DboArgumentName) + .WithParameter(Constants.CancellationTokenType, Constants.CancellationTokenArgument) + .WithBody(GenerateFullBody(placeHolder, context)); + + return new MethodWithSqlStatement(methodBuilder, placeHolder, sql.ToString()); + } + + public static MethodWithSqlStatement CreateUpdateStatement(TableContext context) + { + var placeHolder = $"Update{context.DboType}"; + var sql = new StringBuilder(); + sql.AppendLine() + .AppendLine($"\t\t\tUPDATE {context.TableName}") + .AppendLine("\t\t\tSET"); + + for (var i = 0; i < context.Fields.Length; i++) + { + var fieldContext = context.Fields[i]; + if (fieldContext.Name == context.PrimaryKey.Name) + { + continue; + } + + sql.Append($"\t\t\t\t{fieldContext.Name} = {GetPlaceholderName(fieldContext)}"); + if (i < context.Fields.Length - 1) + { + sql.AppendLine(","); + } + } + + sql.AppendLine() + .AppendLine("\t\t\tWHERE") + .AppendLine($"\t\t\t\t{context.PrimaryKey.Name} = {GetPlaceholderName(context.PrimaryKey)};"); + + var methodBuilder = SyntaxBuilder.CreateMethod(Constants.ValueTaskType, Constants.UpdateMethodName) + .WithModifiers($"{Constants.Public} {Constants.Async}") + .WithParameter(context.DboType, Constants.DboArgumentName) + .WithParameter(Constants.CancellationTokenType, Constants.CancellationTokenArgument) + .WithBody(GenerateFullBody(placeHolder, context)); + + return new MethodWithSqlStatement(methodBuilder, placeHolder, sql.ToString()); + } + + public static MethodWithSqlStatement CreateDeleteStatement(TableContext context) + { + var placeHolder = $"Delete{context.DboType}"; + var sql = new StringBuilder(); + sql.AppendLine() + .AppendLine($"\t\t\tDELETE FROM {context.TableName}") + .AppendLine($"\t\t\tWHERE {context.PrimaryKey.Name} = {GetPlaceholderName(context.PrimaryKey)};"); + + var bodyBuilder = new StringBuilder(@$" +using var command = this.{Constants.DbConnectionPropertyName}.CreateCommand(); +command.CommandText = @""{placeHolder}""; +"); + + if (!DataTypeMappings.DataTypeToString.TryGetValue(context.PrimaryKey.Type, out var convertedType)) + { + throw new InvalidOperationException($"Unable to find conversion for {context.PrimaryKey.Type}"); + } + + (var convertToName, _) = SyntaxParsers.GetConversionNames(context.PrimaryKey.PropertyType, convertedType); + var valueProvider = context.PrimaryKey.RequiresConversion + ? $"this.{convertToName}({Constants.PrimaryKeyArgumentName})" + : $"{Constants.PrimaryKeyArgumentName}"; + + bodyBuilder.AppendLine($"command.Parameters.Add(this.{Constants.CreateParameterMethod}(command, \"{GetPlaceholderName(context.PrimaryKey)}\", {valueProvider}));"); + bodyBuilder.AppendLine("await command.ExecuteNonQueryAsync(cancellationToken);"); + + var methodBuilder = SyntaxBuilder.CreateMethod(Constants.ValueTaskType, Constants.DeleteMethodName) + .WithModifiers($"{Constants.Public} {Constants.Async}") + .WithParameter(context.PrimaryKey.PropertyType, Constants.PrimaryKeyArgumentName) + .WithParameter(Constants.CancellationTokenType, Constants.CancellationTokenArgument) + .WithBody(bodyBuilder.ToString()); + + return new MethodWithSqlStatement(methodBuilder, placeHolder, sql.ToString()); + } + + public static MethodWithSqlStatement CreateFindStatement(TableContext context) + { + var placeHolder = $"Find{context.DboType}"; + var sql = new StringBuilder(); + sql.AppendLine() + .AppendLine($"\t\t\tSELECT * FROM {context.TableName}") + .AppendLine($"\t\t\tWHERE {context.PrimaryKey.Name} = {GetPlaceholderName(context.PrimaryKey)};"); + + var bodyBuilder = new StringBuilder(@$" +using var command = this.{Constants.DbConnectionPropertyName}.CreateCommand(); +command.CommandText = @""{placeHolder}""; +"); + + if (!DataTypeMappings.DataTypeToString.TryGetValue(context.PrimaryKey.Type, out var convertedType)) + { + throw new InvalidOperationException($"Unable to find conversion for {context.PrimaryKey.Type}"); + } + + (var convertToName, _) = SyntaxParsers.GetConversionNames(context.PrimaryKey.PropertyType, convertedType); + var valueProvider = context.PrimaryKey.RequiresConversion + ? $"this.{convertToName}({Constants.PrimaryKeyArgumentName})" + : $"{Constants.PrimaryKeyArgumentName}"; + + bodyBuilder.AppendLine($"command.Parameters.Add(this.{Constants.CreateParameterMethod}(command, \"{GetPlaceholderName(context.PrimaryKey)}\", {valueProvider}));"); + bodyBuilder.AppendLine($"using var reader = await command.ExecuteReaderAsync({Constants.CancellationTokenArgument});"); + bodyBuilder.AppendLine($"if (!await reader.ReadAsync({Constants.CancellationTokenArgument})) return default;"); + bodyBuilder.AppendLine($"return new {context.DboType}"); + bodyBuilder.AppendLine("{"); + foreach((var index, var fieldContext) in context.Fields.Select((f, i) => (i, f))) + { + if (!DataTypeMappings.DataTypeToString.TryGetValue(fieldContext.Type, out var convertedFieldType)) + { + continue; + } + + (_, var convertFromName) = SyntaxParsers.GetConversionNames(fieldContext.PropertyType, convertedFieldType); + + var fieldValueProvider = fieldContext.RequiresConversion + ? $"this.{convertFromName}({GetReaderMethod("reader", fieldContext)}({index}))" + : $"{GetReaderMethod("reader", fieldContext)}({index})"; + + var nullCheckGuardedProvider = fieldContext.IsNullable + ? $"await reader.IsDBNullAsync({index}, {Constants.CancellationTokenArgument}) ? default : {fieldValueProvider}" + : fieldValueProvider; + + bodyBuilder.Append($"{fieldContext.PropertyName} = {nullCheckGuardedProvider}"); + if (index < context.Fields.Length - 1) + { + bodyBuilder.AppendLine(","); + } + } + bodyBuilder + .AppendLine() + .AppendLine("};"); + + var methodBuilder = SyntaxBuilder.CreateMethod($"{Constants.ValueTaskType}<{context.DboType}>", Constants.FindMethodName) + .WithModifiers($"{Constants.Public} {Constants.Async}") + .WithParameter(context.PrimaryKey.PropertyType, Constants.PrimaryKeyArgumentName) + .WithParameter(Constants.CancellationTokenType, Constants.CancellationTokenArgument) + .WithBody(bodyBuilder.ToString()); + + return new MethodWithSqlStatement(methodBuilder, placeHolder, sql.ToString()); + } + + private static string GenerateFullBody(string placeHolder, TableContext context) + { + var bodyBuilder = new StringBuilder(@$" +using var command = this.{Constants.DbConnectionPropertyName}.CreateCommand(); +command.CommandText = @""{placeHolder}""; +"); + + foreach (var fieldContext in context.Fields) + { + var propertyType = fieldContext.PropertyType; + var dataType = fieldContext.Type; + if (!DataTypeMappings.DataTypeToString.TryGetValue(dataType, out var convertedType)) + { + continue; + } + + (var convertToName, _) = SyntaxParsers.GetConversionNames(propertyType, convertedType); + var valueProvider = fieldContext.RequiresConversion + ? $"this.{convertToName}({Constants.DboArgumentName}.{fieldContext.PropertyName})" + : $"{Constants.DboArgumentName}.{fieldContext.PropertyName}"; + + bodyBuilder.AppendLine($"command.Parameters.Add(this.{Constants.CreateParameterMethod}(command, \"{GetPlaceholderName(fieldContext)}\", {valueProvider}));"); + } + + bodyBuilder.AppendLine("await command.ExecuteNonQueryAsync(cancellationToken);"); + return bodyBuilder.ToString(); + } + + private static string GetPlaceholderName(FieldContext context) => $"@{context.Name}"; + + private static string GetReaderMethod(string readerVariableName, FieldContext context) + { + var readMethod = context.Type switch + { + DataTypes.SMALLINT => "GetInt16", + DataTypes.INTEGER => "GetInt32", + DataTypes.BIGINT => "GetInt64", + DataTypes.REAL => "GetFloat", + DataTypes.DOUBLE_PRECISION => "GetDouble", + DataTypes.DECIMAL => "GetDecimal", + DataTypes.CHAR => "GetChar", + DataTypes.VARCHAR => "GetString", + DataTypes.TEXT => "GetString", + DataTypes.DATE => "GetDateTime", + DataTypes.TIME => "GetDateTime", + DataTypes.TIMESTAMP => "GetDateTime", + DataTypes.BOOLEAN => "GetBoolean", + DataTypes.BLOB => "GetBytes", + _ => throw new NotSupportedException($"Data type {context.Type} is not supported.") + }; + + var readerMethod = context.IsEnum + ? $"({context.PropertyType}){readerVariableName}.{readMethod}" + : $"{readerVariableName}.{readMethod}"; + + if(context.PropertyType.ToLower().TrimEnd('?') is "datetimeoffset" or "system.datetimeoffset") + { + return context.IsNullable + ? $"(DateTimeOffset?){readerMethod}" + : $"(DateTimeOffset){readerMethod}"; + } + + return readerMethod; + } +} diff --git a/Table/ConversionPropertyGenerator.cs b/Table/ConversionPropertyGenerator.cs new file mode 100644 index 0000000..2d954db --- /dev/null +++ b/Table/ConversionPropertyGenerator.cs @@ -0,0 +1,29 @@ +using Squealify.Contexts; +using Sybil; + +namespace Squealify.Table; +public static class ConversionPropertyGenerator +{ + public static (PropertyBuilder ConvertTo, PropertyBuilder ConvertFrom)? GenerateConversionProperties(FieldContext fieldContext) + { + var propertyType = fieldContext.PropertyType; + var dataType = fieldContext.Type; + if (!DataTypeMappings.DataTypeToString.TryGetValue(dataType, out var convertedType)) + { + return default; + } + + (var convertToName, var convertFromName) = SyntaxParsers.GetConversionNames(propertyType, convertedType); + var convertTo = SyntaxBuilder.CreateProperty($"{Constants.FuncType}<{propertyType}, {convertedType}>", convertToName) + .WithModifier(Constants.Protected) + .WithModifier(Constants.Abstract) + .WithAccessor(SyntaxBuilder.CreateGetter()); + + var convertFrom = SyntaxBuilder.CreateProperty($"{Constants.FuncType}<{convertedType}, {propertyType}>", convertFromName) + .WithModifier(Constants.Protected) + .WithModifier(Constants.Abstract) + .WithAccessor(SyntaxBuilder.CreateGetter()); + + return (convertTo, convertFrom); + } +} diff --git a/Table/CreateParameterMethodGenerator.cs b/Table/CreateParameterMethodGenerator.cs new file mode 100644 index 0000000..23e492c --- /dev/null +++ b/Table/CreateParameterMethodGenerator.cs @@ -0,0 +1,21 @@ +using Sybil; + +namespace Squealify.Table; +public static class CreateParameterMethodGenerator +{ + public static MethodBuilder CreateParameterMethod() + { + var methodBuilder = SyntaxBuilder.CreateMethod(Constants.DbParameterType, Constants.CreateParameterMethod) + .WithModifier(Constants.Protected) + .WithParameter(Constants.DbCommandType, Constants.CreateParameterCommandArgument) + .WithParameter(Constants.StringType, Constants.CreateParameterNameArgument) + .WithParameter(Constants.NullableObject, Constants.CreateParameterValueArgument) + .WithBody($@" +var parameter = {Constants.CreateParameterCommandArgument}.CreateParameter(); +parameter.ParameterName = {Constants.CreateParameterNameArgument}; +parameter.Value = {Constants.CreateParameterValueArgument} ?? DBNull.Value; +return parameter;"); + + return methodBuilder; + } +} diff --git a/Table/TableCreateMethodGenerator.cs b/Table/TableCreateMethodGenerator.cs new file mode 100644 index 0000000..c68889a --- /dev/null +++ b/Table/TableCreateMethodGenerator.cs @@ -0,0 +1,89 @@ +using Squealify.Contexts; +using Sybil; +using System.Text; + +namespace Squealify.Table; +public static class TableCreateMethodGenerator +{ + public static (MethodWithSqlStatement Create, MethodWithSqlStatement CreateIfNotExists) GenerateCreateMethods(TableContext ctx) + { + (var createMethod, var createMethodPlaceholder, var createMethodBody) = + GenerateCreate(Constants.CreateTableStatement, Constants.CreateTableMethod, ctx); + (var createIfNotExistsMethod, var createIfNotExistsPlacholder, var createIfNotExistsBody) = + GenerateCreate(Constants.CreateTableIfNotExistsStatement, Constants.CreateTableIfNotExistsMethod, ctx); + + return ( + new MethodWithSqlStatement(createMethod, createMethodPlaceholder, createMethodBody), + new MethodWithSqlStatement(createIfNotExistsMethod, createIfNotExistsPlacholder, createIfNotExistsBody)); + } + + /// + /// Generates the create method. Generates the CommandText body but puts it inside a placeholder, to avoid auto-formatting by Roslyn. + /// + private static (MethodBuilder Method, string Placeholder, string CommandText) GenerateCreate(string createStatement, string createName, TableContext ctx) + { + var commandPlaceholder = $"__COMMAND_TEXT_{createName}__"; + var commandTextBuilder = new StringBuilder(); + commandTextBuilder.AppendLine().AppendLine($"\t\t\t{createStatement} {ctx.TableName} ("); + + //Generate normal, primary and unique fields + for (var i = 0; i < ctx.Fields.Length; i++) + { + var field = ctx.Fields[i]; + commandTextBuilder.Append($"\t\t\t\t{field.Name} {field.Type.ToString().Replace('_', ' ')}"); + if (field.IsPrimaryKey) + { + commandTextBuilder.Append(' ').Append(Constants.PrimaryKey); + } + + if (field.IsUnique) + { + commandTextBuilder.Append(' ').Append(Constants.Unique); + } + + if (!field.IsPrimaryKey && + !field.IsNullable) + { + commandTextBuilder.Append(' ').Append(Constants.NotNull); + } + + if (field.IsForeignKey && + field.ReferenceTable is string referenceTable && + field.ReferenceField is string referenceField) + { + commandTextBuilder.Append(' ').Append($"{Constants.References} {referenceTable}({referenceField})"); + } + + if (i < ctx.Fields.Length - 1) + { + commandTextBuilder.AppendLine(","); + } + } + + if (ctx.TableUniques.Length > 0) + { + commandTextBuilder.Append(',').AppendLine(); + } + + for (var i = 0; i < ctx.TableUniques.Length; i++) + { + var tableUnique = ctx.TableUniques[i]; + commandTextBuilder.Append($"\t\t\t\t{Constants.Unique} ({string.Join(", ", tableUnique.ColumnNames)})"); + if (i < ctx.TableUniques.Length - 1) + { + commandTextBuilder.AppendLine(","); + } + } + + commandTextBuilder.AppendLine(");"); + var methodBuilder = SyntaxBuilder.CreateMethod(Constants.ValueTaskType, createName) + .WithModifier(Constants.Public) + .WithModifier(Constants.Async) + .WithParameter(Constants.CancellationTokenType, Constants.CancellationTokenArgument) + .WithBody($@" + using var command = this.{Constants.DbConnectionPropertyName}.CreateCommand(); + command.CommandText = @""{commandPlaceholder}""; + await command.ExecuteNonQueryAsync({Constants.CancellationTokenArgument});"); + return (methodBuilder, commandPlaceholder, commandTextBuilder.ToString()); + } +} diff --git a/TableAttributeGenerator.cs b/TableAttributeGenerator.cs new file mode 100644 index 0000000..54bf39d --- /dev/null +++ b/TableAttributeGenerator.cs @@ -0,0 +1,41 @@ +using Microsoft.CodeAnalysis; +using Sybil; +using System; + +namespace Squealify; + +[Generator(LanguageNames.CSharp)] +public sealed class TableAttributeGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(GenerateAttribute); + } + + private static void GenerateAttribute(IncrementalGeneratorPostInitializationContext context) + { + var builder = SyntaxBuilder.CreateCompilationUnit() + .WithNamespace(SyntaxBuilder.CreateNamespace(Constants.Namespace) + .WithClass(SyntaxBuilder.CreateClass(Constants.TableAttributeName) + .WithBaseClass(nameof(Attribute)) + .WithModifiers($"{Constants.Public} {Constants.Sealed}") + .WithConstructor(SyntaxBuilder.CreateConstructor(Constants.TableAttributeName) + .WithModifier(Constants.Public) + .WithBody($"this.{Constants.TableNameProperty} = string.Empty;")) + .WithConstructor(SyntaxBuilder.CreateConstructor(Constants.TableAttributeName) + .WithModifier(Constants.Public) + .WithParameter(Constants.StringType, Constants.TableNameArgumentName) + .WithBody($"this.{Constants.TableNameProperty} = {Constants.TableNameArgumentName};")) + .WithAttribute(SyntaxBuilder.CreateAttribute("AttributeUsage") + .WithArgument(AttributeTargets.Class) + .WithArgument("Inherited", false) + .WithArgument("AllowMultiple", false)) + .WithProperty(SyntaxBuilder.CreateProperty(Constants.StringType, Constants.TableNameProperty) + .WithModifier(Constants.Public) + .WithAccessor(SyntaxBuilder.CreateGetter())))); + + var syntax = builder.Build(); + var source = syntax.ToFullString(); + context.AddSource($"{Constants.TableAttributeName}.g.cs", source); + } +} diff --git a/TableContextGenerator.cs b/TableContextGenerator.cs new file mode 100644 index 0000000..a3297e5 --- /dev/null +++ b/TableContextGenerator.cs @@ -0,0 +1,171 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Squealify.Contexts; +using Squealify.Table; +using Sybil; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Squealify; + +[Generator(LanguageNames.CSharp)] +public sealed class TableContextGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var classDeclarations = context.SyntaxProvider.CreateSyntaxProvider( + predicate: static (s, _) => s is ClassDeclarationSyntax, + transform: static (ctx, _) => GetFilteredClassDeclarationSyntax(ctx)).Where(static c => c is not null); + var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect()); + context.RegisterSourceOutput(compilationAndClasses, (sourceProductionContext, tuple) => Execute(tuple.Left, tuple.Right, sourceProductionContext)); + } + + private static ClassDeclarationSyntax? GetFilteredClassDeclarationSyntax(GeneratorSyntaxContext context) + { + var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node; + if (classDeclarationSyntax.AttributeLists + .SelectMany(l => l.Attributes) + .OfType() + .Any(s => s.Name.ToString() is Constants.TableAttributeName or Constants.TableAttributeShortName)) + { + return classDeclarationSyntax; + } + + return default; + } + + private static void Execute(Compilation compilation, ImmutableArray classDeclarationSyntaxes, SourceProductionContext sourceProductionContext) + { + if (classDeclarationSyntaxes.IsDefaultOrEmpty) + { + return; + } + + var maybeLanguageVersion = (compilation.SyntaxTrees.FirstOrDefault()?.Options as CSharpParseOptions)?.LanguageVersion; + if (!maybeLanguageVersion.HasValue) + { + return; + } + + var languageVersion = maybeLanguageVersion.Value; + foreach(var classDeclarationSyntax in classDeclarationSyntaxes) + { + if (classDeclarationSyntax is null) + { + continue; + } + + ExecuteClass(classDeclarationSyntax, compilation, sourceProductionContext, languageVersion); + } + } + + private static void ExecuteClass(ClassDeclarationSyntax classDeclarationSyntax, Compilation compilation, SourceProductionContext sourceProductionContext, LanguageVersion languageVersion) + { + var contextClassName = $"{classDeclarationSyntax.Identifier.ValueText}TableContextBase"; + var usingsSet = new HashSet + { + Constants.UsingSystemDataCommon + }; + + // Add the namespace of the class declaration (if any) + var namespaceNode = classDeclarationSyntax.Ancestors().FirstOrDefault(n => n is NamespaceDeclarationSyntax or FileScopedNamespaceDeclarationSyntax); + if (namespaceNode is BaseNamespaceDeclarationSyntax baseNamespace) + { + var namespaceName = baseNamespace.Name.ToString(); + if (!string.IsNullOrEmpty(namespaceName)) + { + usingsSet.Add(namespaceName); + } + } + + // Collect all "top-level" using directives from the same file + var root = classDeclarationSyntax.SyntaxTree.GetRoot() as CompilationUnitSyntax; + if (root is not null) + { + foreach (var usingDirective in root.Usings) + { + var nameText = usingDirective.Name?.ToString(); + if (nameText is not null) + { + usingsSet.Add(nameText); + } + } + } + + var builder = SyntaxBuilder.CreateCompilationUnit(); + foreach(var u in usingsSet) + { + builder.WithUsing(u); + } + + var namespaceBuilder = languageVersion >= LanguageVersion.CSharp10 ? SyntaxBuilder.CreateFileScopedNamespace(Constants.Namespace) : SyntaxBuilder.CreateNamespace(Constants.Namespace); + builder.WithNamespace(namespaceBuilder); + var classBuilder = SyntaxBuilder.CreateClass(contextClassName) + .WithModifiers($"{Constants.Public} {Constants.Abstract}") + .WithProperty(SyntaxBuilder.CreateProperty(Constants.DbConnectionType, Constants.DbConnectionPropertyName) + .WithModifier(Constants.Protected) + .WithAccessor(SyntaxBuilder.CreateGetter())) + .WithConstructor(SyntaxBuilder.CreateConstructor(contextClassName) + .WithParameter(Constants.DbConnectionType, Constants.DbConnectionArgumentName) + .WithModifier(Constants.Public) + .WithBody($"this.{Constants.DbConnectionPropertyName} = {Constants.DbConnectionArgumentName};")); + namespaceBuilder.WithClass(classBuilder); + + var ctx = GetTableContext(classDeclarationSyntax, compilation); + + // Generate create methods + (var createMethod, var createIfNotExistsMethod) = TableCreateMethodGenerator.GenerateCreateMethods(ctx); + var insertMethod = BasicQueryMethodGenerator.CreateInsertStatement(ctx); + var upsertMethod = BasicQueryMethodGenerator.CreateUpsertStatement(ctx); + var updateMethod = BasicQueryMethodGenerator.CreateUpdateStatement(ctx); + var deleteMethod = BasicQueryMethodGenerator.CreateDeleteStatement(ctx); + var findMethod = BasicQueryMethodGenerator.CreateFindStatement(ctx); + classBuilder + .WithMethod(createMethod.MethodBuilder) + .WithMethod(createIfNotExistsMethod.MethodBuilder) + .WithMethod(insertMethod.MethodBuilder) + .WithMethod(upsertMethod.MethodBuilder) + .WithMethod(updateMethod.MethodBuilder) + .WithMethod(deleteMethod.MethodBuilder) + .WithMethod(findMethod.MethodBuilder) + .WithMethod(CreateParameterMethodGenerator.CreateParameterMethod()); + + // Generate conversion properties + var conversionProperties = ctx.Fields + .Where(f => f.RequiresConversion) + .GroupBy(f => f.PropertyType) + .Select(g => g.First()) + .Select(ConversionPropertyGenerator.GenerateConversionProperties) + .OfType<(PropertyBuilder ConvertTo, PropertyBuilder ConvertFrom)>(); + + foreach((var convertTo, var convertFrom) in conversionProperties) + { + classBuilder.WithProperty(convertTo); + classBuilder.WithProperty(convertFrom); + } + + // Replace placeholders with formatted command text + var syntax = builder.Build(); + var source = syntax.ToFullString() + .Replace(createMethod.Placeholder, createMethod.CommandText) + .Replace(createIfNotExistsMethod.Placeholder, createIfNotExistsMethod.CommandText) + .Replace(insertMethod.Placeholder, insertMethod.CommandText) + .Replace(upsertMethod.Placeholder, upsertMethod.CommandText) + .Replace(updateMethod.Placeholder, updateMethod.CommandText) + .Replace(deleteMethod.Placeholder, deleteMethod.CommandText) + .Replace(findMethod.Placeholder, findMethod.CommandText); + + sourceProductionContext.AddSource($"{contextClassName}.g.cs", source); + } + + private static TableContext GetTableContext(ClassDeclarationSyntax classDeclarationSyntax, Compilation compilation) + { + var fields = SyntaxParsers.GetFieldsFromClass(classDeclarationSyntax, compilation); + var primaryKeyField = fields.First(f => f.IsPrimaryKey); + var tableName = SyntaxParsers.GetTableName(classDeclarationSyntax); + var tableUnqiues = SyntaxParsers.GetTableUniques(classDeclarationSyntax); + return new TableContext(tableName, classDeclarationSyntax.Identifier.ToString(), primaryKeyField, fields, tableUnqiues); + } +} diff --git a/UniqueAttributeGenerator.cs b/UniqueAttributeGenerator.cs new file mode 100644 index 0000000..913af88 --- /dev/null +++ b/UniqueAttributeGenerator.cs @@ -0,0 +1,50 @@ +using Microsoft.CodeAnalysis; +using Sybil; +using System; + +namespace Squealify; + +[Generator(LanguageNames.CSharp)] +public sealed class UniqueAttributeGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(GenerateAttribute); + } + + private static void GenerateAttribute(IncrementalGeneratorPostInitializationContext context) + { + var columnUniqueBuilder = SyntaxBuilder.CreateCompilationUnit() + .WithNamespace(SyntaxBuilder.CreateNamespace(Constants.Namespace) + .WithClass(SyntaxBuilder.CreateClass(Constants.ColumnUniqueAttributeName) + .WithBaseClass(nameof(Attribute)) + .WithModifiers($"{Constants.Public} {Constants.Sealed}") + .WithConstructor(SyntaxBuilder.CreateConstructor(Constants.ColumnUniqueAttributeName) + .WithModifier(Constants.Public)) + .WithAttribute(SyntaxBuilder.CreateAttribute("AttributeUsage") + .WithArgument(AttributeTargets.Property) + .WithArgument("Inherited", false) + .WithArgument("AllowMultiple", false)))); + var columnUniqueSource = columnUniqueBuilder.Build().ToFullString(); + context.AddSource($"{Constants.ColumnUniqueAttributeName}.g.cs", columnUniqueSource); + + var tableUniqueBuilder = SyntaxBuilder.CreateCompilationUnit() + .WithNamespace(SyntaxBuilder.CreateNamespace(Constants.Namespace) + .WithClass(SyntaxBuilder.CreateClass(Constants.TableUniqueAttributeName) + .WithBaseClass(nameof(Attribute)) + .WithModifiers($"{Constants.Public} {Constants.Sealed}") + .WithConstructor(SyntaxBuilder.CreateConstructor(Constants.TableUniqueAttributeName) + .WithModifier(Constants.Public) + .WithParameter($"{Constants.Params} {Constants.StringArrayType}", Constants.TableUniqueArgumentName) + .WithBody($"this.{Constants.TableUniqueProperty} = {Constants.TableUniqueArgumentName};")) + .WithAttribute(SyntaxBuilder.CreateAttribute("AttributeUsage") + .WithArgument(AttributeTargets.Class) + .WithArgument("Inherited", false) + .WithArgument("AllowMultiple", true)) + .WithProperty(SyntaxBuilder.CreateProperty(Constants.StringArrayType, Constants.TableUniqueProperty) + .WithModifiers(Constants.Public) + .WithAccessor(SyntaxBuilder.CreateGetter())))); + var tableUniqueSource = tableUniqueBuilder.Build().ToFullString(); + context.AddSource($"{Constants.TableUniqueAttributeName}.g.cs", tableUniqueSource); + } +} diff --git a/VarcharAttributeGenerator.cs b/VarcharAttributeGenerator.cs new file mode 100644 index 0000000..112dc5a --- /dev/null +++ b/VarcharAttributeGenerator.cs @@ -0,0 +1,42 @@ +using Microsoft.CodeAnalysis; +using Sybil; +using System; + +namespace Squealify; + +[Generator(LanguageNames.CSharp)] +public sealed class VarcharAttributeGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(GenerateAttribute); + } + + private static void GenerateAttribute(IncrementalGeneratorPostInitializationContext context) + { + var builder = SyntaxBuilder.CreateCompilationUnit() + .WithNamespace(SyntaxBuilder.CreateNamespace(Constants.Namespace) + .WithClass(SyntaxBuilder.CreateClass(Constants.VarcharAttributeName) + .WithBaseClass(nameof(Attribute)) + .WithModifiers($"{Constants.Public} {Constants.Sealed}") + .WithConstructor(SyntaxBuilder.CreateConstructor(Constants.VarcharAttributeName) + .WithModifier(Constants.Public) + .WithBody($"this.{Constants.LengthProperty} = 255;")) + .WithConstructor(SyntaxBuilder.CreateConstructor(Constants.VarcharAttributeName) + .WithModifier(Constants.Public) + .WithParameter(Constants.ByteType, Constants.LengthArgumentName) + .WithBody($"this.{Constants.LengthProperty} = {Constants.LengthArgumentName};")) + .WithAttribute(SyntaxBuilder.CreateAttribute("AttributeUsage") + .WithArgument(AttributeTargets.Property) + .WithArgument("Inherited", false) + .WithArgument("AllowMultiple", false)) + .WithProperty(SyntaxBuilder.CreateProperty(Constants.ByteType, Constants.LengthProperty) + .WithModifier(Constants.Public) + .WithAccessor(SyntaxBuilder.CreateGetter())))); + + var syntax = builder.Build(); + var source = syntax.ToFullString(); + context.AddSource($"{Constants.VarcharAttributeName}.g.cs", source); + } +} +