SQL clone record with a unique index - sql

Is there a clean way of cloning a record in SQL that has an index(auto increment). I want to clone all the fields except the index. I currently have to enumerate every field, and use that in an insert select, and I would rather not explicitly list all of the fields, as they may change over time.

Not unless you want to get into dynamic SQL. Since you wrote "clean", I'll assume not.
Edit: Since he asked for a dynamic SQL example, I'll take a stab at it. I'm not connected to any databases at the moment, so this is off the top of my head and will almost certainly need revision. But hopefully it captures the spirit of things:
-- Get list of columns in table
SELECT INTO #t
EXEC sp_columns #table_name = N'TargetTable'
-- Create a comma-delimited string excluding the identity column
DECLARE #cols varchar(MAX)
SELECT #cols = COALESCE(#cols+',' ,'') + COLUMN_NAME FROM #t WHERE COLUMN_NAME <> 'id'
-- Construct dynamic SQL statement
DECLARE #sql varchar(MAX)
SET #sql = 'INSERT INTO TargetTable (' + #cols + ') ' +
'SELECT ' + #cols + ' FROM TargetTable WHERE SomeCondition'
PRINT #sql -- for debugging
EXEC(#sql)

There's no easy and clean way that I can think of off the top of my head, but from a few items in your question I'd be concerned about your underlying architecture. Maybe you have an absolutely legitimate reason for wanting to do this, but usually you want to try to avoid duplicates in a database, not make them easier to cause. Also, explicitly naming columns is usually a good idea. If you're linking to outside code, it makes sure that you don't break that link when you add a new column. If you're not (and it sounds like you probably aren't in this scenario) I still prefer to have the columns listed out because it forces me to review the effects of the change/new column - even if it's just to look at the code and decide that adding the new column is not a problem.

DROP TABLE #tmp_MyTable
SELECT * INTO #tmp_MyTable
FROM MyTable
WHERE MyIndentID = 165
ALTER TABLE #tmp_MyTable
DROP Column MyIndentID
INSERT INTO MyTable
SELECT *
FROM #tmp_MyTable

This also deals with a unique key projectnum as well as the primary key.
CREATE TEMPORARY TABLE projecttemp SELECT * FROM project WHERE projectid='6';
ALTER TABLE projecttemp DROP COLUMN projectid;
UPDATE projecttemp SET projectnum = CONCAT(projectnum, ' CLONED');
INSERT INTO project SELECT NULL,projecttemp.* FROM projecttemp;

You could create an insert trigger to do this, however, you would lose the ability to do an insert with an explicit ID. It would, instead, always use the value from the sequence.

You could create a trigger to do it for you. To make sure that trigger only works for cloning, you could create a separate username CLONE and log in with it. Or, even better, if your DBMS supports it, create a role named CLONE and any user can log in using that role and do the cloning. The trigger code would be something like:
if (CURRENT_ROLE = 'CLONE') then
new.ID = assign new id from generator/sequence
Of course, you would grant that role only to the users who are allowed to clone records.

Related

Alter table to add dynamic columns based off some previously selected query

