From cfcd9b7bb9c6d7a2ca746e8b106ce04a0ce0772d Mon Sep 17 00:00:00 2001 From: Alex Macocian Date: Tue, 15 Apr 2025 16:26:12 +0200 Subject: [PATCH] Add project files. --- .editorconfig | 139 ++++++++++++ ColumnNameAttributeGenerator.cs | 38 ++++ Constants.cs | 96 ++++++++ Contexts/FieldContext.cs | 31 +++ Contexts/MethodWithSqlStatement.cs | 9 + Contexts/TableContext.cs | 11 + Contexts/TableUniqueContext.cs | 7 + DataTypeMappings.cs | 73 ++++++ DataTypes.cs | 18 ++ ForeignKeyAttributeGenerator.cs | 41 ++++ PrimaryKeyAttributeGenerator.cs | 32 +++ Squealify.csproj | 34 +++ Squealify.sln | 25 +++ SyntaxParsers.cs | 285 ++++++++++++++++++++++++ Table/BasicQueryMethodGenerator.cs | 264 ++++++++++++++++++++++ Table/ConversionPropertyGenerator.cs | 29 +++ Table/CreateParameterMethodGenerator.cs | 21 ++ Table/TableCreateMethodGenerator.cs | 89 ++++++++ TableAttributeGenerator.cs | 41 ++++ TableContextGenerator.cs | 171 ++++++++++++++ UniqueAttributeGenerator.cs | 50 +++++ VarcharAttributeGenerator.cs | 42 ++++ 22 files changed, 1546 insertions(+) create mode 100644 .editorconfig create mode 100644 ColumnNameAttributeGenerator.cs create mode 100644 Constants.cs create mode 100644 Contexts/FieldContext.cs create mode 100644 Contexts/MethodWithSqlStatement.cs create mode 100644 Contexts/TableContext.cs create mode 100644 Contexts/TableUniqueContext.cs create mode 100644 DataTypeMappings.cs create mode 100644 DataTypes.cs create mode 100644 ForeignKeyAttributeGenerator.cs create mode 100644 PrimaryKeyAttributeGenerator.cs create mode 100644 Squealify.csproj create mode 100644 Squealify.sln create mode 100644 SyntaxParsers.cs create mode 100644 Table/BasicQueryMethodGenerator.cs create mode 100644 Table/ConversionPropertyGenerator.cs create mode 100644 Table/CreateParameterMethodGenerator.cs create mode 100644 Table/TableCreateMethodGenerator.cs create mode 100644 TableAttributeGenerator.cs create mode 100644 TableContextGenerator.cs create mode 100644 UniqueAttributeGenerator.cs create mode 100644 VarcharAttributeGenerator.cs 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); + } +} +