yeg
5 天以前 4370ec50a74d8bd14b8c509cf205946082493a91
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
<#
  22 所上位机 SQL Server 只读探查:找出「工步方案」在哪里(工步名称可能只存在于方案里)。
  v2:那台实例有几千个库,全量罗列没法看,所以本版**只报结论**——不列库、不列表,
  只找"像方案的表"与"同时含工步列和名称列的表",命中项才打印细节;其余只给计数。
 
  只读纪律与取数程序一致:只执行 SELECT,不写、不建、不改、不调用存储过程,每条查询都显式设超时。
  甲方可自行审计本文件。
 
  用法(在那台机器上,Windows 认证免密码):
      powershell -ExecutionPolicy Bypass -File .\j22-introspect.ps1
      powershell -ExecutionPolicy Bypass -File .\j22-introspect.ps1 -SqlInstance '.\SQLEXPRESS'
      powershell -ExecutionPolicy Bypass -File .\j22-introspect.ps1 -Database 'SomeDb'   # 只看指定库
#>
param(
    [string]$SqlInstance = '.\SQLEXPRESS',
    [int]$ConnectTimeoutSec = 10,
    [int]$CommandTimeoutSec = 15,
    # 非 DB_ 库最多探查多少个(按创建时间倒序,最早的先跳过)。几千个库时防止跑太久。
    [int]$MaxDatabases = 100,
    [int]$SampleRows = 3,
    # 最多打印多少张命中表:命中太多就没法复制了,其余只计数。
    [int]$MaxHits = 25,
    [string]$Database = '',
    [string]$ReportFile = ''
)
 
$ErrorActionPreference = 'Stop'
 
# 报告同时写文件:控制台一次刷不完,文件可以慢慢复制。
if (-not $ReportFile) {
    $base = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
    $ReportFile = Join-Path $base 'j22-introspect-report.txt'
}
$report = New-Object System.Collections.ArrayList
function Say([string]$text) {
    Write-Host $text
    [void]$report.Add($text)
}
 
# 只允许常规库名/表名:库名与表名会被拼进查询(标识符不能用参数),异常字符一律跳过。
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() }
}
 
# 表名像"方案/配方/任务"的,以及同时含工步列与名称列的,都算候选。
$planishTable = "(name like '%Step%' or name like '%Recipe%' or name like '%Task%' or name like '%Plan%'" +
    " or name like '%Program%' or name like '%Scheme%' or name like '%Procedure%' or name like '%Formula%')"
$stepishColumn = "(c.name like '%Step%' or c.name like '%工步%')"
$nameishColumn = "(c.name like '%Name%' or c.name like '%Desc%' or c.name like '%Title%'" +
    " or c.name like '%Remark%' or c.name like '%说明%' or c.name like '%名称%' or c.name like '%备注%')"
$nameishLike = "(?i)(name|desc|title|remark|说明|名称|备注)"
 
$connectionString = "Server=$SqlInstance;Integrated Security=True;Connect Timeout=$ConnectTimeoutSec;" +
    "Application Name=j22-introspect"
$connection = New-Object System.Data.SqlClient.SqlConnection $connectionString
$connection.Open()
 