Is it possible to alter a table and add columns with a dynamic name/data type based off some previously select query?
The pseudo equivalent for what I'm looking to do in SQL would be:
foreach row in tableA
{
alter tableB add row.name row.datatype
}
This is for SQL Server.
As mentioned, you can do this with dynamic sql. Something along these lines:
Declare #SQL1 nvarchar(4000)
SELECT #SQL1=N'ALTER TABLE mytable'+NCHAR(13)+NCHAR(10)
+N' ADD COLUMN '+ my_new_column_name + ' varchar(25)'+NCHAR(13)+NCHAR(10)
-- SELECT LEN(#SQL1), #SQL1
EXECUTE (#SQL1)
Apart from the fact that this is messy, error prone, a security risk, requires high authorization to execute and needs multiple variables for batches bigger than 4000 characters, it is usually also a bad idea from a design point of view (depending on when/why you are doing this).
Sure, you can do this with dynamic sql.

Generate tables with unique names

I need to create non-temporary tables in a MariaDB 10.3 database using Node. I therefore need a way of generating a table name that is guaranteed to be unique.
The Node function cannot access information regarding any unique feature about what or when the tables are made, so I cannot build the name from a timestamp or connection ID. I can only verify the name's uniqueness using the current database.
This question had a PostgreSQL answer suggesting the following:
SET #name = GetBigRandomNumber();
WHILE TableExists(#name)
BEGIN
SET #name = GetBigRandomNumber();
END
I attempted a MariaDB implementation using #name = CONCAT(MD5(RAND()),MD5(RAND())) to generate a random 64 character string, and (COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE #name) >0 to check if it was a unique name:
SET #name = CONCAT(MD5(RAND()),MD5(RAND()));
WHILE ((COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE #name) >0) DO
SET #name = CONCAT(MD5(RAND()),MD5(RAND()));
END WHILE;
CREATE TABLE #name ( ... );
However I get a syntax error when I try to run the above query. My SQL knowledge isn't that great so I'm at a loss as to what the problem might be.
Furthermore, is this approach efficient? The randomly generated name is long enough that it is very unlikely to have any clashes with any current table in the database, so the WHILE loop will very rarely need to run, but is there some sort of built in function to auto increment table names, or something similar?
SET #name := UUID();
If the dashes in that cause trouble, then
SET #name := REPLACE(UUID(), '-', '');
It will be safer (toward uniqueness) than RAND(). And, in theory, there will be no need to verify its uniqueness. After all, that's the purpose of UUIDs.

inset into exec dbo.sp_executesql (multiple statement string)

I use this code:
select #SQLString=N'select ....(long select query).....'
+ char(10) + '; drop table _item_list '
and afterwards
insert #tbl EXEC dbo.sp_executesql #SQLString
It seems to be working, but can someone give me some clear info on this? I was expecting the executesql thing to have to be "faithful", as in related in its entirety, to the insert statement preceding it. It seems however that it has no problem feeding the select to the insert, and doing the drop table independently. Also, is the ; before the drop necessary?
You have many ways to populate data to a table:
INSERT INTO TABLE VALUES
SELECT INTO FROM
INSERT INTO SELECT
INSERT INTO EXEC <-- what you are talking about
And the ; is not very necessary, just to separate batch, the result will be the same, but for some may need to consider scope, such as batch or session, then it matters.
UPDATE:
That is fine with irrelevant statements afterward of the select, even there are more than one select in your USP, but you have to make sure all the returned results (the select statement) need to be compatible with the table definition that you are referring to.

How do I insert the results from "EXEC()" in a temp table

I need som help with a problem, our company have a vendor that deliver a database to us. inside that database, the vendor has a table with alot of t sql scripts. What i want to do is the following, i want to make a select to find the script and then execut the script and store the result in a variable or temp tabel. I can not alter the script from the vedor, so I need the result into something i can manupilate. Another problem is that i dont know how many columns ther result will have. So it has to be flexible. Like one script have 5 columns and and the next script has 8 and so on.
exsample:
DECLARE #SQL nvarchar(MAX) = ( Select distinct script_details
from scripttable where .......)
This will give me the script I want to use, then I use
EXEC(#SQL)
to execute the script.
Then my problem is, the result from this I want into a variable or a table.
I have tryed to make a temp table like this:
create table #TmpTblSP (col1 varchar(MAX),col2 varchar(MAX),col3 varchar(MAX),col4 varchar(MAX),col5 varchar(MAX),col6 varchar(MAX),col7 varchar(MAX),col8 varchar(MAX),col9 varchar(MAX),col10 varchar(MAX),col11 varchar(MAX),col12 varchar(MAX))
then
insert into #TmpTblSP
EXEC(#SQL)
This gives me the following error:
Msg 213, Level 16, State 7, Line 1
Column name or number of supplied values does not match table definition.
But if i know how many columns there are and specify that into the insert it works.
insert into #TmpTblSP(Col1,Col2,Col3)
EXEC(#SQL)
But here you se my problem, I dont know how many columns there are in every script. I could make one script for every script the vendor has, but that will be alot, it's like 3000 scripts in that table and they change them often.
You could try something like:
DECLARE #SQL nvarchar(MAX) = (
Select distinct script_details
into #temptbl
from scripttable where .......
);
EXEC(#SQL);
If you don't know how many columns yous #sql gives then the only solution is use SELECT INTO. I use it in this way:
DECLARE #QRY nvarchar(MAX) = ( Select distinct script_details
from scripttable where .......)
SET #sql = 'SELECT * into ' + #temptablename + ' FROM (' + #qry + ') A '
It gives some flexibility
Remember that it is easy to check structure of the table created in this way in sys so you can build another #SQL from this info if needed.
I this as well recommended to split "SELECT INTO" to 2 parts
One is
SELECT INTO ......... WHERE 1=2
Second
INSERT INTO SELECT ......
Creation of table locks all DB. So it is good to create it as fast as possible and then insert into it.

Is my stored procedure executing out of order?

Brief history:
I'm writing a stored procedure to support a legacy reporting system (using SQL Server Reporting Services 2000) on a legacy web application.
In keeping with the original implementation style, each report has a dedicated stored procedure in the database that performs all the querying necessary to return a "final" dataset that can be rendered simply by the report server.
Due to the business requirements of this report, the returned dataset has an unknown number of columns (it depends on the user who executes the report, but may have 4-30 columns).
Throughout the stored procedure, I keep a column UserID to track the user's ID to perform additional querying. At the end, however, I do something like this:
UPDATE #result
SET Name = ppl.LastName + ', ' + ppl.FirstName
FROM #result r
LEFT JOIN Users u ON u.id = r.userID
LEFT JOIN People ppl ON ppl.id = u.PersonID
ALTER TABLE #result
DROP COLUMN [UserID]
SELECT * FROM #result r ORDER BY Name
Effectively I set the Name varchar column (that was previously left NULL while I was performing some pivot logic) to the desired name format in plain text.
When finished, I want to drop the UserID column as the report user shouldn't see this.
Finally, the data set returned has one column for the username, and an arbitrary number of INT columns with performance totals. For this reason, I can't simply exclude the UserID column since SQL doesn't support "SELECT * EXCEPT [UserID]" or the like.
With this known (any style pointers are appreciated but not central to this problem), here's the problem:
When I execute this stored procedure, I get an execution error:
Invalid column name 'userID'.
However, if I comment out my DROP COLUMN statement and retain the UserID, the stored procedure performs correctly.
What's going on? It certainly looks like the statements are executing out of order and it's dropping the column before I can use it to set the name strings!
[Edit 1]
I defined UserID previously (the whole stored procedure is about 200 lies of mostly irrelevant logic, so I'll paste snippets:
CREATE TABLE #result ([Name] NVARCHAR(256), [UserID] INT);
Case sensitivity isn't the problem but did point me to the right line - there was one place in which I had userID instead of UserID. Now that I fixed the case, the error message complains about UserID.
My "broken" stored procedure also works properly in SQL Server 2008 - this is either a 2000 bug or I'm severely misunderstanding how SQL Server used to work.
Thanks everyone for chiming in!
For anyone searching this in the future, I've added an extremely crude workaround to be 2000-compatible until we update our production version:
DECLARE #workaroundTableName NVARCHAR(256), #workaroundQuery NVARCHAR(2000)
SET #workaroundQuery = 'SELECT [Name]';
DECLARE cur_workaround CURSOR FOR
SELECT COLUMN_NAME FROM [tempdb].INFORMATION_SCHEMA.Columns WHERE TABLE_NAME LIKE '#result%' AND COLUMN_NAME <> 'UserID'
OPEN cur_workaround;
FETCH NEXT FROM cur_workaround INTO #workaroundTableName
WHILE ##FETCH_STATUS = 0
BEGIN
SET #workaroundQuery = #workaroundQuery + ',[' + #workaroundTableName + ']'
FETCH NEXT FROM cur_workaround INTO #workaroundTableName
END
CLOSE cur_workaround;
DEALLOCATE cur_workaround;
SET #workaroundQuery = #workaroundQuery + ' FROM #result ORDER BY Name ASC'
EXEC(#workaroundQuery);
Thanks everyone!
A much easier solution would be to not drop the column, but don't return it in the final select.
There are all sorts of reasons why you shouldn't be returning select * from your procedure anyway.
EDIT: I see now that you have to do it this way because of an unknown number of columns.
Based on the error message, is the database case sensitive, and so there's a difference between userID and UserID?
This works for me:
CREATE TABLE #temp_t
(
myInt int,
myUser varchar(100)
)
INSERT INTO #temp_t(myInt, myUser) VALUES(1, 'Jon1')
INSERT INTO #temp_t(myInt, myUser) VALUES(2, 'Jon2')
INSERT INTO #temp_t(myInt, myUser) VALUES(3, 'Jon3')
INSERT INTO #temp_t(myInt, myUser) VALUES(4, 'Jon4')
ALTER TABLE #temp_t
DROP Column myUser
SELECT * FROM #temp_t
DROP TABLE #temp_t
It says invalid column for you. Did you check the spelling and ensure there even exists that column in your temp table.
You might try wrapping everything preceding the DROP COLUMN in a BEGIN...COMMIT transaction.
At compile time, SQL Server is probably expanding the * into the full list of columns. Thus, at run time, SQL Server executes "SELECT UserID, Name, LastName, FirstName, ..." instead of "SELECT *". Dynamically assembling the final SELECT into a string and then EXECing it at the end of the stored procedure may be the way to go.