diff --git a/README.md b/README.md index e20b0c6..a1efeeb 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,11 @@ A collection of production-ready .NET utility libraries published as the **omy.U - **omy.Utils.Reflection** – reflection extensions such as `PropertyOrFieldInfo` and dynamic delegate invocation. - **omy.Utils.Xml** – attribute-driven XML processing and `XmlDataProcessor` helpers. - **omy.Utils.VirtualMachine** – minimal VM framework with attribute-defined instructions and configurable endianness. -- **omy.Utils.OData** – OData client helpers and related generators. -- **omy.Utils.DependencyInjection** – dependency injection helpers plus source generators. +- **omy.Utils.OData** – OData client helpers and metadata utilities. +- **omy.Utils.OData.Generators** – Roslyn source generator for OData models from EDMX metadata. +- **omy.Utils.DependencyInjection** – attribute-based dependency injection helpers. +- **omy.Utils.DependencyInjection.Generators** – Roslyn source generator that emits DI registrations. +- **omy.Utils.IO.Serialization.Generators** – Roslyn source generator for stream serializers. > Additional project-level READMEs provide deeper details for specialized packages like serialization or generators. diff --git a/Utils.Data/Sql/DeleteStatementParser.cs b/Utils.Data/Sql/DeleteStatementParser.cs index c6c0567..4c7c1b8 100644 --- a/Utils.Data/Sql/DeleteStatementParser.cs +++ b/Utils.Data/Sql/DeleteStatementParser.cs @@ -27,13 +27,32 @@ public SqlDeleteStatement Parse(WithClause? withClause) { parser.ExpectKeyword("DELETE"); - var segments = this.ReadSegments( - DeleteReader, - FromReader, - OutputReader, - WhereReader, - ReturningReader - ); + var segments = new Dictionary + { + [DeleteReader.Clause] = DeleteReader.TryRead( + parser, + FromReader.Clause, + UsingReader.Clause, + OutputReader.Clause, + WhereReader.Clause, + ReturningReader.Clause, + ClauseStart.StatementEnd), + }; + + ReadSegments( + segments, + FromReader, + UsingReader, + OutputReader, + WhereReader, + ReturningReader); + + if (segments.GetValueOrDefault(DeleteReader.Clause) == null + && segments.GetValueOrDefault(OutputReader.Clause) != null + && segments.GetValueOrDefault(FromReader.Clause) is SqlSegment fromSegment) + { + segments[DeleteReader.Clause] = new SqlSegment("Target", fromSegment.Parts, parser.SyntaxOptions); + } return new SqlDeleteStatement( segments.GetValueOrDefault(DeleteReader.Clause), diff --git a/Utils.Data/Sql/InsertStatementParser.cs b/Utils.Data/Sql/InsertStatementParser.cs index b9087fa..c3700d9 100644 --- a/Utils.Data/Sql/InsertStatementParser.cs +++ b/Utils.Data/Sql/InsertStatementParser.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Utils.Collections; namespace Utils.Data.Sql; @@ -29,9 +30,26 @@ public SqlInsertStatement Parse(WithClause? withClause) parser.ExpectKeyword("INTO"); } - var segments = base.ReadSegments( - IntoReader, - OutputReader); + var segments = new Dictionary + { + [IntoReader.Clause] = IntoReader.TryRead( + parser, + OutputReader.Clause, + ValuesReader.Clause, + ClauseStart.Select, + ReturningReader.Clause, + ClauseStart.StatementEnd), + }; + + if (parser.CheckKeyword("OUTPUT")) + { + segments[OutputReader.Clause] = OutputReader.TryRead( + parser, + ValuesReader.Clause, + ClauseStart.Select, + ReturningReader.Clause, + ClauseStart.StatementEnd); + } if (parser.CheckKeyword("VALUES")) { @@ -48,7 +66,7 @@ public SqlInsertStatement Parse(WithClause? withClause) withClause ); } - else if (parser.CheckKeyword("SELECT")) + else if (parser.CheckKeyword("SELECT") || parser.CheckKeyword("WITH")) { var sourceQuery = parser.ParseStatement(); ReadSegments( diff --git a/Utils.Data/Sql/SelectStatementParser.cs b/Utils.Data/Sql/SelectStatementParser.cs index 267a7ec..dd68eb9 100644 --- a/Utils.Data/Sql/SelectStatementParser.cs +++ b/Utils.Data/Sql/SelectStatementParser.cs @@ -28,9 +28,25 @@ public SqlSelectStatement Parse(WithClause? withClause) parser.ExpectKeyword("SELECT"); bool isDistinct = parser.TryConsumeKeyword("DISTINCT"); - var segments = this.ReadSegments( - SelectReader, - FromReader, + var segments = new Dictionary + { + [SelectReader.Clause] = SelectReader.TryRead( + parser, + FromReader.Clause, + WhereReader.Clause, + GroupByReader.Clause, + HavingReader.Clause, + OrderByReader.Clause, + LimitReader.Clause, + OffsetReader.Clause, + ReturningReader.Clause, + SetOperatorReader.Clause, + ClauseStart.StatementEnd), + }; + + ReadSegments( + segments, + FromReader, WhereReader, GroupByReader, HavingReader, @@ -42,14 +58,14 @@ public SqlSelectStatement Parse(WithClause? withClause) return new SqlSelectStatement( segments.GetValueOrDefault(SelectReader.Clause), - segments.GetValueOrDefault(FromReader.Clause), - segments.GetValueOrDefault(WhereReader.Clause), - segments.GetValueOrDefault(GroupByReader.Clause), - segments.GetValueOrDefault(HavingReader.Clause), - segments.GetValueOrDefault(OrderByReader.Clause), - segments.GetValueOrDefault(LimitReader.Clause), - segments.GetValueOrDefault(OffsetReader.Clause), - segments.GetValueOrDefault(SetOperatorReader.Clause), + segments.GetValueOrDefault(FromReader.Clause), + segments.GetValueOrDefault(WhereReader.Clause), + segments.GetValueOrDefault(GroupByReader.Clause), + segments.GetValueOrDefault(HavingReader.Clause), + segments.GetValueOrDefault(OrderByReader.Clause), + segments.GetValueOrDefault(LimitReader.Clause), + segments.GetValueOrDefault(OffsetReader.Clause), + segments.GetValueOrDefault(SetOperatorReader.Clause), withClause, isDistinct); } diff --git a/Utils.Data/Sql/SqlQueryAnalyzer.cs b/Utils.Data/Sql/SqlQueryAnalyzer.cs index 45aa0a2..cb5fc8e 100644 --- a/Utils.Data/Sql/SqlQueryAnalyzer.cs +++ b/Utils.Data/Sql/SqlQueryAnalyzer.cs @@ -24,6 +24,14 @@ public static class SqlQueryAnalyzer public static SqlQuery Parse(string sql, SqlSyntaxOptions? syntaxOptions = null) { ArgumentException.ThrowIfNullOrWhiteSpace(sql); + string trimmedSql = sql.Trim(); + if (trimmedSql.Length > 1 + && trimmedSql[0] == '"' + && trimmedSql[^1] == '"' + && trimmedSql.Count(c => c == '"') == 2) + { + sql = trimmedSql[1..^1]; + } syntaxOptions ??= SqlSyntaxOptions.Default; var parser = SqlParser.Create(sql, syntaxOptions); SqlStatement statement = parser.ParseStatement(); @@ -1110,7 +1118,7 @@ public SqlDeleteStatement(SqlSegment? target, SqlSegment from, SqlSegment? @usin wherePart = where == null ? null : new WherePart(where); partReferenceBindings = [ - new PartReferenceBinding(DeletePartReader.Singleton, part => deletePart ??= part), + new PartReferenceBinding("Target", DeletePartReader.Singleton.PartFactory, part => deletePart ??= part), new PartReferenceBinding(WherePartReader.Singleton, part => wherePart ??= part), ]; } @@ -1201,7 +1209,7 @@ protected override string BuildSql() } builder.Append("DELETE"); - if (Target != null) + if (Target != null && !string.Equals(Target.ToSql(), From.ToSql(), StringComparison.Ordinal)) { builder.Append(' '); builder.Append(Target.ToSql()); diff --git a/Utils.Data/Sql/SqlStatementPartReaders.cs b/Utils.Data/Sql/SqlStatementPartReaders.cs index 29a80d0..5450e44 100644 --- a/Utils.Data/Sql/SqlStatementPartReaders.cs +++ b/Utils.Data/Sql/SqlStatementPartReaders.cs @@ -313,6 +313,7 @@ private WherePartReader() { } /// The parsed WHERE predicate when found; otherwise, null. public SqlSegment? TryRead(SqlParser parser, params IEnumerable clauseTerminators) { + parser.TryConsumeSegmentKeyword("WHERE", out _); var predicateReader = new PredicateReader(parser); return predicateReader.ReadPredicate("Where", [..clauseTerminators]); } @@ -354,6 +355,7 @@ private GroupByPartReader() { } /// The parsed GROUP BY segment when found; otherwise, null. public SqlSegment? TryRead(SqlParser parser, params IEnumerable clauseTerminators) { + parser.TryConsumeSegmentKeyword("GROUP BY", out _); var expressionListReader = new ExpressionListReader(parser); var expressions = expressionListReader.ReadExpressions("GroupByExpr", false, [.. clauseTerminators]); @@ -414,6 +416,7 @@ private HavingPartReader() { } /// The parsed HAVING segment when found; otherwise, null. public SqlSegment? TryRead(SqlParser parser, params IEnumerable clauseTerminators) { + parser.TryConsumeSegmentKeyword("HAVING", out _); var predicateReader = new PredicateReader(parser); return predicateReader.ReadPredicate("Having", [.. clauseTerminators]); } @@ -456,6 +459,7 @@ private OrderByPartReader() { } /// The parsed ORDER BY segment when found; otherwise, null. public SqlSegment? TryRead(SqlParser parser, params IEnumerable clauseTerminators) { + parser.TryConsumeSegmentKeyword("ORDER BY", out _); var expressionListReader = new ExpressionListReader(parser); var expressions = expressionListReader.ReadExpressions( "OrderByExpr", @@ -519,6 +523,7 @@ private LimitPartReader() { } /// The parsed LIMIT segment when found; otherwise, null. public SqlSegment? TryRead(SqlParser parser, params IEnumerable clauseTerminators) { + parser.TryConsumeSegmentKeyword("LIMIT", out _); var tokens = parser.ReadSectionTokens([..clauseTerminators]); return parser.BuildSegment("Limit", tokens); } @@ -561,6 +566,7 @@ private OffsetPartReader() { } /// The parsed OFFSET segment when found; otherwise, null. public SqlSegment? TryRead(SqlParser parser, params IEnumerable clauseTerminators) { + parser.TryConsumeSegmentKeyword("OFFSET", out _); var tokens = parser.ReadSectionTokens( [..clauseTerminators]); return parser.BuildSegment("Offset", tokens); @@ -601,6 +607,7 @@ public ValuesPartReader() { } /// The parsed VALUES segment. public SqlSegment? TryRead(SqlParser parser, params IEnumerable clauseTerminators) { + parser.TryConsumeSegmentKeyword("VALUES", out _); var tokens = parser.ReadSectionTokens([..clauseTerminators]); return parser.BuildSegment("Values", tokens); } @@ -639,6 +646,7 @@ private OutputPartReader() { } /// The parsed OUTPUT segment. public SqlSegment? TryRead(SqlParser parser, params IEnumerable clauseTerminators) { + parser.TryConsumeSegmentKeyword("OUTPUT", out _); var expressionListReader = new ExpressionListReader(parser); var expressions = expressionListReader.ReadExpressions("OutputExpr", true, [..clauseTerminators]); return BuildExpressionListSegment(parser, "Output", expressions); @@ -695,6 +703,7 @@ private ReturningPartReader() { } /// The parsed RETURNING segment. public SqlSegment? TryRead(SqlParser parser, params IEnumerable clauseTerminators) { + parser.TryConsumeSegmentKeyword("RETURNING", out _); var tokens = parser.ReadSectionTokens([..clauseTerminators]); return parser.BuildSegment("Returning", tokens); } @@ -801,6 +810,11 @@ private DeletePartReader() { } tokens.Add(parser.Read()); } + if (tokens.Count == 0) + { + return null; + } + return parser.BuildSegment("Target", tokens); } } @@ -876,6 +890,7 @@ private SetPartReader() { } /// The parsed SET segment. public SqlSegment? TryRead(SqlParser parser, params IEnumerable clauseTerminators) { + parser.TryConsumeSegmentKeyword("SET", out _); var tokens = parser.ReadSectionTokens([..clauseTerminators]); return parser.BuildSegment("Set", tokens); } diff --git a/Utils.Data/Sql/StatementParserBase.cs b/Utils.Data/Sql/StatementParserBase.cs index 8f45561..3e195cc 100644 --- a/Utils.Data/Sql/StatementParserBase.cs +++ b/Utils.Data/Sql/StatementParserBase.cs @@ -12,7 +12,7 @@ abstract internal class StatementParserBase protected readonly SqlParser parser; public static IPartReader SelectReader = SelectPartReader.Singleton; - public static IPartReader SpdateTargetReader => UpdatePartReader.Singleton; + public static IPartReader UpdateTargetReader => UpdatePartReader.Singleton; public static IPartReader DeleteReader => DeletePartReader.Singleton; public static IPartReader FromReader => FromPartReader.Singleton; public static IPartReader UsingReader => UsingPartReader.Singleton; @@ -54,7 +54,8 @@ protected Dictionary ReadSegments(Dictionary r.Clause)); + var clauseTerminators = readersQueue.Select(r => r.Clause).Append(ClauseStart.StatementEnd); + segment = reader.TryRead(parser, clauseTerminators); } } segments[reader.Clause] = segment; diff --git a/Utils.Data/Sql/UpdateStatementParser.cs b/Utils.Data/Sql/UpdateStatementParser.cs index cc59329..b7e9e5c 100644 --- a/Utils.Data/Sql/UpdateStatementParser.cs +++ b/Utils.Data/Sql/UpdateStatementParser.cs @@ -25,22 +25,33 @@ public SqlUpdateStatement Parse(WithClause? withClause) { parser.ExpectKeyword("UPDATE"); - var segments = base.ReadSegments( - SpdateTargetReader, + var segments = new Dictionary + { + [UpdateTargetReader.Clause] = UpdateTargetReader.TryRead( + parser, + SetReader.Clause, + OutputReader.Clause, + FromReader.Clause, + WhereReader.Clause, + ReturningReader.Clause, + ClauseStart.StatementEnd), + }; + + ReadSegments( + segments, SetReader, OutputReader, FromReader, WhereReader, - ReturningReader - ); + ReturningReader); return new SqlUpdateStatement( - segments.GetValueOrDefault(SpdateTargetReader.Clause), + segments.GetValueOrDefault(UpdateTargetReader.Clause), segments.GetValueOrDefault(SetReader.Clause), segments.GetValueOrDefault(FromReader.Clause), segments.GetValueOrDefault(WhereReader.Clause), - segments.GetValueOrDefault(OutputReader.Clause), - segments.GetValueOrDefault(ReturningReader.Clause), + segments.GetValueOrDefault(OutputReader.Clause), + segments.GetValueOrDefault(ReturningReader.Clause), withClause); } } diff --git a/Utils.Net/Net/NntpClient.cs b/Utils.Net/Net/NntpClient.cs index de12159..10852e5 100644 --- a/Utils.Net/Net/NntpClient.cs +++ b/Utils.Net/Net/NntpClient.cs @@ -37,6 +37,22 @@ protected override async Task OnConnect(Stream stream, bool leaveOpen, Cancellat await EnsureCompletionAsync(greeting); } + /// + /// Parses NNTP response lines, treating non-numeric data lines as preliminary responses. + /// + /// Response line from the server. + /// The parsed response. + protected override ServerResponse ParseResponseLine(string line) + { + ServerResponse response = base.ParseResponseLine(line); + if (response.Severity == ResponseSeverity.Unknown && response.Code == line) + { + return new ServerResponse(response.Code, ResponseSeverity.Preliminary, response.Message); + } + + return response; + } + /// /// Selects the specified newsgroup. /// @@ -307,4 +323,3 @@ private async Task> ReadMultilineAsync(CancellationToken c } } } - diff --git a/UtilsTest/OData/ODataContextGeneratorTests.cs b/UtilsTest/OData/ODataContextGeneratorTests.cs index 2aaa47b..f8f8d44 100644 --- a/UtilsTest/OData/ODataContextGeneratorTests.cs +++ b/UtilsTest/OData/ODataContextGeneratorTests.cs @@ -113,6 +113,22 @@ public void GeneratorLoadsCompressedMetadataFromHttp() try { + using (var client = new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All })) + { + try + { + using var response = client.GetAsync(metadataUrl).GetAwaiter().GetResult(); + if (!response.IsSuccessStatusCode) + { + Assert.Inconclusive($"Unable to fetch metadata from '{metadataUrl}'. Status code: {response.StatusCode}."); + } + } + catch (Exception ex) + { + Assert.Inconclusive($"Unable to reach metadata server at '{metadataUrl}'. {ex.GetType().Name}: {ex.Message}"); + } + } + string source = $$""" using Utils.OData; @@ -235,26 +251,29 @@ private static int ReserveEphemeralPort() /// A task that completes once the request has been processed. private static async Task ServeCompressedMetadataAsync(HttpListener listener, string metadataPath) { - try + while (listener.IsListening) { - var context = await listener.GetContextAsync().ConfigureAwait(false); - context.Response.StatusCode = 200; - context.Response.AddHeader("Content-Encoding", "gzip"); - context.Response.ContentType = "application/xml"; - - var responseStream = context.Response.OutputStream; - using (var gzip = new GZipStream(responseStream, CompressionLevel.Fastest, leaveOpen: true)) - using (var fileStream = File.OpenRead(metadataPath)) + try { - await fileStream.CopyToAsync(gzip).ConfigureAwait(false); - } + var context = await listener.GetContextAsync().ConfigureAwait(false); + context.Response.StatusCode = 200; + context.Response.AddHeader("Content-Encoding", "gzip"); + context.Response.ContentType = "application/xml"; + + var responseStream = context.Response.OutputStream; + using (var gzip = new GZipStream(responseStream, CompressionLevel.Fastest, leaveOpen: true)) + using (var fileStream = File.OpenRead(metadataPath)) + { + await fileStream.CopyToAsync(gzip).ConfigureAwait(false); + } - responseStream.Flush(); - context.Response.Close(); - } - catch (HttpListenerException) - { - return; + responseStream.Flush(); + context.Response.Close(); + } + catch (Exception ex) when (ex is HttpListenerException or ObjectDisposedException) + { + return; + } } } diff --git a/docs/getting-started.md b/docs/getting-started.md index 5ec4071..c99a385 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -36,6 +36,7 @@ The libraries target stable target frameworks for consumers: - **net8.0** for most foundational packages - **net9.0** for networking and selected libraries +- **netstandard2.0** for Roslyn source generators Building the repository may require the latest .NET SDK, but consuming the NuGet packages only requires the listed TFMs.