lib/functions/Private/Get-SqlScriptFragment.ps1

function Get-SqlScriptFragment {
    <#
    .SYNOPSIS
        Parses a T-SQL script with ScriptDom once and hands back both the parse errors and the
        syntax tree.
 
    .DESCRIPTION
        All three validation passes of issue #61 read the same tree: the syntax pass maps the parse
        errors, the schema pass resolves identifiers out of the AST, and the Omada compatibility pass
        runs its rule predicates over it. Parsing three times per debounced keystroke would be three
        times the work for one answer, so the parse lives here and every pass is handed the result.
 
        The return shape mirrors Get-SqlSyntaxDiagnostic's, for the same reason: "nothing to report"
        and "could not look" are different answers.
 
            Status Ok the script was parsed; Fragment and ParseError are populated
                           Unavailable ScriptDom is not loaded, so nothing was parsed
            Fragment the TSqlScript root, or $null
            ParseError the parse errors ScriptDom reported; empty for a clean script
            ParserVersion the TSqlNNNParser actually used, or $null
 
        Unavailable is a return value, never an exception (issue #61 acceptance criterion 6).
 
        A script that does not parse still yields a Fragment: ScriptDom recovers, and a partial tree
        lets the later passes say something useful about the statements it did understand. Callers
        that need a trustworthy tree check ParseError themselves.
 
    .PARAMETER SqlText
        The script to parse. Null, empty and whitespace-only input are valid and yield Status Ok with
        Fragment $null - there is nothing to inspect, which is not the same as a parser that could not
        look. Every caller already treats a null Fragment as "nothing to say about this script".
 
    .PARAMETER ParserVersion
        An explicit TSqlNNNParser to use. Omit to take the newest parser the loaded assembly ships.
 
    .OUTPUTS
        [PSCustomObject] with Status, Fragment, ParseError and ParserVersion.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false, Position = 0)]
        [AllowNull()]
        [AllowEmptyString()]
        [string]$SqlText,
        [Parameter(Mandatory = $false)]
        [AllowNull()]
        [AllowEmptyString()]
        [string]$ParserVersion
    )

    # DELIBERATELY no $Script:Tracer preamble. $PSBoundParameters IS the user's query text here, and
    # the preamble would copy it into the trace on every debounced keystroke (issue #61 section 5).

    try {
        $ParserType = Get-SqlParserType -ParserVersion $ParserVersion

        if ($null -eq $ParserType) {
            return [PSCustomObject]@{
                Status        = "Unavailable"
                Fragment      = $null
                ParseError    = @()
                ParserVersion = $null
            }
        }

        if ([string]::IsNullOrWhiteSpace($SqlText)) {
            return [PSCustomObject]@{
                Status        = "Ok"
                Fragment      = $null
                ParseError    = @()
                ParserVersion = $ParserType.Name
            }
        }

        # initialQuotedIdentifiers: $true matches the SET QUOTED_IDENTIFIER ON that .NET clients -
        # Omada's included - connect under. It decides what "..." means: a delimited identifier under
        # ON, a string literal under OFF. The schema pass reads identifiers out of this tree, so the
        # setting has to be the tenant's.
        $Parser = $ParserType::new($true)

        $ParseError = $null
        $Reader = [System.IO.StringReader]::new($SqlText)
        try {
            $Fragment = $Parser.Parse($Reader, [ref]$ParseError)
        }
        finally {
            $Reader.Dispose()
        }

        return [PSCustomObject]@{
            Status        = "Ok"
            Fragment      = $Fragment
            ParseError    = @(@($ParseError) | Where-Object { $null -ne $_ })
            ParserVersion = $ParserType.Name
        }
    }
    catch {
        # A parser that throws must not be worse than a parser that is missing. The exception message
        # can quote the script, so it is not logged.
        "The T-SQL parser failed while reading the query; client-side diagnostics are unavailable for this run." | Write-LogOutput -LogType DEBUG

        return [PSCustomObject]@{
            Status        = "Unavailable"
            Fragment      = $null
            ParseError    = @()
            ParserVersion = $null
        }
    }
}