Update one certain bit in a bitmask field (SQL Server) - sql-server

I have an int column containing 8 bits in my SQL Server database. How can I update certain bit without affecting others?
For example, I have a value
11010000
and I want to set bit1 and bit2 to 1, so it would become
11010011
Looked through bitwise operators but couldn't find proper solution.
My goal is not only to update certain bit, but also to avoid database locks.
So when transaction1 updates bit1 in a certain record, another transaction2 could update bit2 in the same field of the same record at the same time.
Is this possible? Or is using 8 separate bit columns the only way?

What you are looking for are the Bitwise operations. To always turn on the proper bit, use a bitwise OR. So, to turn on bits 1 and 2 (total value = 3), use an statement like this (assumes the value in #value is 208, or 11010000 binary):
SET #value = #value | 3
-- or, the alternate form
SET #value |= 3
Other operators are the Bitwise AND (SET #value = #value & 3), and the bitwise NOT (SET #Value = #value ~ 3), and the Bitwise XOR (SET #value = #value ^ 3).
That said, having eight bit fields is easier logically for a new programmer. I don't need to find something special to see that the field ShowCurrencySymbol is the flag for showing a currency symbol, as opposed to finding out what the fifth bit in a byte does. And, since the fields are compacted internally so that eight one-bit, non-nullable fields = one byte of space used (adding NULL takes two bits per bit).
Finally, you can't have two transactions simultaneously update a field on the same row. While one update is occurring, the row will be locked, preventing the other update from processing. If you really want something like this, you will have to use a much more expansive method - a separate BitValues or Flags table, something like this:
CREATE TABLE Flags (
RowID int not null,
FlagName varchar(16) Not Null,
BitValue bit not null,
CONSTRAINT PK_Flags PRIMARY KEY (RowID, FlagName)
);
Then you read and write your flags from this table separate from the row.

A pseudo-bitset value is when you have an integer or string that, when selected or printed, looks like a bitset. Something like this:
DECLARE #bits varchar(8) = '1010'
SELECT Right(8, REPLICATIE('0', 8) + #bits
Value returned: 00001010
For this type of bitset, you need to remember that you are not setting the bits, but are representing the bits instead, and you need to set the representation of the bits in the same way. For example, the function below can be used to set bits in a string representation of your bitstring:
CREATE FUNCTION dbo.SetBitInString
(
#Source char(8),
#Position int,
#Action char(1) = 'S',
#Value bit = NULL
)
RETURNS Char(8)
AS
BEGIN
DECLARE #work Char(8) = '00000000'
SET #work = Right(#work + ISNULL(LTRIM(RTRIM(#Source)), ''), 8)
SET #Work =
CASE #Action
WHEN 'S'-- Set
THEN Stuff(#Work, #Position, 1, '1')
WHEN 'R' -- Reset
THEN Stuff(#Work, #Position, 1, '0')
WHEN 'X' -- XOR value with position
THEN STUFF(#Work, #Position, 1, CAST(CAST(SubString(#Work, #Position, 1) as int) ^ ISNULL(#Value, 0) as CHAR(1)))
WHEN 'Q'
THEN '1'
---- add other options as needed - this is a quick example.
ELSE #Action
END
IF (#Action = #Work)
RAISERROR('Bad Action (%s) Submitted', 13, 1, #Action)
RETURN #Work
END
Reading a bit is a simple SUBSTRING. Constants can be defined for the meaning of each bit (e.g. DECLARE #ShowTotalPrice = 4 -- The 4th bit is the Show Total Price flag)
This should give you enough to go on if you want to use this style of setting, where the displayed value is your bitset represented in 1s and 0s.

Related

MS SQL Obfuscation of Varchar(10) to Integer and back?

I have a two fields that identify a user, one being a Integer and one being a varchar(10) in the format of 'AA111X' where the first two are alpha and the final x is alphanumeric, and need to convert that into an integer for the integer field as a translation. The integer value used to be provided for us but is no longer. The answer may well be this isn't possible, and a lookup table will have to be used but I'm trying to avoid the schema change if possible.
Is it necessary that you actually treat the first two characters as some value base 26, and the last character as some value base 36? Or is it only necessary that you can generate a unique integer for any possible input, in a way that can be converted back if necessary?
If the latter, and if the existing values are considered case insensitive, this solution will always result in a value that fits in a 4 byte integer:
declare #val varchar(10) = 'zz111z';
select cast(
concat(
ascii(substring(val, 1, 1)),
ascii(substring(val, 2, 1)),
substring(val, 3, 3),
ascii(substring(val, 6, 1))
)
as int
)
from (select upper(#val)) v(val);

Overrwrite the n-th byte in a binary(256)

How can I overwrite the n-th byte in a binary(256) using a stored procedure?
The procedure accepts two parameters:
#n tinyint -- index of byte
#value tinyint -- value to be written
I know that I can read a byte by using substring function, so I thought that I can write a byte using the stuff funtction, but it doesn't work.
I'n using SQL Server 2012.
You can use stuff, but it returns a new value, not changes the value in-place.
set #pattern = stuff(#pattern, 5, 1, 0x80); -- changes 5th byte to 0x80

How to get the count of digits after the decimal point in a float column in ms sql?

I have to count the digits after the decimal point in a database hosted by a MS Sql Server (2005 or 2008 does not matter), in order to correct some errors made by users.
I have the same problem on an Oracle database, but there things are less complicated.
Bottom line is on Oracle the select is:
select length( substr(to_char(MY_FIELD), instr(to_char(MY_FILED),'.',1,1)+1, length(to_char(MY_FILED)))) as digits_length
from MY_TABLE
where the filed My_filed is float(38).
On Ms Sql server I try to use:
select LEN(SUBSTRING(CAST(MY_FIELD AS VARCHAR), CHARINDEX('.',CAST(MY_FILED AS VARCHAR),1)+1, LEN(CAST(MY_FIELD AS VARCHAR)))) as digits_length
from MY_TABLE
The problem is that on MS Sql Server, when i cast MY_FIELD as varchar the float number is truncated by only 2 decimals and the count of the digits is wrong.
Can someone give me any hints?
Best regards.
SELECT
LEN(CAST(REVERSE(SUBSTRING(STR(MY_FIELD, 13, 11), CHARINDEX('.', STR(MY_FIELD, 13, 11)) + 1, 20)) AS decimal))
from TABLE
I have received from my friend a very simple solution which is just great. So I will post the workaround in order to help others in the same position as me.
First, make function:
create FUNCTION dbo.countDigits(#A float) RETURNS tinyint AS
BEGIN
declare #R tinyint
IF #A IS NULL
RETURN NULL
set #R = 0
while #A - str(#A, 18 + #R, #r) <> 0
begin
SET #R = #R + 1
end
RETURN #R
END
GO
Second:
select MY_FIELD,
dbo.countDigits(MY_FIELD)
from MY_TABLE
Using the function will get you the exact number of digits after the decimal point.
The first thing is to switch to using CONVERT rather than CAST. The difference is, with CONVERT, you can specify a format code. CAST uses whatever the default format code is:
When expression is float or real, style can be one of the values shown in the following table. Other values are processed as 0.
None of the formats are particularly appealing, but I think the best for you to use would be 2. So it would be:
CONVERT(varchar(25),MY_FIELD,2)
This will, unfortunately, give you the value in scientific notation and always with 16 digits e.g. 1.234567890123456e+000. To get the number of "real" digits, you need to split this number apart, work out the number of digits in the decimal portion, and offset it by the number provided in the exponent.
And, of course, insert usual caveats/warnings about trying to talk about digits when dealing with a number which has a defined binary representation. The number of "digits" of a particular float may vary depending on how it was calculated.
I'm not sure about speed. etc or the elegance of this code. it was for some ad-hoc testing to find the first decimal value . but this code could be changed to loop through all the decimals and find the last time a value was greater than zero easily.
DECLARE #NoOfDecimals int = 0
Declare #ROUNDINGPRECISION numeric(32,16) = -.00001000
select #ROUNDINGPRECISION = ABS(#ROUNDINGPRECISION)
select #ROUNDINGPRECISION = #ROUNDINGPRECISION - floor(#ROUNDINGPRECISION)
while #ROUNDINGPRECISION < 1
Begin
select #NoOfDecimals = #NoOfDecimals +1
select #ROUNDINGPRECISION = #ROUNDINGPRECISION * 10
end;
select #NoOfDecimals

Can I use a hash of fields instead of direct field comparison to simplify comparison of records?

I am integrating between 4 data sources:
InternalDeviceRepository
ExternalDeviceRepository
NightlyDeviceDeltas
MidDayDeviceDeltas
Changes flow into the InternalDeviceRepository from the other three sources.
All sources eventually are transformed to have the definition of
FIELDS
=============
IdentityField
Contract
ContractLevel
StartDate
EndDate
ContractStatus
Location
IdentityField is the PrimaryKey, Contract Key is a secondary Key only if a match exists, otherwise a new record needs to be created.
Currently I compare all the fields in a WHERE clause in SQL Statements and also in a number of places in SSIS packages. This creates some unclean looking SQL and SSIS packages.
I've been mulling computing a hash of ContractLevel, StartDate, EndDate, ContractStatus, and Location and adding that to each of the input tables. This would allow me to use a single value for comparison, instead of 5 separate ones each time.
I've never done this before, nor have I seen it done. Is there a reason that it should be used, or is that a cleaner way to do it?
It is a valid approach. Consider to introduce a calculated field with the hash and index on it.
You may use either CHECKSUM function or write your own hash function like this:
CREATE FUNCTION dbo.GetMyLongHash(#data VARBINARY(MAX))
RETURNS VARBINARY(MAX)
WITH RETURNS NULL ON NULL INPUT
AS
BEGIN
DECLARE #res VARBINARY(MAX) = 0x
DECLARE #position INT = 1, #len INT = DATALENGTH(#data)
WHILE 1 = 1
BEGIN
SET #res = #res + HASHBYTES('MD5', SUBSTRING(#data, #position, 8000))
SET #position = #position+8000
IF #Position > #len
BREAK
END
WHILE DATALENGTH(#res) > 16 SET #res= dbo.GetMyLongHash(#res)
RETURN #res
END
which will give you 16-byte value - you may take all the 16 bytes as Guid, or only first 8-bytes as bigint and compare it.
Adapt the function in your way - to accept string as parameter or even all the your fields instead of varbinary
BUT
be careful with strings casing, datetime formats
if using CHECKSUM - check also other fields, checksum produces dublicates
avoid using 4-byte hash result on relaively big table

StringToDecimal Function - T-SQL Question

I have some dirty input data that is being imported into a raw source table within SQL Server (2008 R2). Fields that are defined as decimal(9,2) or decimal(4,2) by the input provider are coming in as strings, however, the strings do not always conform to the data definition (go figure!).
We import the data from flat files into the raw tables,then apply some conversion scripts to insert the 'cleaned' data into tables with the proper data types assigned to columns.
For instance:
raw_table
TotalAmount varchar(12)
clean_table
TotalAmount decimal(9,2)
Now, my question is this. If I want to do some 'basic' cleanup on this, I would want to do it in a function along the lines of:
CREATE FUNCTION [dbo].[StringToDecimal]
(
#conversionString VARCHAR(12)
)
RETURNS DECIMAL(9,2)
AS
BEGIN
DECLARE #rsp DECIMAL(9,2)
IF ISNUMERIC( LTRIM(RTRIM(REPLACE(#conversionString,' ',''))) ) = 1
BEGIN
SET #rsp = ISNULL( CONVERT( decimal(17,6), NULLIF( LTRIM(RTRIM(REPLACE(#conversionString,' ',''))),'') ), 0 )
END
ELSE
BEGIN
SET #rsp = 0 -- or we can return NULL here
END
RETURN #rsp
END
However, how could one go about supporting various sized decimals in this mix? Is there a way to parametrize the response type? I considered just returning a decimal of the largest size we generally see, then converting it again on the other end, however, you run into arithmetic overflow issues.
Would appreciate any thoughts/insight into solving this one!
Is there a way to parametrize the response type?
It's simpler than you think. Just return as a VARCHAR and do the casting to decimal(x,y) from the VARCHAR. You don't even need to cast - you can directly assign a VARCHAR (as long as it holds valid decimal data) to a decimal column/variable.
I will create 2 functions instead. StringToDecimal2 does the actual conversion, but returns one of 6 "error codes". You can use it to check why a string is invalid. Or use the wrapper dbo.StringToDecimal which just turns the invalid codes into NULL.
CREATE FUNCTION [dbo].[StringToDecimal2]
(
#conversionString VARCHAR(12),
#precision int, -- total digits
#scale int -- after decimal point
)
RETURNS VARCHAR(100)
AS
BEGIN
-- remove spaces, we'll allow this error. no need to trim
set #conversionString = REPLACE(#conversionString,' ','')
-- note: 1,234.56 (thousands separated) will be invalid, so will 1,234,56 (European decimals)
-- well, ok, let's clean up the thousands separators. BUT! It will incorrectly scale European decimals
set #conversionString = REPLACE(#conversionString,',','')
-- we don't support scientific notation either, so 1e4 (10,000) is out
if #conversionString like '%[^0-9.+-]%' return 'INVALID1' -- only digits and decimal are valid (plus +-)
if #conversionString like '%.%.%' return 'INVALID2' -- too many decimals
if #conversionString like '_%[+-]%' return 'INVALID3' -- +- symbol not in the first position
if #conversionString like '[.+-]' return 'INVALID4' -- a single character from "+-."
if #conversionString like '[+-].' return 'INVALID5' -- symbol and decimal only
-- add a decimal place so it is easier to work with below
if #conversionString not like '%.%'
set #conversionString = #conversionString + '.'
-- allow decimal places to go only as far as scale
set #conversionString = left(#conversionString, charindex('.', #conversionString)+#scale)
-- ensure the data is within precision number of digits in total
if charindex('.', #conversionString) > #precision - #scale + 1
return 'INVALID6' -- too many digits before decimal
RETURN #conversionString
END
GO
CREATE FUNCTION [dbo].[StringToDecimal]
(
#conversionString VARCHAR(12),
#precision int, -- total digits
#scale int -- after decimal point
)
RETURNS VARCHAR(100)
AS
BEGIN
RETURN case when [dbo].[StringToDecimal2](#conversionString, #precision, #scale) like 'INVALID%'
then null else [dbo].[StringToDecimal2](#conversionString, #precision, #scale) end
END
GO
Some tests:
select [dbo].[StringToDecimal2]('12342342', 9,2)
select convert(decimal(9,2),[dbo].[StringToDecimal]('1234234', 9,2))
select convert(decimal(9,2),[dbo].[StringToDecimal]('12342342', 9,2))
select convert(decimal(9,2),[dbo].[StringToDecimal]('123423.3333', 9,2))
select convert(decimal(20,10),[dbo].[StringToDecimal]('123423sd.3333', 20,10))
select convert(decimal(20,10),[dbo].[StringToDecimal]('123423sd..3333', 20,10))
select convert(decimal(20,10),[dbo].[StringToDecimal]('-123423.3333', 20,10))
select convert(decimal(20,10),[dbo].[StringToDecimal]('+123423..3333', 20,10))
Thanks for the extra information. It sounds like you have three steps:
Remove all characters from the string that are not digits or a decimal point (do you ever get multiple points in one string?)
Convert to (9,5) or (4,1) as appropriate (how do you decide this? is there rounding? does 10X.781 become 10.78100 or 10.7 or 10.8?)
Insert/update the final value somewhere
Based on point 1 alone, I would immediately avoid TSQL and think about an external script or CLR procedure. A CLR function could do the parsing, but you still have the problem of returning different data types.
Since this appears to be some kind of ETL task, in my environment I would probably implement it as a script component in an SSIS package. The component would do the parsing and send the clean data to different outputs for further processing. If it was a one-time task I would use a Python script to parse the input data and generate INSERT or UPDATE statements.
I don't know if any of those solutions are suitable for you, but maybe it'll give you some ideas. And you should probably avoid the ISNUMERIC() function; search this site or Google to find some of the 'strange' input that it considers to be numeric.

Resources