Access pass-through query passing comma separated list as a parameter to SQL stored procedure - tsql

The SQL server is 2008. I have an Access 2016 front-end for reporting purposes. One report requires that one or more Product Classes from a list be chosen to report on. I have the VBA that creates the pass-through query with the appropriate single line:
exec dbo.uspINVDAYS 'A3,A4,A6,AA,AB'
I have this SQL code that should take the list as hard-coded here:
DECLARE #parProductClasses NVARCHAR(200) = 'A3,A4,A6,AA,AB';
DECLARE #ProductClasses NVARCHAR(200),#delimiter NVARCHAR(1) = ',';
SET #ProductClasses = #parProductClasses;
DECLARE #DAYS INT,#numDAYS int;
SET #DAYS = 395;
SET #numDAYS = #DAYS;
SELECT UPINVENTORY.StockCode, UPINVENTORY.[Description], UPINVENTORY.Supplier, UPINVENTORY.ProductClass
, UPINVENTORY.WarehouseToUse
, CAST(UPINVENTORY.Ebq AS INT)Ebq
, cast(UPINVENTORY.QtyOnHand AS INT)QtyOnHand
, cast(UPINVENTORY.PrevYearQtySold AS INT)PrevYearQtySold
, cast(UPINVENTORY.YtdQtyIssued AS INT)YtdQtyIssued
,#numDAYS as numDAYS
,CAST(ROUND((PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS,0) AS INT)TOTAL
,CASE WHEN (PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS
= 0 THEN 0
ELSE CAST(ROUND(QTYONHAND/((PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS),0)AS INT)
END FINAL
,CASE WHEN (PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS
= 0 THEN 0
ELSE CAST(ROUND(QTYONHAND/((PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS),0)AS INT)
END FINAL1
FROM
TablesCoE.dbo.vwRPUpInventory UPINVENTORY
WHERE UPINVENTORY.ProductClass IN (Select val From TablesCoE.dbo.split(#ProductClasses,','));
When I run this I get:
Msg 468, Level 16, State 9, Line 9
Cannot resolve the collation conflict between "SQL_Latin1_General_CP1_CI_AS" and "Latin1_General_BIN" in the equal to operation.
I cannot determine where
COLLATE SQL_Latin1_General_CP1_CI_AS
should go. Where am I equating or comparing? The SQL IN clause cannot handle the comma-separated list since it is not a strict SQL table.
Here's the code used to create the dbo.split() function:
CREATE FUNCTION dbo.split(
#delimited NVARCHAR(MAX),
#delimiter NVARCHAR(100)
) RETURNS #t TABLE (id INT IDENTITY(1,1), val NVARCHAR(MAX))
AS
BEGIN
DECLARE #xml XML
SET #xml = N'<t>' + REPLACE(#delimited,#delimiter,'</t><t>') + '</t>'
INSERT INTO #t(val)
SELECT r.value('.','varchar(MAX)') as item
FROM #xml.nodes('/t') as records(r)
RETURN
END
Thanks to Sandeep Mittal and I am sure others have very similar split functions. Run separately this function does operate as expected and provides a table of the comma-separated list objects.
DECLARE #parProductClasses NVARCHAR(200) = 'A3,A4,A6,AA,AB';
DECLARE #ProductClasses NVARCHAR(200),#delimiter NVARCHAR(1) = ',';
SET #ProductClasses = #parProductClasses;
Select val From TablesCoE.dbo.split(#ProductClasses,',')
Returns
val
A3
A4
A6
AA
AB

try this.
WHERE concat(',',#ProductClasses,',') like concat('%',UPINVENTORY.ProductClass,'%')
it's a silly way of checking if your productClass is within the #productClasses list.

After attempting to use a prefabricated table-valued variable versus on the fly in the WHERE clause, neither worked, I then started to try different placements of the COLLATE statement. I was complacent in applying COLLATE to the right-side with the collation listed on the left in the SQL error message. I tried the collation listed on the right of the SQL error message to the left side of the WHERE clause and the SQL code works to spec now. Here it is:
DECLARE #parProductClasses NVARCHAR(200) = 'A3,A4,A6,AA,AB';
DECLARE #ProductClasses NVARCHAR(200),#delimiter NVARCHAR(1) = ',';
SET #ProductClasses = #parProductClasses;
DECLARE #DAYS INT,#numDAYS int;
SET #DAYS = 395;
SET #numDAYS = #DAYS;
SELECT UPINVENTORY.StockCode, UPINVENTORY.[Description], UPINVENTORY.Supplier, UPINVENTORY.ProductClass
, UPINVENTORY.WarehouseToUse
, CAST(UPINVENTORY.Ebq AS INT)Ebq
, cast(UPINVENTORY.QtyOnHand AS INT)QtyOnHand
, cast(UPINVENTORY.PrevYearQtySold AS INT)PrevYearQtySold
, cast(UPINVENTORY.YtdQtyIssued AS INT)YtdQtyIssued
,#numDAYS as numDAYS
,CAST(ROUND((PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS,0) AS INT)TOTAL
,CASE WHEN (PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS
= 0 THEN 0
ELSE CAST(ROUND(QTYONHAND/((PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS),0)AS INT)
END FINAL
,CASE WHEN (PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS
= 0 THEN 0
ELSE CAST(ROUND(QTYONHAND/((PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS),0)AS INT)
END FINAL1
FROM
TablesCoE.dbo.vwRPUpInventory UPINVENTORY
WHERE UPINVENTORY.ProductClass COLLATE Latin1_General_BIN IN (SELECT val FROM TablesCoE.dbo.split(#ProductClasses,','));
Thanks for your suggestions #Krish and #Isaac.
Tim

Related

How to concat two vars in while loop

I'd like to construct a 'union all' query with a while loop.
I've already tried += concatenation but doesn't work.
DECLARE #cnt1 int , #concat nvarchar(max), #qry nvarchar(500);
SET #cnt1 = 1;
WHILE #cnt1 < 99
BEGIN
SET #qry = 'select name_' + CAST(#cnt1 AS CHAR) + ' , name2_' + CAST(#cnt1 AS CHAR) + ', m.state1 FROM table1 P left join table2 M on M.name = P.name_' + CAST(#cnt1 AS CHAR) + ' where p.nb > 1';
SET #cnt1 = #cnt1 + 1;
SET #concat += ' UNION ALL ' + #qry
END
EXEC sp_executesql #concat
#concat is still empty at the end of the loop...
Thank you vm
Since #concat isn't initialized, the default value is null. Concatenating a value with null results in null, so no progress is made in your loop. Initializing it to an empty string:
declare #Concat as NVarChar(max) = N'';
will fix the problem.
Tip: The default length of a Char, VarChar, NChar or NVarChar is one character most of the time. When it is the target type of a Cast or Convert then it is 30 characters. Best practice: Always specify a length.

Parameterize Bulk Insert for Microsoft SQL Server 2014

I am trying to parameterize the LASTROW argument of a BULK INSERT query.
The last row of data in the textfile I am importing in to SQL Server contains information about the number of rows in the textfile. And I plan to use that number to populate the value of the LASTROW argument of the BULK INSERT query.
An example of the textfile:
20161003|3504|1360|
20161003|3540|1441|
EOF|2
The query I am working on:
DECLARE #FilePath VARCHAR(500)
SELECT #FilePath = 'C:\path\textfile.txt'
DECLARE #file VARCHAR(MAX)
SELECT #file = (SELECT * FROM OPENROWSET(BULK N'C:\path\textfile.txt', SINGLE_CLOB) AS Content)
DECLARE #LastRow INT
DECLARE #LastRow_String VARCHAR(10)
SELECT #LastRow_String = (SELECT SUBSTRING(#file, CHARINDEX('EOF|', #file)+4, LEN(#file)))
SELECT #LastRow = (SELECT CAST(#LastRow_String AS INT))
DECLARE #Query VARCHAR(MAX)
SELECT #Query = ('BULK INSERT [Database].[dbo].[Table] FROM ''' + #FilePath +
''' WITH ( FIELDTERMINATOR = ''|'', ROWTERMINATOR = ''|\n'', LASTROW = ' + #LastRow + ')')
When I execute the above query I get this error message:
Msg 245, Level 16, State 1, Line 11
Conversion failed when converting the varchar value 'BULK INSERT [Database].[dbo].[Table] FROM 'C:\path\textfile.txt' WITH ( FIELDTERMINATOR = '|', ROWTERMINATOR = '|\n', LASTROW = ' to data type int.
Any help is appreciated!
The problem is that LastRow is an integer; you may need to use LastRow_String for the concatenation into Query; if you are concerned about the white spaces, you can do ltrim(rtrim(#LastRow_String))

check which well known text can be transformed using geometry::STPolyFromText(

I have some data which I bulk import into this table structure:
CREATE TABLE #Temp
(
WellKnownText NVARCHAR(MAX)
)
Some of the entries are not valid. So something like this:
SELECT geometry::STPolyFromText(WellKnownText,4326) FROM #Temp
does not work for all rows and thus falls over.
What is the best way to detect which WellKnownText are not valid? I have used MakeValid in the past - so ideally I would like to fix entries as much as possible.
PS:
This does not work:
SELECT * FROM #Temp
WHERE geometry::STPolyFromText(WellKnownText,4326).STIsValid() = 0
PPS:
I chose a loop based approach in the end along those lines:
IF OBJECT_ID('tempdb..#Temp') IS NOT NULL DROP TABLE #Temp;
IF OBJECT_ID('tempdb..#Temp1') IS NOT NULL DROP TABLE #Temp1;
DECLARE #LoopCounter INT = 1;
DECLARE #MaxCounter INT;
DECLARE #Valid BIT;
DECLARE #ValidCounter INT;
DECLARE #WellKnownText NVARCHAR(MAX);
CREATE TABLE #Temp
(
Guid UNIQUEIDENTIFIER,
PostcodeFraction NVARCHAR(50),
WellKnownText NVARCHAR(MAX),
GeoJson NVARCHAR(MAX)
);
CREATE TABLE #Temp1
(
Guid UNIQUEIDENTIFIER,
PostcodeFraction NVARCHAR(50),
WellKnownText NVARCHAR(MAX),
GeoJson NVARCHAR(MAX)
);
BULK INSERT #Temp FROM 'D:\PolygonData.txt' WITH (FIELDTERMINATOR = '\t', FIRSTROW = 2, ROWTERMINATOR = '\n');
ALTER TABLE #Temp ADD Id INT IDENTITY(1,1);
SELECT #MaxCounter = MAX(Id) FROM #Temp
SET #ValidCounter = 0;
WHILE(#LoopCounter <= #MaxCounter)
BEGIN
BEGIN TRY
SELECT #WellKnownText = WellKnownText FROM #Temp WHERE Id = #LoopCounter;
SET #Valid = GEOMETRY::STGeomFromText(#WellKnownText,4326).STIsValid();
SET #ValidCounter = #ValidCounter + 1;
END TRY
BEGIN CATCH
SET #Valid = 0;
END CATCH
IF(#Valid = 1)
BEGIN
INSERT INTO #TEMP1
SELECT Guid, PostcodeFraction, WellKnownText, GeoJson FROM #Temp WHERE Id = #LoopCounter;
END
SET #LoopCounter = #LoopCounter + 1;
END
PRINT #ValidCounter;
SELECT * FROM #TEMP1;
As requested in the comments, some possible solutions
I guess you're really looking for a function that can be CROSS APPLYed, something like
SELECT * FROM #Temp T
CROSS APPLY IsWKTValidFunc(T.WellKnownText, 4326) F
WHERE F.IsValid = <somecondition>
(Or even added to as computed column to give you a flag that's set on inserting your WKT)
Stored Proc
https://gis.stackexchange.com/questions/66642/detecting-invalid-wkt-in-text-column-in-sql-server has a simple SP that wraps GEOMETREY::STGeomFromText in a try catch block.
However, stored procs cannot be CROSS APPLYed (or called from a UDF that can be) so this would result in a cursor based solution.
UDF
A UDF can be cross applied, but can't have a TRY-CATCH block. You also can't call the above SP from a UDF. So not much use there.
CLR UDF
Wrap the GEOMETREY::STGeomFromText call in a CLR UDF that can be CROSS APPLIED, can have try catch and other error checking, rules etc, and return a flag indicating valid text. I haven't tried this one out but this sounds like the best option if CLR is enabled in your environment.
Hope this gives you some ideas. Feedback in the comments to these suggestions appreciated.

TSQL Replace Doubled Characters

Let's say I have data:
heloo
cuube
triniity
How to write script that will replace those "doubled" characters with only one? So the result from the above data set would be:
helo
cube
trinity
Usually I post some script where I tried to achieve this, but this time I can't think of any.
This should work:
CREATE PROCEDURE remove_duplicate_characters(#string VARCHAR(100))
AS
DECLARE #result VARCHAR(100)
SET #result=''
SELECT #result=#result+MIN(SUBSTRING(#string ,number,1)) FROM
(
SELECT number FROM master..spt_values WHERE type='p' AND number BETWEEN 1 AND len(#string )) AS t GROUP BY SUBSTRING(#string,number,1) ORDER BY MIN(number)
)
SELECT #result
GO
You then call it like this:
EXEC remove_duplicate_characters 'heloo'
Source
This script does not depend on having access to master functions, and just relies on t-sql string functions.
declare #word varchar(100) = 'aaaacuuuuuubeeeee', #result varchar(100) = ''
declare #letter char, #idx int = 0, #lastletter char = ''
while(#idx <= len(#word))
begin
select #letter = substring(#word,#idx,1)
if (#letter != #lastletter)
begin
select #result = concat(#result,#letter)
end
select #lastletter = #letter,#idx = #idx + 1
end
select #result

Add comma every nth character in value

my problem is pretty simple. I get a value from a sql select which looks like this:
ARAMAUBEBABRBGCNDKDEEEFOFIFRGEGRIEISITJPYUCAKZKG
and I need it like this:
AR,AM,AU,BE,BA,BR,BG,CN,DK,DE,EE,FO,FI,FR,GE,GR,IE,IS,IT,JP,YU,CA,KZ,KG
The length is different in each dataset.
I tried it with format(), stuff() and so on but nothing brought me the result I need.
Thanks in advance
With a little help of a numbers table and for xml path.
-- Sample table
declare #T table
(
Value nvarchar(100)
)
-- Sample data
insert into #T values
('ARAMAU'),
('ARAMAUBEBABRBGCNDKDEEEFOFIFRGEGRIEISITJPYUCAKZKG')
declare #Len int
set #Len = 2;
select stuff(T2.X.value('.', 'nvarchar(max)'), 1, 1, '')
from #T as T1
cross apply (select ','+substring(T1.Value, 1+Number*#Len, #Len)
from Numbers
where Number >= 0 and
Number < len(T1.Value) / #Len
order by Number
for xml path(''), type) as T2(X)
Try on SE-Data
Time to update your resume.
create function DontDoThis (
#string varchar(max),
#count int
)
returns varchar(max)
as
begin
declare #result varchar(max) = ''
declare #token varchar(max) = ''
while DATALENGTH(#string) > 0
begin
select #token = left(#string, #count)
select #string = REPLACE(#string, #token, '')
select #result += #token + case when DATALENGTH(#string) = 0 then '' else ',' end
end
return #result
end
Call:
declare #test varchar(max) = 'ARAMAUBEBABRBGCNDKDEEEFOFIFRGEGRIEISITJPYUCAKZKG'
select dbo.DontDoThis(#test, 2)
gbn's comment is exactly right, if not very diplomatic :) TSQL is a poor language for string manipulation, but if you write a CLR function to do this then you will have the best of both worlds: .NET string functions called from pure TSQL.
I believe this is what QQping is looking for.
-- select .dbo.DelineateEachNth('ARAMAUBEBABRBGCNDKDEEEFOFIFRGEGRIEISITJPYUCAKZKG',2,',')
create function DelineateEachNth
(
#str varchar(max), -- Incoming String to parse
#length int, -- Length of desired segment
#delimiter varchar(100) -- Segment delimiter (comma, tab, line-feed, etc)
)
returns varchar(max)
AS
begin
declare #resultString varchar(max) = ''
-- only set delimiter(s) when lenght of string is longer than desired segment
if LEN(#str) > #length
begin
-- continue as long as there is a remaining string to parse
while len(#str) > 0
begin
-- as long as know we still need to create a segment...
if LEN(#str) > #length
begin
-- build result string from leftmost segment length
set #resultString = #resultString + left(#str, #length) + #delimiter
-- continually shorten result string by current segment
set #str = right(#str, len(#str) - #length)
end
-- as soon as the remaining string is segment length or less,
-- just use the remainder and empty the string to close the loop
else
begin
set #resultString = #resultString + #str
set #str = ''
end
end
end
-- if string is less than segment length, just pass it through
else
begin
set #resultString = #str
end
return #resultString
end
With a little help from Regex
select Wow=
(select case when MatchIndex %2 = 0 and MatchIndex!=0 then ',' + match else match end
from dbo.RegExMatches('[^\n]','ARAMAUBEBABRBGCNDKDEEEFOFIFRGEGRIEISITJPYUCAKZKG',1)
for xml path(''))