try {
    Say "已连接:$SqlInstance(只读探查,不写任何数据)"
    Say ''
 
    # ---------------------------------------------------------------- 1. 要探查哪些库
    $all = Invoke-ReadOnlyQuery $connection @'
select name, create_date
from sys.databases
where database_id > 4
order by create_date desc
'@
    $tests = New-Object System.Collections.ArrayList
    $others = New-Object System.Collections.ArrayList
    foreach ($row in $all) {
        if ([string]$row[0] -like 'DB[_]%') { [void]$tests.Add($row) } else { [void]$others.Add($row) }
    }
 
    if ($Database) {
        $targets = New-Object System.Collections.ArrayList
        [void]$targets.Add($Database)
        Say "=== 1. 指定库探查:[$Database] ==="
    }
    else {
        Say "=== 1. 探查范围 ==="
        Say ("  用户库共 " + $all.Count + " 个;其中 DB_ 开头的测试库(取数程序已在读)" + $tests.Count +
            " 个,本次不看")
        Say ("  其他库 " + $others.Count + " 个,按创建时间倒序探查前 " + $MaxDatabases + " 个")
        $targets = New-Object System.Collections.ArrayList
        $index = 0
        foreach ($row in $others) {
            if ($index -ge $MaxDatabases) { break }
            [void]$targets.Add([string]$row[0])
            $index++
        }
        if ($others.Count -gt 0 -and $others.Count -le 20) {
            Say ("  其他库清单:" + (($others | ForEach-Object { [string]$_[0] }) -join '、'))
        }
    }
    if ($targets.Count -eq 0) {
        Say ''
        Say '=== 结论:库里没有其他库,工步方案不在数据库中 ==='
        Say '  下一步走文件侧:软件目录里的工步方案文件(.sto 方案、TaskModel.xml 等)。'
    }
 
    # ---------------------------------------------------------------- 2. 找候选表
    $candidates = New-Object System.Collections.ArrayList
    $tableCount = 0
    foreach ($database in $targets) {
        if (-not (Test-SafeName $database)) { Say "  跳过库名异常的库:$database"; continue }
        try {
            $byName = Invoke-ReadOnlyQuery $connection (
                "select name from [$database].sys.tables where $planishTable")
            $byColumn = Invoke-ReadOnlyQuery $connection @"
select tb.name
from [$database].sys.tables tb
where exists (select 1 from [$database].sys.columns c
              where c.object_id = tb.object_id and $stepishColumn)
  and exists (select 1 from [$database].sys.columns c
              where c.object_id = tb.object_id and $nameishColumn)
"@
            $tableCount += (Invoke-ReadOnlyQuery $connection "select count(*) from [$database].sys.tables")[0][0]
        }
        catch { Say ("  读取 [$database] 失败:" + $_.Exception.Message); continue }
        $seen = @{}
        foreach ($row in $byName) {
            $table = [string]$row[0]
            if ($seen.ContainsKey($table)) { continue }
            $seen[$table] = $true
            [void]$candidates.Add([pscustomobject]@{ Database = $database; Table = $table; Why = '表名像方案' })
        }
        foreach ($row in $byColumn) {
            $table = [string]$row[0]
            if ($seen.ContainsKey($table)) { continue }
            $seen[$table] = $true
            [void]$candidates.Add([pscustomobject]@{ Database = $database; Table = $table; Why = '含工步列+名称列' })
        }
    }
 
    Say ''
    Say '=== 2. 候选表统计 ==='
    Say ("  探查 " + $targets.Count + " 个库、共 " + $tableCount + " 张表;命中候选 " + $candidates.Count + " 张")
 
    # ---------------------------------------------------------------- 3. 候选表的列与样例行
    $hits = 0
    foreach ($candidate in $candidates) {
        $database = $candidate.Database
        $table = $candidate.Table
        if (-not (Test-SafeName $table)) { continue }
        try {
            # 带上 schema:表不一定在 dbo 下,用真实 schema 拼三段名,否则计数与取样会失败。
            $columns = Invoke-ReadOnlyQuery $connection @"
select c.name, schema_name(tb.schema_id) as schema_name
from [$database].sys.columns c
join [$database].sys.tables tb on tb.object_id = c.object_id
where tb.name = '$table'
order by c.column_id
"@
            $schema = [string]$columns[0][1]
            if (-not (Test-SafeName $schema)) { continue }
            $rows = (Invoke-ReadOnlyQuery $connection "select count(*) from [$database].[$schema].[$table]")[0][0]
        }
        catch { continue }
        $names = @()
        $allColumns = @()
        foreach ($column in $columns) {
            $columnName = [string]$column[0]
            $allColumns += $columnName
            if ($columnName -match $nameishLike) { $names += $columnName }
        }
        # 只报"有名称类列"的表:没有名称列的表存不下工步名称,不是我们要找的。
        if ($names.Count -eq 0) { continue }
        if ($rows -le 0) { continue }
        $hits++
        # 命中太多就没法复制了:只打印前 -MaxHits 条,其余只计数(报告文件里也只有这些)。
        if ($hits -gt $MaxHits) { continue }
        Say ''
        Say ("  [$database].[$schema].[$table]  行数 $rows  命中原因:" + $candidate.Why)
        Say ("    列:" + ($allColumns -join ', '))
        $projection = ($names | Select-Object -First 6 | ForEach-Object { "[$_]" }) -join ', '
        try {
            $sample = Invoke-ReadOnlyQuery $connection "select top $SampleRows $projection from [$database].[$schema].[$table]"
            foreach ($line in $sample) {
                Say ('    样例:' + (($line | ForEach-Object { if ($null -eq $_) { '<null>' } else { [string]$_ } }) -join ' | '))
            }
        }
        catch { Say ('    取样失败:' + $_.Exception.Message) }
    }
 
    Say ''
    if ($hits -eq 0) {
        Say '=== 结论:没有找到"带工步名称的表" ==='
        Say '  下一步:① 提高 -MaxDatabases 再看一遍;② 转文件侧,查软件目录里的工步方案文件。'
    }
    else {
        Say ("=== 结论:命中 " + $hits + " 张带名称列的候选表(最多打印 " + $MaxHits + " 张,见上),把上面这段发回即可 ===")
    }
    Say ('报告同时写在:' + $ReportFile)
}
finally {
    $connection.Close(); $connection.Dispose()
    # 中途出错也把已有内容落盘:控制台刷不完,文件可以慢慢复制。
    $report | Set-Content -LiteralPath $ReportFile -Encoding UTF8
}