param( [string]$ServerInstance = "localhost", [string]$Database = "", [string[]]$ObjectName = @(), [datetime]$FromDate = "2025-01-01", [datetime]$ToDate = "2026-01-01", [string]$OutputDirectory = (Join-Path $env:USERPROFILE "Desktop"), [int]$SampleRows = 500, [int]$MaxRowsPerObject = 0, [switch]$DiscoverOnly, [switch]$ExportCandidates, [switch]$IncludeSystemDatabases ) $ErrorActionPreference = "Stop" function New-Connection { param([string]$DbName) $builder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder $builder["Data Source"] = $ServerInstance $builder["Initial Catalog"] = $DbName $builder["Integrated Security"] = $true $builder["TrustServerCertificate"] = $true $builder["Connect Timeout"] = 15 return New-Object System.Data.SqlClient.SqlConnection($builder.ConnectionString) } function Invoke-DataTable { param( [string]$DbName, [string]$Sql, [hashtable]$Parameters = @{} ) $conn = New-Connection $DbName $cmd = $conn.CreateCommand() $cmd.CommandText = $Sql $cmd.CommandTimeout = 300 foreach ($key in $Parameters.Keys) { $param = $cmd.Parameters.Add("@$key", [System.Data.SqlDbType]::NVarChar, 4000) $param.Value = [string]$Parameters[$key] } $table = New-Object System.Data.DataTable try { $conn.Open() $reader = $cmd.ExecuteReader() $table.Load($reader) } finally { $conn.Dispose() } return $table } function Convert-ToCsvValue { param($Value) if ($null -eq $Value -or $Value -is [System.DBNull]) { return "" } if ($Value -is [datetime]) { $text = $Value.ToString("yyyy-MM-dd HH:mm:ss") } else { $text = [string]$Value } $text = $text.Replace('"', '""') return '"' + $text + '"' } function Export-QueryToCsv { param( [string]$DbName, [string]$Sql, [string]$Path ) $conn = New-Connection $DbName $cmd = $conn.CreateCommand() $cmd.CommandText = $Sql $cmd.CommandTimeout = 0 $writer = New-Object System.IO.StreamWriter($Path, $false, [System.Text.Encoding]::UTF8) $rowCount = 0 try { $conn.Open() $reader = $cmd.ExecuteReader() $headers = for ($i = 0; $i -lt $reader.FieldCount; $i++) { Convert-ToCsvValue $reader.GetName($i) } $writer.WriteLine(($headers -join ";")) while ($reader.Read()) { $values = for ($i = 0; $i -lt $reader.FieldCount; $i++) { Convert-ToCsvValue $reader.GetValue($i) } $writer.WriteLine(($values -join ";")) $rowCount++ } } finally { $writer.Dispose() $conn.Dispose() } return $rowCount } function Quote-NamePart { param([string]$Name) return "[" + $Name.Replace("]", "]]") + "]" } function Split-SqlObjectName { param([string]$Name) $parts = $Name.Split(".", 2) if ($parts.Count -eq 1) { return [pscustomobject]@{ SchemaName = "dbo"; ObjectName = $parts[0] } } return [pscustomobject]@{ SchemaName = $parts[0].Trim("[", "]"); ObjectName = $parts[1].Trim("[", "]") } } function Get-UserDatabases { $sql = @" SELECT name FROM sys.databases WHERE state_desc = 'ONLINE' AND HAS_DBACCESS(name) = 1 $(if ($IncludeSystemDatabases) { "" } else { "AND database_id > 4" }) ORDER BY name; "@ Invoke-DataTable "master" $sql | ForEach-Object { $_.name } } function Get-CandidateObjects { param([string]$DbName) $sql = @" WITH object_columns AS ( SELECT s.name AS SchemaName, o.name AS ObjectName, o.type_desc AS ObjectType, c.name AS ColumnName, t.name AS TypeName, c.max_length, c.precision, c.scale FROM sys.objects o JOIN sys.schemas s ON s.schema_id = o.schema_id JOIN sys.columns c ON c.object_id = o.object_id JOIN sys.types t ON t.user_type_id = c.user_type_id WHERE o.type IN ('U', 'V') AND o.is_ms_shipped = 0 ), scored AS ( SELECT SchemaName, ObjectName, ObjectType, SUM(CASE WHEN LOWER(ObjectName) LIKE '%fact%' OR LOWER(ObjectName) LIKE '%invoice%' OR LOWER(ObjectName) LIKE '%venta%' OR LOWER(ObjectName) LIKE '%sales%' OR LOWER(ObjectName) LIKE '%albar%' OR LOWER(ObjectName) LIKE '%pedido%' THEN 5 ELSE 0 END) + SUM(CASE WHEN LOWER(ColumnName) LIKE '%fecha%' OR LOWER(ColumnName) LIKE '%date%' THEN 2 ELSE 0 END) + SUM(CASE WHEN LOWER(ColumnName) LIKE '%cliente%' OR LOWER(ColumnName) LIKE '%customer%' THEN 2 ELSE 0 END) + SUM(CASE WHEN LOWER(ColumnName) LIKE '%articulo%' OR LOWER(ColumnName) LIKE '%item%' OR LOWER(ColumnName) LIKE '%producto%' THEN 2 ELSE 0 END) + SUM(CASE WHEN LOWER(ColumnName) LIKE '%importe%' OR LOWER(ColumnName) LIKE '%neto%' OR LOWER(ColumnName) LIKE '%total%' OR LOWER(ColumnName) LIKE '%amount%' THEN 3 ELSE 0 END) + SUM(CASE WHEN LOWER(ColumnName) LIKE '%cantidad%' OR LOWER(ColumnName) LIKE '%quantity%' OR LOWER(ColumnName) LIKE '%unidades%' THEN 2 ELSE 0 END) AS Score, COUNT(*) AS ColumnCount, STRING_AGG(CONVERT(nvarchar(max), ColumnName), ', ') WITHIN GROUP (ORDER BY ColumnName) AS Columns FROM object_columns GROUP BY SchemaName, ObjectName, ObjectType ) SELECT TOP (80) DB_NAME() AS DatabaseName, SchemaName, ObjectName, ObjectType, Score, ColumnCount, Columns FROM scored WHERE Score > 0 ORDER BY Score DESC, ObjectName; "@ Invoke-DataTable $DbName $sql } function Get-DateColumns { param( [string]$DbName, [string]$SchemaName, [string]$ObjectNameValue ) $sql = @" SELECT c.name AS ColumnName FROM sys.objects o JOIN sys.schemas s ON s.schema_id = o.schema_id JOIN sys.columns c ON c.object_id = o.object_id JOIN sys.types t ON t.user_type_id = c.user_type_id WHERE s.name = @schema AND o.name = @object AND ( t.name IN ('date', 'datetime', 'datetime2', 'smalldatetime') OR LOWER(c.name) LIKE '%fecha%' OR LOWER(c.name) LIKE '%date%' ) ORDER BY CASE WHEN LOWER(c.name) LIKE '%fact%' OR LOWER(c.name) LIKE '%invoice%' THEN 0 WHEN LOWER(c.name) LIKE '%fecha%' OR LOWER(c.name) LIKE '%date%' THEN 1 ELSE 2 END, c.column_id; "@ Invoke-DataTable $DbName $sql @{ schema = $SchemaName; object = $ObjectNameValue } | ForEach-Object { $_.ColumnName } } function Build-SelectSql { param( [string]$SchemaName, [string]$ObjectNameValue, [string]$DateColumn, [int]$TopRows ) $topClause = if ($TopRows -gt 0) { "TOP ($TopRows)" } else { "" } $qualified = "$(Quote-NamePart $SchemaName).$(Quote-NamePart $ObjectNameValue)" if ([string]::IsNullOrWhiteSpace($DateColumn)) { return "SELECT $topClause * FROM $qualified;" } $from = $FromDate.ToString("yyyy-MM-dd") $to = $ToDate.ToString("yyyy-MM-dd") $dateColumnSql = Quote-NamePart $DateColumn return @" SELECT $topClause * FROM $qualified WHERE TRY_CONVERT(date, $dateColumnSql) >= CONVERT(date, '$from') AND TRY_CONVERT(date, $dateColumnSql) < CONVERT(date, '$to') ORDER BY TRY_CONVERT(date, $dateColumnSql); "@ } function Normalize-FileName { param([string]$Value) return ($Value -replace '[\\/:*?"<>|]', '_') } $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $runDirectory = Join-Path $OutputDirectory "Sage_SQL_CSV_Export_$timestamp" New-Item -ItemType Directory -Path $runDirectory -Force | Out-Null $databases = if ([string]::IsNullOrWhiteSpace($Database)) { @(Get-UserDatabases) } else { @($Database) } $summary = New-Object System.Collections.Generic.List[object] $allCandidates = New-Object System.Collections.Generic.List[object] foreach ($db in $databases) { Write-Host "Scanning database: $db" try { $candidates = @(Get-CandidateObjects $db) foreach ($candidate in $candidates) { $allCandidates.Add($candidate) } } catch { $summary.Add([pscustomobject]@{ Database = $db Object = "" Action = "Discovery failed" Rows = 0 File = "" Error = $_.Exception.Message }) } } $candidatePath = Join-Path $runDirectory "candidate_objects.csv" if ($allCandidates.Count -gt 0) { $allCandidates | Export-Csv -LiteralPath $candidatePath -NoTypeInformation -Encoding UTF8 -Delimiter ";" } if (-not $DiscoverOnly) { $objectsToExport = New-Object System.Collections.Generic.List[object] foreach ($name in $ObjectName) { if ([string]::IsNullOrWhiteSpace($name)) { continue } if ([string]::IsNullOrWhiteSpace($Database)) { throw "When -ObjectName is used, pass -Database as well." } $parsed = Split-SqlObjectName $name $objectsToExport.Add([pscustomobject]@{ DatabaseName = $Database SchemaName = $parsed.SchemaName ObjectName = $parsed.ObjectName }) } if ($ExportCandidates) { foreach ($candidate in ($allCandidates | Sort-Object DatabaseName, @{Expression="Score"; Descending=$true} | Select-Object -First 25)) { $objectsToExport.Add([pscustomobject]@{ DatabaseName = $candidate.DatabaseName SchemaName = $candidate.SchemaName ObjectName = $candidate.ObjectName }) } } foreach ($object in $objectsToExport) { $db = $object.DatabaseName $schema = $object.SchemaName $objectNameValue = $object.ObjectName try { $dateColumn = @(Get-DateColumns $db $schema $objectNameValue | Select-Object -First 1)[0] $limit = if ($MaxRowsPerObject -gt 0) { $MaxRowsPerObject } elseif ($ObjectName.Count -gt 0) { 0 } else { $SampleRows } $sql = Build-SelectSql $schema $objectNameValue $dateColumn $limit $fileName = Normalize-FileName "$db.$schema.$objectNameValue.csv" $path = Join-Path $runDirectory $fileName Write-Host "Exporting $db.$schema.$objectNameValue -> $path" $rows = Export-QueryToCsv $db $sql $path $summary.Add([pscustomobject]@{ Database = $db Object = "$schema.$objectNameValue" Action = "Exported" Rows = $rows File = $path DateColumn = $dateColumn Error = "" }) } catch { $summary.Add([pscustomobject]@{ Database = $db Object = "$schema.$objectNameValue" Action = "Export failed" Rows = 0 File = "" DateColumn = "" Error = $_.Exception.Message }) } } } $summaryPath = Join-Path $runDirectory "export_summary.csv" $summary | Export-Csv -LiteralPath $summaryPath -NoTypeInformation -Encoding UTF8 -Delimiter ";" $readmePath = Join-Path $runDirectory "README.txt" @" Sage SQL CSV export =================== Server instance: $ServerInstance Database filter: $(if ($Database) { $Database } else { "(all accessible user databases)" }) From date: $($FromDate.ToString("yyyy-MM-dd")) To date: $($ToDate.ToString("yyyy-MM-dd")) Files: - candidate_objects.csv: SQL tables/views that look relevant for sales/invoices. - export_summary.csv: export status and row counts. - *.csv: exported samples or selected full exports. Recommended workflow: 1. Run discovery first: .\Export-SageSqlCsv.ps1 -DiscoverOnly 2. Send candidate_objects.csv to Trafag/IT for selection. 3. Export selected objects: .\Export-SageSqlCsv.ps1 -Database "DATABASE_NAME" -ObjectName "schema.table_or_view" 4. If the selected object is very large, add: -FromDate "2025-01-01" -ToDate "2026-01-01" -MaxRowsPerObject 100000 The script only reads data. It does not change SQL Server or Sage. "@ | Set-Content -LiteralPath $readmePath -Encoding UTF8 Write-Host "" Write-Host "Created folder:" Write-Host " $runDirectory" Write-Host "" Write-Host "Main files:" Write-Host " $candidatePath" Write-Host " $summaryPath"