I'm trying to use tSQLt AssertResultSetsHaveSameMetaData to check the metadata from a query that returns a large number of columns.
When it fails the message that details the 'expected/but was' details is truncated, so I can't the two pieces of information to see what is wrong.
Is there a way to output the message so that it doesn't truncate (for example, to a file)?
It depends on what you are actually testing. I agree that on wide result sets the output from AssertResultsSetsHaveSameMetaData can be a little unwieldy. For stored procedures you would write your tests like this:
create procedure [ProcedureTests].[test SelectProcedure result set contract]
as
begin
create table #expected
(
name varchar(500) not null
, column_ordinal int not null identity(1,1)
, system_type_name varchar(500) not null
, Nullability varchar(16) not null
)
; with expectedCte (name, system_type_name, Nullability)
as
(
select 'ItemId' , 'int' , 'not null'
union all select 'ActorId' , 'int' , 'not null'
union all select 'LanId' , 'nvarchar(200)' , 'not null'
union all select 'ConsumerId' , 'int' , 'not null'
union all select 'ConsumerMoniker' , 'nvarchar(200)' , 'not null'
union all select 'ProfileTypeId' , 'int' , 'not null'
union all select 'ProfileTypeName' , 'varchar(50)' , 'not null'
union all select 'ProfileId' , 'int' , 'not null'
)
insert #expected
(
name
, system_type_name
, Nullability
)
select name, system_type_name, Nullability from expectedCte;
--! Act
select
name
, column_ordinal
, system_type_name
, case is_nullable when 1 then 'null' else 'not null' end as [Nullability]
into
#actual
from
sys.dm_exec_describe_first_result_set_for_object(object_id('mySchema.SelectProcedure'), 0);
--! Assert
exec tSQLt.AssertEqualsTable #expected, #actual;
end;
go
Whilst for views you could use this (slightly different) approach:
alter procedure [ViewTests].[test ViewName resultset contract]
as
begin
create table [ViewTests].[expected]
(
TransactionId int not null
, SourceId int not null
, SourceKey nvarchar(50) not null
, TransactionTypeId int not null
, TransactionStatusId int not null
, LastModified datetime not null
);
--! You comparison may be as simple as this (but see alternative approach below)
exec tSQLt.AssertEqualsTableSchema '[ViewTests].[expected]', 'mySchema.ViewName';
--!
--! Seems that is_nullable column on dm_exec_describe_first_result_set (used by tSQLt.AssertEqualsTableSchema)
--! can be a bit flakey where views are concerned so you may need to ignore nullability when testing
--! this view (so comment out that column in both SELECTs)
--!
select
c.name as [ColumnName]
, c.column_id as [ColumnPosition]
, case
when st.name in ('char', 'varchar', 'varbinary')
then st.name + '(' + case when c.max_length = -1 then 'max' else coalesce(cast(c.max_length as varchar(8)), '???') end + ')'
when st.name in ('nchar', 'nvarchar')
then st.name + '(' + case when c.max_length = -1 then 'max' else coalesce(cast(c.max_length / 2 as varchar(8)), '???') end + ')'
when st.name in ('decimal', 'numeric')
then st.name + '(' + coalesce(cast(c.precision as varchar(8)), '???') + ',' + coalesce(cast(c.scale as varchar(8)), '???') + ')'
when st.name in ('time', 'datetime2', 'datetimeoffset')
then st.name + '(' + coalesce(cast(c.precision as varchar(8)), '???') + ')'
else st.name
end as [DataType]
, c.[precision] as [NumericScale]
, c.scale as [NumericPrecision]
, c.collation_name as [CollationName]
, cast(case c.is_nullable when 1 then 'null' else 'not null' end as varchar(16)) as [Nullability]
into
#expected
from
sys.columns as c
inner join sys.types as st
on st.system_type_id = c.system_type_id
and st.user_type_id = c.user_type_id
where
c.[object_id] = object_id('[ViewTests].[expected]')
select
name as [ColumnName]
, column_ordinal as [ColumnPosition]
, system_type_name as [DataType]
, [precision ]as [NumericScale]
, scale as [NumericPrecision]
, collation_name as [CollationName]
, cast(case is_nullable when 1 then 'null' else 'not null' end as varchar(16)) as [Nullability]
into
#actual
from
sys.dm_exec_describe_first_result_set('select * from mySchema.ViewName, null, null)
exec tSQLt.AssertEqualsTable '#expected', '#actual' ;
end
go
In the even of any failures, the reason will be much clearer as the output is more like AssertEqualsTable.
You could try EXEC [tSQLt].[XmlResultFormatter]; before running your test. This is intended for use in "build server" scenarios, but could probably be pressed into service to show you more of the output in SSMS.
Related
The search filter in the function below doesn't seem to work. If I don't provide a search parameter, it works, otherwise I get no recordset back. I'm assuming I am making a mess of the single-quoting and ILIKE, but not sure how to re-write this properly. Suggestions?
CREATE OR REPLACE FUNCTION get_operator_basic_by_operator(
_operator_id UUID DEFAULT NULL,
_search TEXT DEFAULT NULL,
_page_number INTEGER DEFAULT 1,
_page_size INTEGER DEFAULT 10,
_sort_col TEXT DEFAULT 'username',
_sort_dir TEXT DEFAULT 'asc',
_include_deleted BOOLEAN DEFAULT FALSE
)
RETURNS TABLE (
id UUID,
party_id UUID,
party_name TEXT,
username VARCHAR(32),
profile_picture_uri VARCHAR(512),
first_name VARCHAR(64),
last_name VARCHAR(64),
street VARCHAR(128),
specifier VARCHAR(128),
city VARCHAR(64),
state VARCHAR(2),
zipcode VARCHAR(9),
primary_email CITEXT,
primary_phone VARCHAR(10),
secondary_email CITEXT,
secondary_phone VARCHAR(10),
last_login TIMESTAMP WITH TIME ZONE,
created TIMESTAMP WITH TIME ZONE,
deleted TIMESTAMP WITH TIME ZONE
)
AS $$
DECLARE
_offset BIGINT;
BEGIN
IF (_page_number < 1 OR _page_number IS NULL) THEN
RAISE EXCEPTION '_page_number cannot be null or less than 1.';
END IF;
IF (_page_size < 1 OR _page_size IS NULL) THEN
RAISE EXCEPTION '_page_size cannot be null or less than 1.';
END IF;
IF (_sort_dir <> 'asc' AND _sort_dir <> 'desc') THEN
RAISE EXCEPTION '_sort_dir must be "asc" or "desc".';
END IF;
_offset := (_page_size * (_page_number-1));
RETURN QUERY EXECUTE '
SELECT
o.id,
p.id,
p.party_name,
o.username,
o.profile_picture_uri,
o.first_name,
o.last_name,
o.street,
o.specifier,
o.city,
o.state,
o.zipcode,
o.primary_email,
o.primary_phone,
o.secondary_email,
o.secondary_phone,
o.last_login,
o.created,
o.deleted
FROM
operator o
LEFT JOIN
party p ON o.party_id = p.id
WHERE ( -- include all or only those active, based on _include_deleted
$1 OR o.deleted > statement_timestamp()
)
AND o.party_id IN ( -- limit to operators in same party
SELECT oi.party_id FROM operator oi WHERE oi.id = $2
)
AND ( -- use optional search filter
$3 IS NULL
OR
o.username ILIKE ''%$3%''
OR
o.first_name ILIKE ''%$3%''
OR
o.last_name ILIKE ''%$3%''
OR
o.primary_email ILIKE ''%$3%''
)
ORDER BY ' || quote_ident(_sort_col) || ' ' || _sort_dir || '
LIMIT
$4
OFFSET
$5'
USING _include_deleted, _operator_id, _search, _page_size, _offset;
END;
The parameter is inserted as a text with quotes, so you should use it this way:
...
o.username ILIKE concat(''%'', $3, ''%'')
...
Personally I would use format() and dollar-quotes.
Strings handles as is, you should to pass ready to use values:
EXECUTE '... o.username ILIKE $3 ...' using ..., '%' || _search || '%', ...
I need to parse a full name in the format, prefix first middle last suffix, but not all parts may be included. I have the prefix first middle and last working, but Jr gets stuffed in with the last name. How do I get the suffix to come out in a suffix column? Example includes data.
SELECT
FIRST_NAME.INPUT_DATA
,FIRST_NAME.PREFIX
,FIRST_NAME.FIRST_NAME
,CASE WHEN 0 = CHARINDEX(' ',FIRST_NAME.REMAINING)
THEN NULL --no more spaces found, consider remaining to be last name
ELSE SUBSTRING(
FIRST_NAME.REMAINING
,1
,CHARINDEX(' ',FIRST_NAME.REMAINING)-1
)
END AS MIDDLE_NAME
,SUBSTRING(
FIRST_NAME.REMAINING
,1 + CHARINDEX(' ',FIRST_NAME.REMAINING)
,LEN(FIRST_NAME.REMAINING)
) AS LAST_NAME
FROM
(
SELECT
PREFIX.PREFIX
,CASE WHEN 0 = CHARINDEX(' ',PREFIX.REMAINING)
THEN PREFIX.REMAINING --no space found, return the entire string
ELSE SUBSTRING(
PREFIX.REMAINING
,1
,CHARINDEX(' ',PREFIX.REMAINING)-1
)
END AS FIRST_NAME
,CASE WHEN 0 = CHARINDEX(' ',PREFIX.REMAINING)
THEN NULL --no spaces found, consider to be first name
ELSE SUBSTRING(
PREFIX.REMAINING
,CHARINDEX(' ',PREFIX.REMAINING)+1
,LEN(PREFIX.REMAINING)
)
END AS REMAINING
,PREFIX.INPUT_DATA
FROM
(
SELECT --CLEAN_DATA
--if first three characters match list,
--parse as a "PREFIX". else return NULL for PREFIX.
CASE WHEN SUBSTRING(CLEAN_DATA.FULL_NAME,1,3) IN ('MR ','MS ','DR ','MRS')
THEN LTRIM(RTRIM(SUBSTRING(CLEAN_DATA.FULL_NAME,1,3)))
ELSE NULL
END AS PREFIX
,CASE WHEN SUBSTRING(CLEAN_DATA.FULL_NAME,1,3) IN ('MR ','MS ','DR ','MRS')
THEN LTRIM(RTRIM(SUBSTRING(CLEAN_DATA.FULL_NAME,4,LEN(CLEAN_DATA.FULL_NAME))))
ELSE LTRIM(RTRIM(CLEAN_DATA.FULL_NAME))
END AS REMAINING
,CLEAN_DATA.INPUT_DATA
FROM
(
SELECT
--trim leading & trailing spaces to prepare for processing
--replace extra spaces in name
REPLACE(REPLACE(LTRIM(RTRIM(FULL_NAME)),' ',' '),' ',' ') AS FULL_NAME
,FULL_NAME AS INPUT_DATA
FROM
(
--test with test data, or table
--table
--SELECT CONTACT AS FULL_NAME
--FROM CONTACT
--test data
--/*
SELECT 'Andy D Where' AS FULL_NAME
UNION SELECT 'Cathy T Landers' AS FULL_NAME
UNION SELECT 'Ms Annie Wint There' AS FULL_NAME
UNION SELECT 'Frank Fields' AS FULL_NAME
UNION SELECT 'Howdy U Pokes Jr.' AS FULL_NAME
--*/
) SOURCE_DATA
) CLEAN_DATA
) PREFIX
) FIRST_NAME
--credits to JStyons of course
Hope this helps. I have only added Generational SUFFIX titles(Sr, Jr), If more are needed you could add to the Case statement as needed. I am also assuming that your Db is case insensitive.
Assumption (Business Rules):
First Name has no spaces
Middle Name has no spaces
Last name has no spaces
Prefix's are only of the form 'MR ','MS ','DR ','MRS' with no period "."
Suffix's are only of the form 'Sr', 'Jr', 'Sr.', 'Jr.'
The Database is case insensitive
IF OBJECT_ID('tempdb..#cte_SpaceFix') IS NOT NULL
DROP TABLE #cte_SpaceFix
;WITH cte_OriginalData (FullName)
AS (
SELECT 'Andy D Where'
UNION
SELECT 'Cathy T Landers'
UNION
SELECT 'Ms Annie Wint There'
UNION
SELECT 'Ms Annie Wint There Jr'
UNION
SELECT 'Mrs Annie There Jr'
UNION
SELECT 'Frank Fields'
UNION
SELECT 'Howdy U Pokes Jr.'
UNION
SELECT 'Howdy U Pokes Sr.'
UNION
SELECT 'Cathy T Landers Jr'
UNION
SELECT 'Landers Jr'
)
,cte_FullNameRemoveTail AS
(
SELECT LTRIM(RTRIM(FullName)) AS FullName
FROM cte_OriginalData
)
,cte_Parse_Prefix(Prefix,FullFirst_Prefix,FullName) AS
(
SELECT CASE
WHEN SUBSTRING(FullName, 1, 3) IN ('MR ','MS ','DR ','MRS')
THEN LTRIM(RTRIM(SUBSTRING(FullName, 1, 3)))
ELSE NULL
END AS Prefix,
CASE
WHEN SUBSTRING(FullName, 1, 3) IN ('MR ','MS ','DR ','MRS')
THEN LTRIM(RTRIM(SUBSTRING(FullName, 4, 8000)))
ELSE LTRIM(RTRIM(FullName))
END AS FullFirst_Prefix,
FullName
FROM cte_FullNameRemoveTail
)
,cte_Parse_Suffix(Prefix,FullFirst_Prefix_Suffix,Suffix,FullName) AS
(
SELECT Prefix,
CASE
WHEN RIGHT(FullFirst_Prefix,3) = ' JR' THEN LTRIM(RTRIM(SUBSTRING(FullFirst_Prefix,1,LEN(FullFirst_Prefix)-3)))
WHEN RIGHT(FullFirst_Prefix,4) = ' JR.' THEN LTRIM(RTRIM(SUBSTRING(FullFirst_Prefix,1,LEN(FullFirst_Prefix)-4)))
WHEN RIGHT(FullFirst_Prefix,3) = ' SR' THEN LTRIM(RTRIM(SUBSTRING(FullFirst_Prefix,1,LEN(FullFirst_Prefix)-3)))
WHEN RIGHT(FullFirst_Prefix,4) = ' SR.' THEN LTRIM(RTRIM(SUBSTRING(FullFirst_Prefix,1,LEN(FullFirst_Prefix)-4)))
ELSE LTRIM(RTRIM(FullFirst_Prefix))
END AS FullFirst_Prefix_Suffix,
CASE
WHEN RIGHT(FullFirst_Prefix,3) = ' JR'
OR RIGHT(FullFirst_Prefix,4) = ' JR.'
THEN 'Jr'
WHEN RIGHT(FullFirst_Prefix,3) = ' SR'
OR RIGHT(FullFirst_Prefix,4) = ' SR.'
THEN 'Sr'
ELSE NULL
END AS Suffix,
FullName
FROM cte_Parse_Prefix
)
,cte_SpaceFix(Prefix, FullFirst_Prefix_Suffix, Suffix, FullName) AS
(
SELECT Prefix,
CASE
WHEN LEN(FullFirst_Prefix_Suffix) - LEN(REPLACE(FullFirst_Prefix_Suffix, ' ', '')) > 2 THEN REPLACE(REPLACE(REPLACE(REPLACE(FullFirst_Prefix_Suffix,SPACE(5), SPACE(1)),SPACE(4), SPACE(1)),SPACE(3), SPACE(1)),SPACE(2), SPACE(1))
ELSE FullFirst_Prefix_Suffix
END AS FullFirst_Prefix_Suffix,
Suffix,
FullName
FROM cte_Parse_Suffix
)
SELECT * INTO #cte_SpaceFix
FROM cte_SpaceFix
;WITH cte_Parse_FirstName(Prefix, FirstName, Suffix, FullFirst_Prefix_Suffix_FirstName, FullName) AS
(
SELECT Prefix,
CASE
WHEN FullFirst_Prefix_Suffix IS NULL THEN NULL
WHEN LEN(FullFirst_Prefix_Suffix) - LEN(REPLACE(FullFirst_Prefix_Suffix, ' ', '')) >= 1 THEN LEFT(FullFirst_Prefix_Suffix,CHARINDEX(' ',FullFirst_Prefix_Suffix))
WHEN LEN(FullFirst_Prefix_Suffix) - LEN(REPLACE(FullFirst_Prefix_Suffix, ' ', '')) = 0 THEN FullFirst_Prefix_Suffix
ELSE NULL
END AS FirstName,
Suffix,
CASE
WHEN FullFirst_Prefix_Suffix IS NULL THEN NULL
WHEN LEN(FullFirst_Prefix_Suffix) - LEN(REPLACE(FullFirst_Prefix_Suffix, ' ', '')) >= 1 THEN LTRIM(RTRIM(REPLACE(FullFirst_Prefix_Suffix,LEFT(FullFirst_Prefix_Suffix,CHARINDEX(' ',FullFirst_Prefix_Suffix)),'')))
WHEN LEN(FullFirst_Prefix_Suffix) - LEN(REPLACE(FullFirst_Prefix_Suffix, ' ', '')) = 0 THEN NULL
ELSE NULL
END AS FullFirst_Prefix_Suffix_FirstName,
FullName
FROM #cte_SpaceFix
)
,cte_Parse_LastName(Prefix, FirstName, LastName, Suffix, MiddleName, FullName) AS
(
SELECT Prefix,
FirstName,
CASE
WHEN FullFirst_Prefix_Suffix_FirstName IS NULL THEN NULL
WHEN LEN(FullFirst_Prefix_Suffix_FirstName) - LEN(REPLACE(FullFirst_Prefix_Suffix_FirstName, ' ', '')) >= 1 THEN SUBSTRING(FullFirst_Prefix_Suffix_FirstName,CHARINDEX(' ',FullFirst_Prefix_Suffix_FirstName)+1,8000)
WHEN LEN(FullFirst_Prefix_Suffix_FirstName) - LEN(REPLACE(FullFirst_Prefix_Suffix_FirstName, ' ', '')) = 0 THEN FullFirst_Prefix_Suffix_FirstName
ELSE NULL
END AS LastName,
Suffix,
CASE
WHEN FullFirst_Prefix_Suffix_FirstName IS NULL THEN NULL
WHEN LEN(FullFirst_Prefix_Suffix_FirstName) - LEN(REPLACE(FullFirst_Prefix_Suffix_FirstName, ' ', '')) >= 1 THEN LEFT(FullFirst_Prefix_Suffix_FirstName,CHARINDEX(' ',FullFirst_Prefix_Suffix_FirstName))
ELSE NULL
END AS MiddleName,
FullName
FROM cte_Parse_FirstName
)
SELECT Prefix, FirstName, MiddleName, LastName, Suffix--, FullName
FROM cte_Parse_LastName
IF OBJECT_ID('tempdb..#cte_SpaceFix') IS NOT NULL
DROP TABLE #cte_SpaceFix
This is part of the T-SQL.I am getting below error. Can anyone guide me.The issue is because of the value column which is of nvarchar datatype
SELECT RuleID, SourceID, DataFileID, ChildCount, DP_State
FROM
(SELECT DP_State.RuleID, CAST(DP_State.SourceID AS VARCHAR(20)) AS SourceID, CAST(DP_State.DataFileID AS VARCHAR(20)) AS DataFileID, ChildCount, DP_State
FROM (
SELECT RuleID ,
RuleResultID ,
CASE WHEN ISNUMERIC(ISNULL([ResultValue], 0)) = 1 THEN
CAST(ISNULL([Value], 0) AS BIGINT)
ELSE
-1
END AS ChildCount,
Error I am getting :
Try this if you are using SQL Server 2012 or later
SELECT RuleID,
SourceID,
DataFileID,
ChildCount,
DP_State
FROM (
SELECT DP_State.RuleID,
CAST(DP_State.SourceID AS VARCHAR(20)) AS SourceID,
CAST(DP_State.DataFileID AS VARCHAR(20)) AS DataFileID,
ChildCount,
DP_State
FROM (
SELECT RuleID,
RuleResultID,
CASE
WHEN TRY_CONVERT(INT, ISNULL([ResultValue],0)) IS NOT NULL
THEN CAST(ISNULL([Value], 0) AS BIGINT)
ELSE - 1
END AS ChildCount,
)
)
You are implicitly casting to an integer by first checking [value] against 0 in ISNULL(). Only after that you are casting to an integer, instead try the following:
Evaluate against a string:
CAST(ISNULL([Value], '0') AS BIGINT)
Or first cast to an integer:
ISNULL(CAST([Value] AS BIGINT), 0)
the array_to_string is returning text (916-555-1212), however postgresql is treating it as set-operation even with explicit ::text casting.
select nullif(
array_to_string(
regexp_matches('9165551212', '(\d{3})?(\d{3})(\d{4})')::text[]
,'-')::text
, '');
ERROR: NULLIF does not support set arguments
yet I can use char_length which expects text and it works
select char_length(
array_to_string(
regexp_matches('9165551212', '(\d{3})?(\d{3})(\d{4})')::text[]
,'-')::text
)
char_length
-------------
12
yet wrap even that in a nullif and same error
select nullif(
char_length(
array_to_string(
regexp_matches('9165551212', '(\d{3})?(\d{3})(\d{4})')::text[]
,'-')::text
)
,12)
ERROR: NULLIF does not support set arguments
I had the same problem and it seems to be related to the regexp_matches function:
select nullif( (regexp_matches( '123', '(2)' ) )[1] , '' )
; -- ERROR: NULLIF does not support set arguments
select nullif( (regexp_matches( '123', '(2)' )::text[])[1]::text, '' )
; -- ERROR: NULLIF does not support set arguments
select nullif( ( select (regexp_matches( '123', '(2)' ) )[1] ), '' )
; -- ok: gives "2"
so just wrapping the result in a sub-select seems to solve it here as well :-/
Weird... It works if you do
select
nullif(
(
select array_to_string(
regexp_matches(
'9165551212',
'(\d{3})?(\d{3})(\d{4})'
)::text[]
, '-' )
)
, '')
I have an unusual situation. Please consider the following code:
IF OBJECT_ID('tempdb..#CharacterTest') IS NOT NULL
DROP TABLE #CharacterTest
CREATE TABLE #CharacterTest
(
[ID] int IDENTITY(1, 1) NOT NULL,
[CharField] varchar(50) NULL
)
INSERT INTO #CharacterTest (CharField)
VALUES ('A')
, ('A')
, ('A')
, ('A')
, ('B')
, ('B')
, ('B')
, ('C')
, ('C')
, ('D')
, ('D')
, ('F')
, ('G')
, ('H')
, ('I')
, ('J')
, ('K')
, ('L')
, ('M')
, ('N')
, (' ')
, (' ')
, (' ')
, (NULL)
, ('');
I would like a query which gives me a character string like this:
A (16%), B (12%), C(8%)
Please notice the following:
I don't want to have empty strings, strings with all blanks, or nulls listed in the top 3, but I do want the percentage of values calculated using the entire record count for the table.
Ties can be ignored, so if there were 22 values in the list with 8% frequency, it's alright to simply return whichever one is first.
Percentages can be rounded to whole numbers.
I'd like to find the easiest way to write this query while still retaining T-SQL compatibility back to SQL Server 2005. What is the best way to do this? Window Functions?
I'd go for.
WITH T1
AS (SELECT [CharField],
100.0 * COUNT(*) OVER (PARTITION BY [CharField]) /
COUNT(*) OVER () AS Pct
FROM #CharacterTest),
T2
AS (SELECT DISTINCT TOP 3 *
FROM T1
WHERE [CharField] <> '' --Excludes all blank or NULL as well
ORDER BY Pct DESC)
SELECT STUFF((SELECT ',' + [CharField] + ' (' + CAST(CAST(ROUND(Pct,1) AS INT) AS VARCHAR(3)) + ')'
FROM T2
ORDER BY Pct DESC
FOR XML PATH('')), 1, 1, '') AS Result
My first attempt would probably be this. Not saying that it's the best way to handle it, but that it would work.
DECLARE #TotalCount INT
SELECT #TotalCount = COUNT(*) FROM #CharacterTest AS ct
SELECT TOP(3) CharField, COUNT(*) * 1.0 / #TotalCount AS OverallPercentage
FROM #CharacterTest AS ct
WHERE CharField IS NOT NULL AND REPLACE(CharField, ' ', '') <> ''
GROUP BY CharField
ORDER BY COUNT(*) desc
DROP TABLE #CharacterTest
This should get the character string you need:
declare #output varchar(200);
with cte as (
select CharField
, (count(*) * 100) / (select count(*) from #CharacterTest) as CharPct
, row_number() over (order by count(*) desc, CharField) as RowNum
from #CharacterTest
where replace(CharField, ' ', '') not like ''
group by CharField
)
select #output = coalesce(#output + ', ', '') + CharField + ' (' + cast(CharPct as varchar(11)) + '%)'
from cte
where RowNum <= 3
order by RowNum;
select #output;
-- Returns:
-- A (16%), B (12%), C (8%)
I would draw attention to storing a single character in a varchar(50) column, however.