yeg
5 天以前 c4bb5bc9314ae5731ecae347c5aa11116ff1445e
新增 22 所上位机只读探查脚本,用于定位工步方案的存放位置

工步名称只可能来自厂商的工步方案,而这台实例上除 DB_ 开头的测试库以外的库,取数程序
从未遍历过(两条探测查询都带 name like 'DB[_]%')。脚本按与取数程序同一纪律只读探查:
只 SELECT、不写不建不改、每条查询显式设 CommandTimeout,甲方可自行审计。

依次列出非 DB_ 库、这些库里的表、以及表名含 step/recipe/task/plan/config 的候选表的列与
样例行(只取名称/编号类列)。库名与表名拼进查询前先做安全校验,异常字符一律跳过。

本机与交付脚本自身函数的演练:行形状(含单行不被 PowerShell 拆层)、DBNull -> null、
CommandTimeout 确实被设上、名称校验与行打印,全部通过;脚本已加 UTF-8 BOM。
1个文件已添加
142 ■■■■■ 已修改文件
scripts/j22-introspect.ps1 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/j22-introspect.ps1
New file
@@ -0,0 +1,142 @@
<#
  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() }