Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,436 @@
|
||||
param(
|
||||
[string]$Server = $env:ONEC_SQL_SERVER,
|
||||
[string]$Database = $env:ONEC_SQL_DATABASE,
|
||||
[string]$User = $env:ONEC_SQL_USER,
|
||||
[string]$Password = $env:ONEC_SQL_PASSWORD,
|
||||
[string]$ProjectionPath,
|
||||
[string]$OutputPath,
|
||||
[switch]$SkipTableParts
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (-not $Server) { throw "Server is required. Use -Server or ONEC_SQL_SERVER." }
|
||||
if (-not $Database) { throw "Database is required. Use -Database or ONEC_SQL_DATABASE." }
|
||||
if (-not $User) { throw "User is required. Use -User or ONEC_SQL_USER." }
|
||||
if (-not $Password) { throw "Password is required. Use -Password or ONEC_SQL_PASSWORD." }
|
||||
if (-not $ProjectionPath) { throw "ProjectionPath is required." }
|
||||
if (-not $OutputPath) { throw "OutputPath is required." }
|
||||
|
||||
function Test-HasProperty {
|
||||
param(
|
||||
[object]$Object,
|
||||
[string]$Name
|
||||
)
|
||||
return $null -ne $Object -and ($Object.PSObject.Properties.Name -contains $Name)
|
||||
}
|
||||
|
||||
function Convert-SqlValue {
|
||||
param([object]$Value)
|
||||
|
||||
if ($null -eq $Value -or $Value -is [DBNull]) {
|
||||
return $null
|
||||
}
|
||||
if ($Value -is [byte[]]) {
|
||||
return [pscustomobject]@{
|
||||
kind = "binary"
|
||||
length = $Value.Length
|
||||
hex = ([BitConverter]::ToString($Value) -replace "-", "").ToLowerInvariant()
|
||||
}
|
||||
}
|
||||
if ($Value -is [DateTime]) {
|
||||
return $Value.ToString("o")
|
||||
}
|
||||
if ($Value -is [Guid]) {
|
||||
return $Value.ToString()
|
||||
}
|
||||
return $Value
|
||||
}
|
||||
|
||||
function Quote-SqlIdentifier {
|
||||
param([string]$Name)
|
||||
|
||||
return "[" + ($Name -replace "]", "]]") + "]"
|
||||
}
|
||||
|
||||
function New-SelectSql {
|
||||
param(
|
||||
[string]$Table,
|
||||
[object[]]$Columns,
|
||||
[int]$Top
|
||||
)
|
||||
|
||||
if (-not $Table) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$parts = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($column in @($Columns)) {
|
||||
if (-not (Test-HasProperty $column "column")) { continue }
|
||||
if (-not $column.column) { continue }
|
||||
$alias = if ((Test-HasProperty $column "select_alias") -and $column.select_alias) { [string]$column.select_alias } else { [string]$column.column }
|
||||
$parts.Add(" $(Quote-SqlIdentifier ([string]$column.column)) AS $(Quote-SqlIdentifier $alias)")
|
||||
}
|
||||
|
||||
$selectList = if ($parts.Count -gt 0) { [string]::Join(",`n", $parts.ToArray()) } else { " *" }
|
||||
return "SELECT TOP ($Top)`n$selectList`nFROM $(Quote-SqlIdentifier $Table);"
|
||||
}
|
||||
|
||||
function Get-ExistingColumns {
|
||||
param(
|
||||
[System.Data.SqlClient.SqlConnection]$Connection,
|
||||
[string]$Table
|
||||
)
|
||||
|
||||
$set = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||||
if (-not $Table) {
|
||||
return $set
|
||||
}
|
||||
|
||||
$command = $Connection.CreateCommand()
|
||||
$command.CommandText = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @tableName;"
|
||||
$null = $command.Parameters.Add("@tableName", [System.Data.SqlDbType]::NVarChar, 256)
|
||||
$command.Parameters["@tableName"].Value = $Table
|
||||
$reader = $command.ExecuteReader()
|
||||
try {
|
||||
while ($reader.Read()) {
|
||||
$null = $set.Add([string]$reader.GetValue(0))
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$reader.Close()
|
||||
$command.Dispose()
|
||||
}
|
||||
return $set
|
||||
}
|
||||
|
||||
function Get-CandidateTables {
|
||||
param(
|
||||
[System.Data.SqlClient.SqlConnection]$Connection,
|
||||
[string]$BaseTable
|
||||
)
|
||||
|
||||
$tables = New-Object System.Collections.Generic.List[string]
|
||||
if (-not $BaseTable) {
|
||||
return [string[]]$tables.ToArray()
|
||||
}
|
||||
|
||||
$command = $Connection.CreateCommand()
|
||||
$command.CommandText = "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE';"
|
||||
$reader = $command.ExecuteReader()
|
||||
try {
|
||||
$pattern = "^" + [regex]::Escape($BaseTable) + "(X\d+)?$"
|
||||
while ($reader.Read()) {
|
||||
$table = [string]$reader.GetValue(0)
|
||||
if ($table -match $pattern) {
|
||||
$tables.Add($table)
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$reader.Close()
|
||||
$command.Dispose()
|
||||
}
|
||||
|
||||
if (-not $tables.Contains($BaseTable)) {
|
||||
$tables.Add($BaseTable)
|
||||
}
|
||||
return [string[]]($tables.ToArray() | Sort-Object {
|
||||
if ($_ -eq $BaseTable) { 0 } elseif ($_ -match "X(\d+)$") { [int]$Matches[1] } else { 9999 }
|
||||
})
|
||||
}
|
||||
|
||||
function Get-TableRowCount {
|
||||
param(
|
||||
[System.Data.SqlClient.SqlConnection]$Connection,
|
||||
[string]$Table
|
||||
)
|
||||
|
||||
$command = $Connection.CreateCommand()
|
||||
$command.CommandText = "SELECT COUNT(*) FROM $(Quote-SqlIdentifier $Table);"
|
||||
try {
|
||||
return [int]$command.ExecuteScalar()
|
||||
}
|
||||
finally {
|
||||
$command.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Select-EffectiveTable {
|
||||
param(
|
||||
[System.Data.SqlClient.SqlConnection]$Connection,
|
||||
[string]$BaseTable,
|
||||
[object[]]$Columns,
|
||||
[string]$TableView = "effective"
|
||||
)
|
||||
|
||||
if ($TableView -eq "base") {
|
||||
return [pscustomobject]@{
|
||||
base_table = $BaseTable
|
||||
selected_table = $BaseTable
|
||||
matched_columns = 0
|
||||
requested_columns = @($Columns).Count
|
||||
row_count = Get-TableRowCount -Connection $Connection -Table $BaseTable
|
||||
is_companion = $false
|
||||
selection_mode = "base"
|
||||
}
|
||||
}
|
||||
|
||||
$best = $null
|
||||
foreach ($table in @(Get-CandidateTables -Connection $Connection -BaseTable $BaseTable)) {
|
||||
$existing = Get-ExistingColumns -Connection $Connection -Table $table
|
||||
$matched = 0
|
||||
foreach ($column in @($Columns)) {
|
||||
if ((Test-HasProperty $column "column") -and $column.column -and $existing.Contains([string]$column.column)) {
|
||||
$matched += 1
|
||||
}
|
||||
}
|
||||
$rowCount = Get-TableRowCount -Connection $Connection -Table $table
|
||||
$candidate = [pscustomobject]@{
|
||||
base_table = $BaseTable
|
||||
selected_table = $table
|
||||
matched_columns = $matched
|
||||
requested_columns = @($Columns).Count
|
||||
row_count = $rowCount
|
||||
is_companion = $table -ne $BaseTable
|
||||
selection_mode = $TableView
|
||||
}
|
||||
if (
|
||||
$null -eq $best -or
|
||||
$candidate.matched_columns -gt $best.matched_columns -or
|
||||
($candidate.matched_columns -eq $best.matched_columns -and $candidate.row_count -gt $best.row_count)
|
||||
) {
|
||||
$best = $candidate
|
||||
}
|
||||
}
|
||||
return $best
|
||||
}
|
||||
|
||||
function Resolve-ProjectionColumns {
|
||||
param(
|
||||
[System.Data.SqlClient.SqlConnection]$Connection,
|
||||
[string]$Table,
|
||||
[object[]]$Columns,
|
||||
[string]$Scope,
|
||||
[System.Collections.Generic.List[object]]$Diagnostics
|
||||
)
|
||||
|
||||
$existing = Get-ExistingColumns -Connection $Connection -Table $Table
|
||||
$result = New-Object System.Collections.Generic.List[object]
|
||||
foreach ($column in @($Columns)) {
|
||||
if (-not (Test-HasProperty $column "column") -or -not $column.column) {
|
||||
$result.Add($column)
|
||||
continue
|
||||
}
|
||||
if ($existing.Contains([string]$column.column)) {
|
||||
$result.Add($column)
|
||||
continue
|
||||
}
|
||||
$Diagnostics.Add([pscustomobject]@{
|
||||
scope = $Scope
|
||||
table = $Table
|
||||
column = [string]$column.column
|
||||
metadata_path = if (Test-HasProperty $column "metadata_path") { $column.metadata_path } else { $null }
|
||||
})
|
||||
}
|
||||
return [object[]]$result.ToArray()
|
||||
}
|
||||
|
||||
function New-ColumnMap {
|
||||
param([object[]]$Columns)
|
||||
|
||||
$map = @{}
|
||||
foreach ($column in @($Columns)) {
|
||||
if (-not (Test-HasProperty $column "select_alias")) { continue }
|
||||
if (-not $column.select_alias) { continue }
|
||||
$map[[string]$column.select_alias] = $column
|
||||
}
|
||||
return $map
|
||||
}
|
||||
|
||||
function New-Cell {
|
||||
param(
|
||||
[object]$ColumnInfo,
|
||||
[object]$Value
|
||||
)
|
||||
|
||||
$cell = [ordered]@{
|
||||
alias = if (Test-HasProperty $ColumnInfo "select_alias") { $ColumnInfo.select_alias } else { $null }
|
||||
column = if (Test-HasProperty $ColumnInfo "column") { $ColumnInfo.column } else { $null }
|
||||
metadata_path = if (Test-HasProperty $ColumnInfo "metadata_path") { $ColumnInfo.metadata_path } else { $null }
|
||||
metadata_name = if (Test-HasProperty $ColumnInfo "metadata_name") { $ColumnInfo.metadata_name } else { $null }
|
||||
metadata_uuid = if (Test-HasProperty $ColumnInfo "metadata_uuid") { $ColumnInfo.metadata_uuid } else { $null }
|
||||
metadata_field = if (Test-HasProperty $ColumnInfo "metadata_field") { $ColumnInfo.metadata_field } else { $null }
|
||||
value_type = if (Test-HasProperty $ColumnInfo "value_type") { $ColumnInfo.value_type } else { $null }
|
||||
sql_type = if (Test-HasProperty $ColumnInfo "sql_type") { $ColumnInfo.sql_type } else { $null }
|
||||
value = Convert-SqlValue $Value
|
||||
}
|
||||
return [pscustomobject]$cell
|
||||
}
|
||||
|
||||
function Invoke-ProjectionQuery {
|
||||
param(
|
||||
[System.Data.SqlClient.SqlConnection]$Connection,
|
||||
[string]$Sql,
|
||||
[object[]]$Columns
|
||||
)
|
||||
|
||||
if (-not $Sql) {
|
||||
return [pscustomobject]@{
|
||||
row_count = 0
|
||||
columns = @($Columns)
|
||||
rows = @()
|
||||
}
|
||||
}
|
||||
|
||||
$columnMap = New-ColumnMap $Columns
|
||||
$rows = New-Object System.Collections.Generic.List[object]
|
||||
$command = $Connection.CreateCommand()
|
||||
$command.CommandTimeout = 0
|
||||
$command.CommandText = $Sql
|
||||
|
||||
$reader = $command.ExecuteReader()
|
||||
try {
|
||||
while ($reader.Read()) {
|
||||
$row = [ordered]@{}
|
||||
for ($i = 0; $i -lt $reader.FieldCount; $i++) {
|
||||
$alias = $reader.GetName($i)
|
||||
if ($columnMap.ContainsKey($alias)) {
|
||||
$columnInfo = $columnMap[$alias]
|
||||
} else {
|
||||
$columnInfo = [pscustomobject]@{
|
||||
select_alias = $alias
|
||||
column = $alias
|
||||
metadata_path = $alias
|
||||
metadata_name = $alias
|
||||
}
|
||||
}
|
||||
|
||||
$metadataPath = if (Test-HasProperty $columnInfo "metadata_path") { [string]$columnInfo.metadata_path } else { $alias }
|
||||
$physicalColumn = if (Test-HasProperty $columnInfo "column") { [string]$columnInfo.column } else { $alias }
|
||||
$key = "$metadataPath::$physicalColumn"
|
||||
$row[$key] = New-Cell $columnInfo $reader.GetValue($i)
|
||||
}
|
||||
$rows.Add([pscustomobject]$row)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$reader.Close()
|
||||
$command.Dispose()
|
||||
}
|
||||
|
||||
$resultRows = $rows.ToArray()
|
||||
return [pscustomobject]@{
|
||||
row_count = [int]$resultRows.Count
|
||||
columns = [object[]]@($Columns)
|
||||
rows = [object[]]$resultRows
|
||||
}
|
||||
}
|
||||
|
||||
$projection = Get-Content -LiteralPath $ProjectionPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
$resolvedProjectionPath = (Resolve-Path -LiteralPath $ProjectionPath).ProviderPath
|
||||
$connectionString = "Server=$Server;Database=$Database;User ID=$User;Password=$Password;Encrypt=False;TrustServerCertificate=True;Application Name=Codex 1C SQL Read Projection Executor;"
|
||||
$connection = [System.Data.SqlClient.SqlConnection]::new($connectionString)
|
||||
|
||||
try {
|
||||
$connection.Open()
|
||||
$top = if (Test-HasProperty $projection "top") { [int]$projection.top } else { 10 }
|
||||
$tableView = if ((Test-HasProperty $projection "view") -and $projection.view -eq "base") { "base" } else { "effective" }
|
||||
$prunedColumns = [System.Collections.Generic.List[object]]::new()
|
||||
$effectiveTables = [System.Collections.Generic.List[object]]::new()
|
||||
$mainChoice = Select-EffectiveTable `
|
||||
-Connection $connection `
|
||||
-BaseTable $projection.main_table `
|
||||
-Columns @($projection.main_columns) `
|
||||
-TableView $tableView
|
||||
$effectiveTables.Add($mainChoice)
|
||||
$mainColumns = Resolve-ProjectionColumns `
|
||||
-Connection $connection `
|
||||
-Table $mainChoice.selected_table `
|
||||
-Columns @($projection.main_columns) `
|
||||
-Scope "main" `
|
||||
-Diagnostics $prunedColumns
|
||||
$mainSql = New-SelectSql -Table $mainChoice.selected_table -Columns @($mainColumns) -Top $top
|
||||
|
||||
$main = Invoke-ProjectionQuery `
|
||||
-Connection $connection `
|
||||
-Sql $mainSql `
|
||||
-Columns @($mainColumns)
|
||||
|
||||
$tableParts = New-Object System.Collections.Generic.List[object]
|
||||
if (-not $SkipTableParts) {
|
||||
foreach ($part in @($projection.table_parts)) {
|
||||
$partScope = "table_part:$($part.name)"
|
||||
$partChoice = Select-EffectiveTable `
|
||||
-Connection $connection `
|
||||
-BaseTable $part.table `
|
||||
-Columns @($part.columns) `
|
||||
-TableView $tableView
|
||||
$effectiveTables.Add($partChoice)
|
||||
$partColumns = Resolve-ProjectionColumns `
|
||||
-Connection $connection `
|
||||
-Table $partChoice.selected_table `
|
||||
-Columns @($part.columns) `
|
||||
-Scope $partScope `
|
||||
-Diagnostics $prunedColumns
|
||||
$partSql = New-SelectSql -Table $partChoice.selected_table -Columns @($partColumns) -Top $top
|
||||
$queryResult = Invoke-ProjectionQuery `
|
||||
-Connection $connection `
|
||||
-Sql $partSql `
|
||||
-Columns @($partColumns)
|
||||
$tableParts.Add([pscustomobject]@{
|
||||
name = $part.name
|
||||
uuid = $part.uuid
|
||||
table = $partChoice.selected_table
|
||||
base_table = $part.table
|
||||
row_count = $queryResult.row_count
|
||||
columns = $queryResult.columns
|
||||
rows = $queryResult.rows
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$tablePartArray = [object[]]$tableParts.ToArray()
|
||||
$result = [ordered]@{
|
||||
schema = "onec_sql_read_result.v1"
|
||||
projection_schema = $projection.schema
|
||||
projection_path = $resolvedProjectionPath
|
||||
server = $Server
|
||||
database = $Database
|
||||
kind = $projection.kind
|
||||
identity = $projection.identity
|
||||
view = if (Test-HasProperty $projection "view") { $projection.view } else { $null }
|
||||
extension = if (Test-HasProperty $projection "extension") { $projection.extension } else { $null }
|
||||
main = [pscustomobject]@{
|
||||
table = $mainChoice.selected_table
|
||||
base_table = $projection.main_table
|
||||
row_count = $main.row_count
|
||||
columns = $main.columns
|
||||
rows = $main.rows
|
||||
}
|
||||
table_parts = $tablePartArray
|
||||
diagnostics = [pscustomobject]@{
|
||||
projection = if (Test-HasProperty $projection "diagnostics") { $projection.diagnostics } else { $null }
|
||||
pruned_missing_columns = [object[]]$prunedColumns.ToArray()
|
||||
effective_tables = [object[]]$effectiveTables.ToArray()
|
||||
}
|
||||
}
|
||||
|
||||
$outputDirectory = Split-Path -Parent $OutputPath
|
||||
if ($outputDirectory) {
|
||||
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
|
||||
}
|
||||
$result | ConvertTo-Json -Depth 100 | Set-Content -LiteralPath $OutputPath -Encoding UTF8
|
||||
|
||||
[pscustomobject]@{
|
||||
output = (Resolve-Path -LiteralPath $OutputPath).ProviderPath
|
||||
main_rows = $main.row_count
|
||||
table_parts = $tablePartArray.Count
|
||||
table_part_rows = [object[]]@($tablePartArray | ForEach-Object { [pscustomobject]@{ name = $_.name; table = $_.table; rows = $_.row_count } })
|
||||
} | ConvertTo-Json -Depth 10
|
||||
}
|
||||
finally {
|
||||
$connection.Close()
|
||||
$connection.Dispose()
|
||||
}
|
||||
Reference in New Issue
Block a user