Skip to content

Commit 1c2272b

Browse files
committed
test: add postgresql toolkit core helpers
1 parent 3681ad6 commit 1c2272b

9 files changed

Lines changed: 1967 additions & 0 deletions

File tree

docs/superpowers/plans/2026-04-14-postgresql-toolkit.md

Lines changed: 1416 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
Set-StrictMode -Version Latest
2+
$ErrorActionPreference = 'Stop'
3+
4+
<#
5+
.SYNOPSIS
6+
解析 GNU 风格的长参数列表。
7+
8+
.DESCRIPTION
9+
把 `--flag value`、`--flag=value` 和单独布尔开关转换为 hashtable,
10+
统一后续子命令读取参数的方式。
11+
12+
.PARAMETER Arguments
13+
从 CLI 入口透传进来的剩余参数数组。
14+
15+
.OUTPUTS
16+
hashtable
17+
返回键名转为下划线风格的参数表,例如 `--env-file` 会变成 `env_file`。
18+
#>
19+
function ConvertFrom-LongOptionList {
20+
[CmdletBinding()]
21+
param(
22+
[Parameter(Mandatory)]
23+
[string[]]$Arguments
24+
)
25+
26+
$result = @{}
27+
$index = 0
28+
29+
while ($index -lt $Arguments.Count) {
30+
$token = $Arguments[$index]
31+
if (-not $token.StartsWith('--')) {
32+
throw "仅支持 GNU 风格长参数,收到: $token"
33+
}
34+
35+
$trimmed = $token.Substring(2)
36+
if ($trimmed.Contains('=')) {
37+
$parts = $trimmed.Split('=', 2)
38+
$result[$parts[0].Replace('-', '_')] = $parts[1]
39+
$index++
40+
continue
41+
}
42+
43+
if (($index + 1) -lt $Arguments.Count -and -not $Arguments[$index + 1].StartsWith('--')) {
44+
$result[$trimmed.Replace('-', '_')] = $Arguments[$index + 1]
45+
$index += 2
46+
continue
47+
}
48+
49+
$result[$trimmed.Replace('-', '_')] = $true
50+
$index++
51+
}
52+
53+
return $result
54+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
Set-StrictMode -Version Latest
2+
$ErrorActionPreference = 'Stop'
3+
4+
<#
5+
.SYNOPSIS
6+
读取 PostgreSQL `.env` 风格连接配置。
7+
8+
.DESCRIPTION
9+
只支持简单的 `KEY=VALUE` 形式,不执行任何 shell 表达式,
10+
用于安全读取 `PG*` 环境变量示例文件。
11+
12+
.PARAMETER Path
13+
`.env` 文件路径;为空时返回空 hashtable。
14+
15+
.OUTPUTS
16+
hashtable
17+
返回按原始键名保存的环境变量字典。
18+
#>
19+
function Import-PgEnvFile {
20+
[CmdletBinding()]
21+
param(
22+
[string]$Path
23+
)
24+
25+
if ([string]::IsNullOrWhiteSpace($Path)) {
26+
return @{}
27+
}
28+
29+
$values = @{}
30+
foreach ($line in Get-Content -Path $Path) {
31+
if ([string]::IsNullOrWhiteSpace($line) -or $line.TrimStart().StartsWith('#')) {
32+
continue
33+
}
34+
35+
$parts = $line.Split('=', 2)
36+
if ($parts.Count -ne 2) {
37+
throw "无效 env 行: $line"
38+
}
39+
40+
$values[$parts[0].Trim()] = $parts[1].Trim()
41+
}
42+
43+
return $values
44+
}
45+
46+
<#
47+
.SYNOPSIS
48+
解析 PostgreSQL 连接串为结构化字段。
49+
50+
.DESCRIPTION
51+
目前按 URI 方式解析 `postgresql://user:password@host:port/database`,
52+
供统一连接上下文组装逻辑复用。
53+
54+
.PARAMETER ConnectionString
55+
PostgreSQL 连接串;为空时返回空结果。
56+
57+
.OUTPUTS
58+
hashtable
59+
返回 `Host`、`Port`、`User`、`Password`、`Database` 字段。
60+
#>
61+
function ConvertFrom-PgConnectionString {
62+
[CmdletBinding()]
63+
param(
64+
[string]$ConnectionString
65+
)
66+
67+
if ([string]::IsNullOrWhiteSpace($ConnectionString)) {
68+
return @{}
69+
}
70+
71+
$builder = [System.Uri]$ConnectionString
72+
$userInfoParts = $builder.UserInfo.Split(':', 2)
73+
return @{
74+
Host = $builder.Host
75+
Port = if ($builder.Port -gt 0) { $builder.Port } else { $null }
76+
User = $userInfoParts[0]
77+
Password = if ($userInfoParts.Count -gt 1) { $userInfoParts[1] } else { $null }
78+
Database = $builder.AbsolutePath.TrimStart('/')
79+
}
80+
}
81+
82+
<#
83+
.SYNOPSIS
84+
在日志里屏蔽敏感值。
85+
86+
.DESCRIPTION
87+
当前主要用于密码等敏感信息展示时的统一脱敏处理。
88+
89+
.PARAMETER Value
90+
待脱敏的原始字符串。
91+
92+
.OUTPUTS
93+
string
94+
当输入非空时返回 `***`,否则返回原值。
95+
#>
96+
function Mask-PgSecret {
97+
[CmdletBinding()]
98+
param(
99+
[AllowNull()]
100+
[string]$Value
101+
)
102+
103+
if ([string]::IsNullOrEmpty($Value)) {
104+
return $Value
105+
}
106+
107+
return '***'
108+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
Set-StrictMode -Version Latest
2+
$ErrorActionPreference = 'Stop'
3+
4+
<#
5+
.SYNOPSIS
6+
生成统一的 PostgreSQL 连接上下文。
7+
8+
.DESCRIPTION
9+
按“显式参数 > 连接串 > env-file > 当前进程环境变量”的优先级合并连接配置,
10+
让后续命令构建逻辑只依赖一个规范化对象。
11+
12+
.PARAMETER CliOptions
13+
由 `ConvertFrom-LongOptionList` 返回的参数表。
14+
15+
.OUTPUTS
16+
PSCustomObject
17+
返回统一的连接上下文,至少包含 `Host`、`Port`、`User`、`Password`、`Database`。
18+
#>
19+
function Resolve-PgContext {
20+
[CmdletBinding()]
21+
param(
22+
[Parameter(Mandatory)]
23+
[hashtable]$CliOptions
24+
)
25+
26+
$envFilePath = if ($CliOptions.ContainsKey('env_file')) { [string]$CliOptions['env_file'] } else { $null }
27+
$connectionString = if ($CliOptions.ContainsKey('connection_string')) { [string]$CliOptions['connection_string'] } else { $null }
28+
$envFileValues = Import-PgEnvFile -Path $envFilePath
29+
$connectionValues = ConvertFrom-PgConnectionString -ConnectionString $connectionString
30+
31+
$connectionHost = if ($connectionValues.ContainsKey('Host')) { $connectionValues['Host'] } else { $null }
32+
$connectionPort = if ($connectionValues.ContainsKey('Port')) { $connectionValues['Port'] } else { $null }
33+
$connectionUser = if ($connectionValues.ContainsKey('User')) { $connectionValues['User'] } else { $null }
34+
$connectionPassword = if ($connectionValues.ContainsKey('Password')) { $connectionValues['Password'] } else { $null }
35+
$connectionDatabase = if ($connectionValues.ContainsKey('Database')) { $connectionValues['Database'] } else { $null }
36+
37+
$envFileHost = if ($envFileValues.ContainsKey('PGHOST')) { $envFileValues['PGHOST'] } else { $null }
38+
$envFilePort = if ($envFileValues.ContainsKey('PGPORT')) { $envFileValues['PGPORT'] } else { $null }
39+
$envFileUser = if ($envFileValues.ContainsKey('PGUSER')) { $envFileValues['PGUSER'] } else { $null }
40+
$envFilePassword = if ($envFileValues.ContainsKey('PGPASSWORD')) { $envFileValues['PGPASSWORD'] } else { $null }
41+
$envFileDatabase = if ($envFileValues.ContainsKey('PGDATABASE')) { $envFileValues['PGDATABASE'] } else { $null }
42+
43+
$resolvedHost = if ($CliOptions.ContainsKey('host')) { [string]$CliOptions['host'] } elseif ($connectionHost) { $connectionHost } elseif ($envFileHost) { $envFileHost } else { $env:PGHOST }
44+
$resolvedPort = if ($CliOptions.ContainsKey('port')) { [int]$CliOptions['port'] } elseif ($connectionPort) { [int]$connectionPort } elseif ($envFilePort) { [int]$envFilePort } elseif ($env:PGPORT) { [int]$env:PGPORT } else { 5432 }
45+
$resolvedUser = if ($CliOptions.ContainsKey('user')) { [string]$CliOptions['user'] } elseif ($connectionUser) { $connectionUser } elseif ($envFileUser) { $envFileUser } else { $env:PGUSER }
46+
$resolvedPassword = if ($CliOptions.ContainsKey('password')) { [string]$CliOptions['password'] } elseif ($connectionPassword) { $connectionPassword } elseif ($envFilePassword) { $envFilePassword } else { $env:PGPASSWORD }
47+
$resolvedDatabase = if ($CliOptions.ContainsKey('database')) { [string]$CliOptions['database'] } elseif ($connectionDatabase) { $connectionDatabase } elseif ($envFileDatabase) { $envFileDatabase } else { $env:PGDATABASE }
48+
49+
return [PSCustomObject]@{
50+
Host = $resolvedHost
51+
Port = $resolvedPort
52+
User = $resolvedUser
53+
Password = $resolvedPassword
54+
Database = $resolvedDatabase
55+
EnvFile = $envFilePath
56+
}
57+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
Set-StrictMode -Version Latest
2+
$ErrorActionPreference = 'Stop'
3+
4+
<#
5+
.SYNOPSIS
6+
识别 PostgreSQL 恢复输入类型。
7+
8+
.DESCRIPTION
9+
根据输入路径判断是 SQL 文本、归档文件还是目录格式,
10+
供 `restore` 子命令选择 `psql` 或 `pg_restore` 路径。
11+
12+
.PARAMETER InputPath
13+
用户传入的恢复输入路径。
14+
15+
.OUTPUTS
16+
string
17+
返回 `sql`、`archive` 或 `directory`。
18+
#>
19+
function Resolve-PgRestoreInputKind {
20+
[CmdletBinding()]
21+
param(
22+
[Parameter(Mandatory)]
23+
[string]$InputPath
24+
)
25+
26+
if (Test-Path -Path $InputPath -PathType Container) {
27+
return 'directory'
28+
}
29+
30+
$extension = [System.IO.Path]::GetExtension($InputPath).ToLowerInvariant()
31+
switch ($extension) {
32+
'.sql' { return 'sql' }
33+
'.dump' { return 'archive' }
34+
'.backup' { return 'archive' }
35+
'.tar' { return 'archive' }
36+
default { throw "不支持的恢复输入类型: $InputPath" }
37+
}
38+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
Set-StrictMode -Version Latest
2+
$ErrorActionPreference = 'Stop'
3+
4+
<#
5+
.SYNOPSIS
6+
统一输出 PostgreSQL Toolkit 的控制台消息。
7+
8+
.DESCRIPTION
9+
为脚本内部提供统一的日志前缀,便于后续区分信息、警告和错误消息。
10+
11+
.PARAMETER Level
12+
日志级别,仅支持 `info`、`warn`、`error`。
13+
14+
.PARAMETER Message
15+
要输出的消息文本。
16+
#>
17+
function Write-PostgresToolkitMessage {
18+
[CmdletBinding()]
19+
param(
20+
[Parameter(Mandatory)]
21+
[ValidateSet('info', 'warn', 'error')]
22+
[string]$Level,
23+
24+
[Parameter(Mandatory)]
25+
[string]$Message
26+
)
27+
28+
$rendered = "[postgres-toolkit][$Level] $Message"
29+
switch ($Level) {
30+
'warn' { Write-Warning $rendered }
31+
'error' { Write-Error $rendered }
32+
default { Write-Host $rendered }
33+
}
34+
}

0 commit comments

Comments
 (0)