Почему результат DataTable.Load с одной строкой ведет себя так, как будто это DataRow вместо DataTable? - PullRequest
0 голосов
/ 09 января 2019

Я хочу запросить две базы данных на разных серверах для получения одного и того же вида информации и объединить результаты в один список.

Я создал этот пример, который работает, но только если первый сервер возвращает хотя бы две строки.

В примере используется список целых чисел (1 .. 5), но мой исходный код, соединяющий два списка строк, имеет ту же проблему.

# 'quick and dirty' sample
# to be considered insecure,
# do not use as a basis for production code.
function GetData($dbserver, $start, $end)
{
    $qry = "WITH q AS (SELECT  $start AS num UNION ALL SELECT num + 1 FROM q WHERE num < $end) SELECT * FROM q"
    $con = New-Object System.Data.SqlClient.SqlConnection
    $con.ConnectionString = "server=$dbserver;database=A2SDataCentral;Integrated Security=SSPI"
    $con.Open()
    $com = $con.CreateCommand()
    $com.CommandText = $qry
    $res = $com.ExecuteReader()
    $table = New-Object System.Data.DataTable
    $table.Load($res)
    return $table
}

$all = $null # erase any result remaining from a previous run
$t1 = GetData "(local)" 1 2
$t2 = GetData "(local)" 3 5
$all = $t1 + $t2
$all

SQL-запрос, который я здесь использую, является просто фиктивным, генерирующим последовательные числа. Это будет работать только на сервере SQL. В этом примере я запускаю его на одном и том же сервере (localhost) дважды, в действительности он должен запросить два разных сервера, чтобы получить два списка имен и присоединиться к ним.

Используемый как выше, этот пример делает то, что я пытался сделать, конечный результат - список целых чисел от 1 до 5.

num
---
  1
  2
  3
  4
  5

Но когда я изменяю GetData вызовы на это, первый вызов возвращает только одну строку,

$t1 = GetData "(local)" 1 1
$t2 = GetData "(local)" 2 5

сбой с

Ошибка вызова метода, поскольку [System.Data.DataRow] не содержит метод с именем «op_Addition».

Похоже, что функция GetData возвращает не DataTable, а DataRow, если в результате есть только одна строка.

Я попробовал это, но это тоже не решение:

$all = $t1
$all.Rows.Add($t2)

Выдает либо «Коллекция была фиксированного размера» ($t1 имеет 2 строки), либо «Вы не можете вызвать метод для выражения с нулевым значением» ($t1 содержит 1 строку).

Может ли кто-нибудь пролить свет на то, почему результат, состоящий только из 1 строки, вызывает поведение, отличное от нескольких строк?

(Редактировать: привлечь внимание к примеру / небезопасной природе сценария)

1 Ответ

0 голосов
/ 10 января 2019

Вы - жертва PowerShell, пытаясь быть слишком дружелюбным

Когда PowerShell видит DataTable, передаваемую из функции в конвейере, он пытается распутать его и отправить отдельные строки в нисходящем направлении по очереди.

Это может быть чрезвычайно полезно при фильтрации набора данных, например, из запроса к базе данных, но, очевидно, крайне разочаровывает, если вы полагаетесь на некоторое внутреннее поведение DataTable.

Чтобы PowerShell не распутывал строки, используйте Write-Output $table -NoEnumerator или - если вы добавляете атрибут CmdletBinding к своей функции - вы можете напрямую вызвать $PSCmdlet.WriteObject($table,$false):

function GetData($dbserver, $start, $end)
{
    $qry = "WITH q AS (SELECT  $start AS num UNION ALL SELECT num + 1 FROM q WHERE num < $end) SELECT * FROM q"
    $con = New-Object System.Data.SqlClient.SqlConnection
    $con.ConnectionString = "server=$dbserver;database=A2SDataCentral;Integrated Security=SSPI"
    $con.Open()
    $com = $con.CreateCommand()
    $com.CommandText = $qry
    $res = $com.ExecuteReader()
    $table = New-Object System.Data.DataTable
    $table.Load($res)

    Write-Output $table -NoEnumerate
}

или

function GetData
{
    [CmdletBinding()]
    param($dbserver, $start, $end)

    $qry = "WITH q AS (SELECT  $start AS num UNION ALL SELECT num + 1 FROM q WHERE num < $end) SELECT * FROM q"
    $con = New-Object System.Data.SqlClient.SqlConnection
    $con.ConnectionString = "server=$dbserver;database=A2SDataCentral;Integrated Security=SSPI"
    $con.Open()
    $com = $con.CreateCommand()
    $com.CommandText = $qry
    $res = $com.ExecuteReader()
    $table = New-Object System.Data.DataTable
    $table.Load($res)

    $PSCmdlet.WriteObject($table,$false)
}

Бонус проверки безопасности!

Я не могу с чистой совестью ответить на этот вопрос, не указав, что ваша функция в настоящее время подвержена простейшей форме внедрения SQL.

Что если пользователь должен передать нечисловое значение в качестве значения аргумента параметра $start или $end? Что делать, если кто-то решил сделать:

$untrustedUserInput = '0 OR 1 = 1; xp_cmdshell "cmd /c calc.exe" --'

Поздравляю, теперь я владею вашим сервером базы данных :)

В этом конкретном случае вы можете легко снизить этот риск, но строго набрав свои параметры:

function GetData([string]$dbServer, [int]$start, [int]$end)
{
...

или

param([string]$dbServer, [int]$start, [int]$end)

Лучшее решение - параметризовать ваш SQL-запрос:

$qry = "WITH q AS (SELECT @start AS num UNION ALL SELECT num + 1 FROM q WHERE num < @end) SELECT * FROM q"
# ...
$com.Parameters.Add('@start','Int').Value = $start
$com.Parameters.Add('@end','Int').Value = $end

Таким образом, никакие злые строковые значения не попадут в запрос без очистки

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...