Skip to content

Commit 2319e56

Browse files
authored
Merge pull request #8 from thiagoluga/feat/sql-csv-local
feat(sql-csv-local): primeiro end-to-end (SQL keyset → CSV → Local)
2 parents 56ed801 + 80d653a commit 2319e56

29 files changed

Lines changed: 1432 additions & 5 deletions

NeoReports-Decisoes.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,17 @@ Removido da v1 vs. Cap. 16: `IRetryPolicy`, `IExceptionClassifier`, `IAuthProvid
203203
**Decisão.** `ReportBuilder<TRow>` é genérico **apenas** sobre o tipo de linha final `TRow`. O mapeamento de um tipo de origem diferente é expresso por overloads `From<TSource>(IBatchSource<TSource>, Func<TSource,TRow>)` / `From<TSource>(IStreamingSource<TSource>, Func<TSource,TRow>)`, que adaptam a source via `MappingBatchSource`/`MappingStreamingSource`.
204204

205205
**Por quê.** Um passo `Map<TOut>` que troca o tipo do builder quebraria o padrão de registro `AddReport<TRow>("nome", Action<ReportBuilder<TRow>>)` (a lambda continuaria num builder de outro tipo enquanto o registro buildaria o original). O overload de `From` entrega a mesma capacidade ("Map para um tipo de saída" da spec) sem essa armadilha e sem segundo parâmetro genérico no builder. Colunas são declaradas com `.Column(v => v.X, "Header")` (infere `ColumnType` do tipo do membro) ou `Columns(Col(...))`.
206+
207+
---
208+
209+
## D13 — SQL Server keyset source (Sources / PR 3)
210+
211+
**Decisão.**
212+
- Entrada fluente: `Source.Sql(connectionString, sql).Keyset<T,TKey>(v => v.Id, pageSize: 1000)`. O primeiro parâmetro é a **connection string** na v1; resolução por **nome de conexão** (config/DI, como o `"vendas-db"` da spec) fica pós-MVP.
213+
- A query é responsabilidade do autor e **deve** expor um parâmetro `@cursor` na coluna-chave e ordenar por ela — padrão recomendado `WHERE (@cursor IS NULL OR Id > @cursor) ORDER BY Id`. Primeira página manda `@cursor = NULL`.
214+
- **Conexão aberta/fechada por página** (D2/D3). O **cursor** é a última chave da página serializada como `string?` (`BatchResult.NextCursor`); `HasMore` só é verdadeiro quando a página encheu (`Count == pageSize`) e há última chave.
215+
- **Bind de parâmetros defensivo:** só são adicionados ao comando os parâmetros que a query realmente referencia (varredura do texto por `@nome`), evitando "too many parameters"; parâmetros de execução (run-time) sobrepõem os estáticos sem duplicar.
216+
- **Materialização** (`RecordMaterializer<T>`): preferir o construtor mais longo do POCO (records posicionais), casando parâmetros↔colunas por nome (case-insensitive); fallback para construtor vazio + propriedades setáveis. Ordinais de coluna mapeados por nome.
217+
- O `Schema` declarado pela source é um placeholder mínimo — a projeção da pipeline usa as colunas do builder (D1), não o schema da source.
218+
219+
**Por quê.** Keyset com `@cursor` opaco e conexão-por-página atende CA-2 (lê todas as páginas em ordem, sem pular/repetir) e mantém memória constante, deixando checkpoint/multi-worker viáveis depois sem rework. Connection-by-name é açúcar de configuração que não muda o contrato — corta da v1.

NeoReports.sln

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NeoReports.Core", "src\NeoR
1717
EndProject
1818
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NeoReports.Core.UnitTests", "tests\NeoReports.Core.UnitTests\NeoReports.Core.UnitTests.csproj", "{D2208288-ACB6-477E-B8CF-C165A6F0155D}"
1919
EndProject
20+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NeoReports.Formats.Csv", "src\Formats\NeoReports.Formats.Csv\NeoReports.Formats.Csv.csproj", "{B5B9EFDF-B5EF-495F-BEFF-0E347C322CB3}"
21+
EndProject
22+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NeoReports.Destinations.Local", "src\Destinations\NeoReports.Destinations.Local\NeoReports.Destinations.Local.csproj", "{50356BA2-93DA-4F47-A6BA-647BE062DDA0}"
23+
EndProject
24+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NeoReports.Sources.Sql", "src\Sources\NeoReports.Sources.Sql\NeoReports.Sources.Sql.csproj", "{AA185CCC-8741-4032-9C04-88F9DC37FF6B}"
25+
EndProject
26+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "01-sql-to-csv-local", "samples\01-sql-to-csv-local\01-sql-to-csv-local.csproj", "{C6A59A96-D7EA-4AD6-95AF-F9B6EA5A870D}"
27+
EndProject
28+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NeoReports.Formats.Csv.UnitTests", "tests\NeoReports.Formats.Csv.UnitTests\NeoReports.Formats.Csv.UnitTests.csproj", "{2D69478A-68E9-4FBB-AEDE-40866829609F}"
29+
EndProject
30+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NeoReports.Destinations.Local.UnitTests", "tests\NeoReports.Destinations.Local.UnitTests\NeoReports.Destinations.Local.UnitTests.csproj", "{D50FA946-1BA6-4793-8224-2FB629EF0AB5}"
31+
EndProject
32+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NeoReports.Sources.Sql.IntegrationTests", "tests\NeoReports.Sources.Sql.IntegrationTests\NeoReports.Sources.Sql.IntegrationTests.csproj", "{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE}"
33+
EndProject
2034
Global
2135
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2236
Debug|Any CPU = Debug|Any CPU
@@ -63,6 +77,90 @@ Global
6377
{D2208288-ACB6-477E-B8CF-C165A6F0155D}.Release|x64.Build.0 = Release|Any CPU
6478
{D2208288-ACB6-477E-B8CF-C165A6F0155D}.Release|x86.ActiveCfg = Release|Any CPU
6579
{D2208288-ACB6-477E-B8CF-C165A6F0155D}.Release|x86.Build.0 = Release|Any CPU
80+
{B5B9EFDF-B5EF-495F-BEFF-0E347C322CB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
81+
{B5B9EFDF-B5EF-495F-BEFF-0E347C322CB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
82+
{B5B9EFDF-B5EF-495F-BEFF-0E347C322CB3}.Debug|x64.ActiveCfg = Debug|Any CPU
83+
{B5B9EFDF-B5EF-495F-BEFF-0E347C322CB3}.Debug|x64.Build.0 = Debug|Any CPU
84+
{B5B9EFDF-B5EF-495F-BEFF-0E347C322CB3}.Debug|x86.ActiveCfg = Debug|Any CPU
85+
{B5B9EFDF-B5EF-495F-BEFF-0E347C322CB3}.Debug|x86.Build.0 = Debug|Any CPU
86+
{B5B9EFDF-B5EF-495F-BEFF-0E347C322CB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
87+
{B5B9EFDF-B5EF-495F-BEFF-0E347C322CB3}.Release|Any CPU.Build.0 = Release|Any CPU
88+
{B5B9EFDF-B5EF-495F-BEFF-0E347C322CB3}.Release|x64.ActiveCfg = Release|Any CPU
89+
{B5B9EFDF-B5EF-495F-BEFF-0E347C322CB3}.Release|x64.Build.0 = Release|Any CPU
90+
{B5B9EFDF-B5EF-495F-BEFF-0E347C322CB3}.Release|x86.ActiveCfg = Release|Any CPU
91+
{B5B9EFDF-B5EF-495F-BEFF-0E347C322CB3}.Release|x86.Build.0 = Release|Any CPU
92+
{50356BA2-93DA-4F47-A6BA-647BE062DDA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
93+
{50356BA2-93DA-4F47-A6BA-647BE062DDA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
94+
{50356BA2-93DA-4F47-A6BA-647BE062DDA0}.Debug|x64.ActiveCfg = Debug|Any CPU
95+
{50356BA2-93DA-4F47-A6BA-647BE062DDA0}.Debug|x64.Build.0 = Debug|Any CPU
96+
{50356BA2-93DA-4F47-A6BA-647BE062DDA0}.Debug|x86.ActiveCfg = Debug|Any CPU
97+
{50356BA2-93DA-4F47-A6BA-647BE062DDA0}.Debug|x86.Build.0 = Debug|Any CPU
98+
{50356BA2-93DA-4F47-A6BA-647BE062DDA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
99+
{50356BA2-93DA-4F47-A6BA-647BE062DDA0}.Release|Any CPU.Build.0 = Release|Any CPU
100+
{50356BA2-93DA-4F47-A6BA-647BE062DDA0}.Release|x64.ActiveCfg = Release|Any CPU
101+
{50356BA2-93DA-4F47-A6BA-647BE062DDA0}.Release|x64.Build.0 = Release|Any CPU
102+
{50356BA2-93DA-4F47-A6BA-647BE062DDA0}.Release|x86.ActiveCfg = Release|Any CPU
103+
{50356BA2-93DA-4F47-A6BA-647BE062DDA0}.Release|x86.Build.0 = Release|Any CPU
104+
{AA185CCC-8741-4032-9C04-88F9DC37FF6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
105+
{AA185CCC-8741-4032-9C04-88F9DC37FF6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
106+
{AA185CCC-8741-4032-9C04-88F9DC37FF6B}.Debug|x64.ActiveCfg = Debug|Any CPU
107+
{AA185CCC-8741-4032-9C04-88F9DC37FF6B}.Debug|x64.Build.0 = Debug|Any CPU
108+
{AA185CCC-8741-4032-9C04-88F9DC37FF6B}.Debug|x86.ActiveCfg = Debug|Any CPU
109+
{AA185CCC-8741-4032-9C04-88F9DC37FF6B}.Debug|x86.Build.0 = Debug|Any CPU
110+
{AA185CCC-8741-4032-9C04-88F9DC37FF6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
111+
{AA185CCC-8741-4032-9C04-88F9DC37FF6B}.Release|Any CPU.Build.0 = Release|Any CPU
112+
{AA185CCC-8741-4032-9C04-88F9DC37FF6B}.Release|x64.ActiveCfg = Release|Any CPU
113+
{AA185CCC-8741-4032-9C04-88F9DC37FF6B}.Release|x64.Build.0 = Release|Any CPU
114+
{AA185CCC-8741-4032-9C04-88F9DC37FF6B}.Release|x86.ActiveCfg = Release|Any CPU
115+
{AA185CCC-8741-4032-9C04-88F9DC37FF6B}.Release|x86.Build.0 = Release|Any CPU
116+
{C6A59A96-D7EA-4AD6-95AF-F9B6EA5A870D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
117+
{C6A59A96-D7EA-4AD6-95AF-F9B6EA5A870D}.Debug|Any CPU.Build.0 = Debug|Any CPU
118+
{C6A59A96-D7EA-4AD6-95AF-F9B6EA5A870D}.Debug|x64.ActiveCfg = Debug|Any CPU
119+
{C6A59A96-D7EA-4AD6-95AF-F9B6EA5A870D}.Debug|x64.Build.0 = Debug|Any CPU
120+
{C6A59A96-D7EA-4AD6-95AF-F9B6EA5A870D}.Debug|x86.ActiveCfg = Debug|Any CPU
121+
{C6A59A96-D7EA-4AD6-95AF-F9B6EA5A870D}.Debug|x86.Build.0 = Debug|Any CPU
122+
{C6A59A96-D7EA-4AD6-95AF-F9B6EA5A870D}.Release|Any CPU.ActiveCfg = Release|Any CPU
123+
{C6A59A96-D7EA-4AD6-95AF-F9B6EA5A870D}.Release|Any CPU.Build.0 = Release|Any CPU
124+
{C6A59A96-D7EA-4AD6-95AF-F9B6EA5A870D}.Release|x64.ActiveCfg = Release|Any CPU
125+
{C6A59A96-D7EA-4AD6-95AF-F9B6EA5A870D}.Release|x64.Build.0 = Release|Any CPU
126+
{C6A59A96-D7EA-4AD6-95AF-F9B6EA5A870D}.Release|x86.ActiveCfg = Release|Any CPU
127+
{C6A59A96-D7EA-4AD6-95AF-F9B6EA5A870D}.Release|x86.Build.0 = Release|Any CPU
128+
{2D69478A-68E9-4FBB-AEDE-40866829609F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
129+
{2D69478A-68E9-4FBB-AEDE-40866829609F}.Debug|Any CPU.Build.0 = Debug|Any CPU
130+
{2D69478A-68E9-4FBB-AEDE-40866829609F}.Debug|x64.ActiveCfg = Debug|Any CPU
131+
{2D69478A-68E9-4FBB-AEDE-40866829609F}.Debug|x64.Build.0 = Debug|Any CPU
132+
{2D69478A-68E9-4FBB-AEDE-40866829609F}.Debug|x86.ActiveCfg = Debug|Any CPU
133+
{2D69478A-68E9-4FBB-AEDE-40866829609F}.Debug|x86.Build.0 = Debug|Any CPU
134+
{2D69478A-68E9-4FBB-AEDE-40866829609F}.Release|Any CPU.ActiveCfg = Release|Any CPU
135+
{2D69478A-68E9-4FBB-AEDE-40866829609F}.Release|Any CPU.Build.0 = Release|Any CPU
136+
{2D69478A-68E9-4FBB-AEDE-40866829609F}.Release|x64.ActiveCfg = Release|Any CPU
137+
{2D69478A-68E9-4FBB-AEDE-40866829609F}.Release|x64.Build.0 = Release|Any CPU
138+
{2D69478A-68E9-4FBB-AEDE-40866829609F}.Release|x86.ActiveCfg = Release|Any CPU
139+
{2D69478A-68E9-4FBB-AEDE-40866829609F}.Release|x86.Build.0 = Release|Any CPU
140+
{D50FA946-1BA6-4793-8224-2FB629EF0AB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
141+
{D50FA946-1BA6-4793-8224-2FB629EF0AB5}.Debug|Any CPU.Build.0 = Debug|Any CPU
142+
{D50FA946-1BA6-4793-8224-2FB629EF0AB5}.Debug|x64.ActiveCfg = Debug|Any CPU
143+
{D50FA946-1BA6-4793-8224-2FB629EF0AB5}.Debug|x64.Build.0 = Debug|Any CPU
144+
{D50FA946-1BA6-4793-8224-2FB629EF0AB5}.Debug|x86.ActiveCfg = Debug|Any CPU
145+
{D50FA946-1BA6-4793-8224-2FB629EF0AB5}.Debug|x86.Build.0 = Debug|Any CPU
146+
{D50FA946-1BA6-4793-8224-2FB629EF0AB5}.Release|Any CPU.ActiveCfg = Release|Any CPU
147+
{D50FA946-1BA6-4793-8224-2FB629EF0AB5}.Release|Any CPU.Build.0 = Release|Any CPU
148+
{D50FA946-1BA6-4793-8224-2FB629EF0AB5}.Release|x64.ActiveCfg = Release|Any CPU
149+
{D50FA946-1BA6-4793-8224-2FB629EF0AB5}.Release|x64.Build.0 = Release|Any CPU
150+
{D50FA946-1BA6-4793-8224-2FB629EF0AB5}.Release|x86.ActiveCfg = Release|Any CPU
151+
{D50FA946-1BA6-4793-8224-2FB629EF0AB5}.Release|x86.Build.0 = Release|Any CPU
152+
{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
153+
{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
154+
{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE}.Debug|x64.ActiveCfg = Debug|Any CPU
155+
{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE}.Debug|x64.Build.0 = Debug|Any CPU
156+
{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE}.Debug|x86.ActiveCfg = Debug|Any CPU
157+
{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE}.Debug|x86.Build.0 = Debug|Any CPU
158+
{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
159+
{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE}.Release|Any CPU.Build.0 = Release|Any CPU
160+
{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE}.Release|x64.ActiveCfg = Release|Any CPU
161+
{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE}.Release|x64.Build.0 = Release|Any CPU
162+
{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE}.Release|x86.ActiveCfg = Release|Any CPU
163+
{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE}.Release|x86.Build.0 = Release|Any CPU
66164
EndGlobalSection
67165
GlobalSection(SolutionProperties) = preSolution
68166
HideSolutionNode = FALSE
@@ -71,5 +169,12 @@ Global
71169
{7FFF164E-CCD7-448F-8E2A-F0D8F958065F} = {11111111-1111-1111-1111-111111111111}
72170
{EB14C940-E3EA-41F3-9510-6CE73FABA41A} = {11111111-1111-1111-1111-111111111111}
73171
{D2208288-ACB6-477E-B8CF-C165A6F0155D} = {22222222-2222-2222-2222-222222222222}
172+
{B5B9EFDF-B5EF-495F-BEFF-0E347C322CB3} = {11111111-1111-1111-1111-111111111111}
173+
{50356BA2-93DA-4F47-A6BA-647BE062DDA0} = {11111111-1111-1111-1111-111111111111}
174+
{AA185CCC-8741-4032-9C04-88F9DC37FF6B} = {11111111-1111-1111-1111-111111111111}
175+
{C6A59A96-D7EA-4AD6-95AF-F9B6EA5A870D} = {44444444-4444-4444-4444-444444444444}
176+
{2D69478A-68E9-4FBB-AEDE-40866829609F} = {22222222-2222-2222-2222-222222222222}
177+
{D50FA946-1BA6-4793-8224-2FB629EF0AB5} = {22222222-2222-2222-2222-222222222222}
178+
{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE} = {22222222-2222-2222-2222-222222222222}
74179
EndGlobalSection
75180
EndGlobal

build/Directory.Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
1212
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
1313
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.0" />
14+
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
1415
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
1516

1617
<!-- Sources / Formats / Destinations -->
@@ -29,6 +30,7 @@
2930
<PackageVersion Include="FluentAssertions" Version="7.0.0" />
3031
<PackageVersion Include="NSubstitute" Version="5.3.0" />
3132
<PackageVersion Include="Testcontainers.MsSql" Version="4.0.0" />
33+
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
3234

3335
<!-- Benchmarks -->
3436
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />

plan.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ PRs pequenos e independentes, em ordem. Cada um fecha com testes verdes e fecha
2424
- **Depende de:** PR 1.
2525

2626
## PR 3 — Sources.Sql + Formats.Csv + Destinations.Local (primeiro end-to-end)
27-
- [ ] `Source.Sql(...).Keyset(key, pageSize)``IBatchSource<T>`, conexão por página, cursor `string?`, parâmetros parametrizados.
28-
- [ ] `Format.Csv(...)` — writer não-genérico (delimitador, encoding, cabeçalho via `DisplayName`, formatação por cultura).
29-
- [ ] `Destination.Local(pathTemplate)` — tokens `{name}/{date}/{ext}`.
30-
- [ ] Sample `01-sql-to-csv-local`.
31-
- **Aceite:** CA-2, CA-4, CA-7. Report de referência roda fim-a-fim em CSV+Local. SQL testado com Testcontainers.
27+
- [x] `Source.Sql(...).Keyset(key, pageSize)``IBatchSource<T>`, conexão por página, cursor `string?`, parâmetros parametrizados (bind automático só do que a query referencia).
28+
- [x] `Format.Csv(...)` — writer não-genérico (delimitador, encoding, cabeçalho via `DisplayName`, formatação por cultura/format, escaping RFC 4180, CRLF, UTF-8 sem BOM).
29+
- [x] `Destination.Local(pathTemplate)` — tokens `{name}/{date[:fmt]}/{ext}` + parâmetros; publicação atômica (temp + move).
30+
- [x] Sample `01-sql-to-csv-local`.
31+
- **Aceite:** CA-2, CA-4, CA-7. Report de referência roda fim-a-fim em CSV+Local. SQL testado com Testcontainers. ✅ 26 testes verdes (13 Core + 4 CSV + 6 Local + 3 SQL/E2E).
3232
- **Depende de:** PR 2.
3333

3434
## PR 4 — Formats.Xlsx + Destinations.S3
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<Nullable>enable</Nullable>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<IsPackable>false</IsPackable>
9+
<GenerateDocumentationFile>false</GenerateDocumentationFile>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<ProjectReference Include="..\..\src\NeoReports.Core\NeoReports.Core.csproj" />
14+
<ProjectReference Include="..\..\src\Sources\NeoReports.Sources.Sql\NeoReports.Sources.Sql.csproj" />
15+
<ProjectReference Include="..\..\src\Formats\NeoReports.Formats.Csv\NeoReports.Formats.Csv.csproj" />
16+
<ProjectReference Include="..\..\src\Destinations\NeoReports.Destinations.Local\NeoReports.Destinations.Local.csproj" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
21+
<PackageReference Include="Microsoft.Extensions.Logging" />
22+
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
23+
</ItemGroup>
24+
25+
</Project>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.Text;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Logging;
4+
using NeoReports.Core.DependencyInjection;
5+
using NeoReports.Core.Pipeline;
6+
using NeoReports.Destinations.Local;
7+
using NeoReports.Formats.Csv;
8+
using NeoReports.Sources.Sql;
9+
using static NeoReports.Core.Building.ReportColumns;
10+
11+
// Sample 01 — SQL Server -> CSV -> local filesystem (the first end-to-end report).
12+
//
13+
// Run against any SQL Server with a Vendas table:
14+
// dotnet run --project samples/01-sql-to-csv-local -- "<connection-string>"
15+
//
16+
// Expected schema: Vendas(Id BIGINT, Cliente NVARCHAR, Valor DECIMAL, Data DATETIME2).
17+
18+
var connectionString = args.Length > 0
19+
? args[0]
20+
: "Server=localhost;Database=Sales;Trusted_Connection=True;TrustServerCertificate=True";
21+
22+
var services = new ServiceCollection();
23+
services.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Information));
24+
25+
services.AddReport<Venda>("vendas-mensal", b => b
26+
.From(Source.Sql(
27+
connectionString,
28+
"SELECT Id, Cliente, Valor, Data FROM Vendas " +
29+
"WHERE (@cursor IS NULL OR Id > @cursor) ORDER BY Id")
30+
.Keyset<Venda, long>(v => v.Id, pageSize: 1000))
31+
.Filter(v => v.Valor > 0)
32+
.Columns(
33+
Col<Venda, long>(v => v.Id, "ID Venda"),
34+
Col<Venda, string>(v => v.Cliente, "Cliente"),
35+
Col<Venda, decimal>(v => v.Valor, "Valor", format: "C2", culture: "pt-BR"),
36+
Col<Venda, DateTime>(v => v.Data, "Data Venda", format: "yyyy-MM-dd"))
37+
.To(Format.Csv(o => o.Delimiter(';').Encoding(Encoding.UTF8)))
38+
.UploadTo(Destination.Local("./out/{name}-{date:yyyy-MM-dd}.{ext}")));
39+
40+
var provider = services.BuildServiceProvider();
41+
var runner = provider.GetRequiredService<IReportRunner>();
42+
43+
var result = await runner.RunAsync("vendas-mensal");
44+
45+
Console.WriteLine($"Status: {result.Status}");
46+
Console.WriteLine($"Records read/written: {result.Stats.RecordsRead}/{result.Stats.RecordsWritten}");
47+
foreach (var upload in result.Uploads)
48+
Console.WriteLine($"Uploaded: {upload.RemotePath} (success={upload.Success})");
49+
50+
return result.Status == ReportRunStatus.Failed ? 1 : 0;
51+
52+
internal sealed record Venda(long Id, string Cliente, decimal Valor, DateTime Data);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using NeoReports.Core.Building;
2+
3+
namespace NeoReports.Destinations.Local;
4+
5+
/// <summary>Fluent entry points for destinations. Local lives here; S3 adds its own overload.</summary>
6+
public static class Destination
7+
{
8+
/// <summary>Configures a local filesystem destination.</summary>
9+
/// <param name="pathTemplate">Path template (e.g. <c>./out/{name}-{date:yyyy-MM-dd}.{ext}</c>).</param>
10+
/// <returns>A <see cref="DestinationSpec"/> to pass to <c>ReportBuilder.UploadTo(...)</c>.</returns>
11+
public static DestinationSpec Local(string pathTemplate) =>
12+
new(new LocalDestinationFactory(pathTemplate));
13+
}

0 commit comments

Comments
 (0)