| New file |
| | |
| | | <# |
| | | 22 所上位机 SQL Server 只读探查:找出「工步方案」在哪里(工步名称可能只存在于方案里)。 |
| | | 与取数程序同一条纪律:只执行 SELECT,不写、不建、不改、不调用存储过程,每条查询都显式设超时。 |
| | | 甲方可自行审计本文件。 |
| | | |
| | | 为什么需要它:`Ws<N>_IV_0` 采样表里只有工步**号**,没有工步**名称**。 |
| | | 取数程序只遍历 `DB_` 开头的测试库,所以这台实例上**其他库我们从来没看过**—— |
| | | 方案的名称很可能就在那些库里。 |
| | | |
| | | 用法(在那台机器上,Windows 认证免密码): |
| | | powershell -ExecutionPolicy Bypass -File .\j22-introspect.ps1 |
| | | powershell -ExecutionPolicy Bypass -File .\j22-introspect.ps1 -SqlInstance '.\SQLEXPRESS' |
| | | #> |
| | | param( |
| | | [string]$SqlInstance = '.\SQLEXPRESS', |
| | | [int]$ConnectTimeoutSec = 10, |
| | | [int]$CommandTimeoutSec = 15, |
| | | [int]$SampleRows = 3 |
| | | ) |
| | | |
| | | $ErrorActionPreference = 'Stop' |
| | | |
| | | # 只允许常规库名:库名会被拼进查询(标识符不能用参数),异常字符一律跳过,不改写厂商数据。 |
| | | function Test-SafeName([string]$name) { return $name -match '^[A-Za-z0-9_$]+$' } |
| | | |
| | | # 所有查询都经过这里:显式设 CommandTimeout,读满即断,不发加锁读、不开启事务。 |
| | | function Invoke-ReadOnlyQuery($connection, [string]$sql) { |
| | | $command = $connection.CreateCommand() |
| | | $command.CommandText = $sql |
| | | $command.CommandTimeout = $CommandTimeoutSec |
| | | $reader = $command.ExecuteReader() |
| | | try { |
| | | $rows = New-Object System.Collections.ArrayList |
| | | while ($reader.Read()) { |
| | | $values = New-Object System.Collections.ArrayList |
| | | for ($i = 0; $i -lt $reader.FieldCount; $i++) { |
| | | if ($reader.IsDBNull($i)) { [void]$values.Add($null) } |
| | | else { [void]$values.Add($reader.GetValue($i)) } |
| | | } |
| | | [void]$rows.Add($values) |
| | | } |
| | | return , $rows |
| | | } |
| | | finally { $reader.Close(); $command.Dispose() } |
| | | } |
| | | |
| | | function Write-Row($values) { |
| | | $text = ($values | ForEach-Object { if ($null -eq $_) { '<null>' } else { [string]$_ } }) -join ' | ' |
| | | Write-Host (' ' + $text) |
| | | } |
| | | |
| | | $connectionString = "Server=$SqlInstance;Integrated Security=True;Connect Timeout=$ConnectTimeoutSec;" + |
| | | "Application Name=j22-introspect" |
| | | $connection = New-Object System.Data.SqlClient.SqlConnection $connectionString |
| | | $connection.Open() |
| | | Write-Host "已连接:$SqlInstance(只读探查,不写任何数据)" |
| | | |
| | | try { |
| | | # ---------------------------------------------------------------- 1. 库清单 |
| | | Write-Host '' |
| | | Write-Host '=== 1. 用户库清单(database_id > 4) ===' |
| | | $databases = Invoke-ReadOnlyQuery $connection @' |
| | | select name, create_date |
| | | from sys.databases |
| | | where database_id > 4 |
| | | order by name |
| | | '@ |
| | | $testDatabases = New-Object System.Collections.ArrayList |
| | | $otherDatabases = New-Object System.Collections.ArrayList |
| | | foreach ($row in $databases) { |
| | | $name = [string]$row[0] |
| | | if ($name -like 'DB[_]%') { [void]$testDatabases.Add($name) } else { [void]$otherDatabases.Add($row) } |
| | | } |
| | | Write-Host (" 测试库(DB_ 开头,取数程序已在读):" + $testDatabases.Count + " 个") |
| | | Write-Host (" 其他库(取数程序从未遍历):" + $otherDatabases.Count + " 个") |
| | | foreach ($row in $otherDatabases) { Write-Row $row } |
| | | |
| | | # ---------------------------------------------------------------- 2. 其他库里的表 |
| | | Write-Host '' |
| | | Write-Host '=== 2. 其他库里的表 ===' |
| | | $candidates = New-Object System.Collections.ArrayList |
| | | foreach ($row in $otherDatabases) { |
| | | $database = [string]$row[0] |
| | | if (-not (Test-SafeName $database)) { Write-Host " 跳过库名异常的库:$database"; continue } |
| | | Write-Host " [$database]" |
| | | try { |
| | | $tables = Invoke-ReadOnlyQuery $connection ("select name from [$database].sys.tables order by name") |
| | | } |
| | | catch { Write-Host (' 读取失败:' + $_.Exception.Message); continue } |
| | | if ($tables.Count -eq 0) { Write-Host ' (没有表)'; continue } |
| | | foreach ($table in $tables) { |
| | | $tableName = [string]$table[0] |
| | | Write-Host " $tableName" |
| | | # 方案/配方/任务这类表最可能带工步名称,登记下来看列与样例行。 |
| | | if ($tableName -match '(?i)(step|recipe|task|plan|config|program|scheme|procedure|formula)') { |
| | | [void]$candidates.Add([pscustomobject]@{ Database = $database; Table = $tableName }) |
| | | } |
| | | } |
| | | } |
| | | |
| | | # ---------------------------------------------------------------- 3. 候选表的列与样例行 |
| | | Write-Host '' |
| | | Write-Host '=== 3. 候选表(表名含 step/recipe/task/plan/config 等)的列与样例行 ===' |
| | | if ($candidates.Count -eq 0) { |
| | | Write-Host ' 没有匹配到候选表。' |
| | | } |
| | | foreach ($candidate in $candidates) { |
| | | $database = $candidate.Database |
| | | $table = $candidate.Table |
| | | if (-not (Test-SafeName $table)) { continue } |
| | | Write-Host " [$database].[$table]" |
| | | try { |
| | | $columns = Invoke-ReadOnlyQuery $connection @" |
| | | select c.name, ty.name as type_name |
| | | from [$database].sys.columns c |
| | | join [$database].sys.tables tb on tb.object_id = c.object_id |
| | | join [$database].sys.types ty on ty.user_type_id = c.user_type_id |
| | | where tb.name = '$table' |
| | | order by c.column_id |
| | | "@ |
| | | } |
| | | catch { Write-Host (' 读取列失败:' + $_.Exception.Message); continue } |
| | | $names = @() |
| | | foreach ($column in $columns) { |
| | | $columnName = [string]$column[0] |
| | | Write-Host (' ' + $columnName + ' : ' + [string]$column[1]) |
| | | if ($columnName -match '(?i)(name|desc|type|no|id|index|idx)') { $names += $columnName } |
| | | } |
| | | if ($names.Count -eq 0) { continue } |
| | | # 只挑"像是名称/编号"的列看几行:这是为了判断这张表里到底有没有工步名称,不导出业务数据。 |
| | | $projection = ($names | Select-Object -First 8 | ForEach-Object { "[$_]" }) -join ', ' |
| | | try { |
| | | $sample = Invoke-ReadOnlyQuery $connection "select top $SampleRows $projection from [$database].dbo.[$table]" |
| | | foreach ($line in $sample) { Write-Row $line } |
| | | } |
| | | catch { Write-Host (' 取样失败:' + $_.Exception.Message) } |
| | | } |
| | | |
| | | Write-Host '' |
| | | Write-Host '探查结束:本条只读,未写入任何数据。' |
| | | } |
| | | finally { $connection.Close(); $connection.Dispose() } |