Is there a method in MySQL to protect fields from changes? - mysql

We get this daily data feed. (We have no control over the original data, so asking them to correct the database isn't an option.)
The customer records contain addresses in the US. Street address, City, State and Zip.
On our end, we use the data in a database for the marketing department. They sometimes find the address is incorrect, or incomplete, and want to make changes to it. But of course, the next data feed would come in and wipe out their corrections.
Is there a method inf MySQL to protect certain fields from being changes, kind of like a protected cell in a spreadsheet. This is some of the field names of the MySQL record layout:
address1 address2 address3 city state zipcode
What if I created along side of this, additional fields that are flagged either "Y" or "N" as being a protected field:
address1 address1_flag address2 address2_flag
address3 address3_flag city city_flag state
state_flag zipcode zipcode_flag
So when the marketing department corrects, for example, the zipcode, it would set the zipcode_flag to "Y" meaning, Yes protect the field zipcode from further changes. If the original data feed does get corrected at a later point, then if zipcode from the marketing department's database matches the original field, then the zipcode_flag protection would be changed to "N".
Does this sound like the correct method to manage the marketing department's database from the daily feed? Or is there another approach or feature available in MySQL to do this? Thanks!

I don't think there's a "protected" flag or feature, but there are a few roads you could take to accomplish your goal.
The first and most specific would be to create a "restricted user" in MySQL. To restrict the user, you can/would grant only SELECT privileges to the column(s) you don't want modified. To do this you would use:
GRANT SELECT(zipcode) ON addresses TO restrictedUser;
You can see a good example of this here, or get detailed information in the manual.
A second method would be to create a procedure that selects/inserts/updates. This one may be overkill, but could be accomplished to suit your needs and won't require modifying user permissions.
A simple example of a select and update procedure would be (not tested):
CREATE PROCEDURE select_addresses ()
BEGIN
SELECT address1, zipcode FROM addresses;
END
CREATE PROCEDURE update_addresses ( IN recordID INT(11), IN newAddress1 VARCHAR(255) )
BEGIN
SET #query := CONCAT("UPDATE addresses SET address1 = '", newAddress1, "' WHERE id = ", recordId);
PREPARE stmt FROM #query;
EXECUTE stmt;
END
This will allow a user to select any column you specify that they're allowed to read by calling select_addresses() and then perform an update on any allowed column via update_addresses(). You'd have to add several layers of logic to only update variables that have been set, etc - so using a procedure may in fact be overkill =P

You can manage privileges at the column-level: http://dev.mysql.com/doc/refman/5.1/en/grant.html.

Another approach would be to set a trigger that works off a flag in the records. For example if you had a field in the table
modified TINYINT(1) NOT NULL DEFAULT 0
THEN you create a Trigger to preserve the data
CREATE DEFINER = CURRENT_USER TRIGGER `database_name`.`table_name_BEFORE_UPDATE` BEFORE UPDATE ON `table_name` FOR EACH ROW
BEGIN
IF (NEW.modified=1) THEN
/* Then set any fields you want to preserve to their old value */
SET NEW.address1=OLD.address1;
SET NEW.address2=OLD.address2;
SET NEW.city=OLD.city;
SET NEW.stateprov=OLD.stateprov;
SET NEW.postal_code=OLD.postal_code;
SET NEW.country=OLD.country;
END IF;
END
This would allow you to restrict modifications unless modified is set to 1. If you wanted to take it one step further and once modified prevent it from ever being modified, the modified flag could also be included so once set it couldn't change short of pulling the trigger from the db
CREATE DEFINER = CURRENT_USER TRIGGER `database_name`.`table_name_BEFORE_UPDATE` BEFORE UPDATE ON `table_name` FOR EACH ROW
BEGIN
IF (NEW.modified=1) THEN
/* Then set any fields you want to preserve to their old value */
SET NEW.modified=OLD.modified;
SET NEW.address1=OLD.address1;
SET NEW.address2=OLD.address2;
SET NEW.city=OLD.city;
SET NEW.stateprov=OLD.stateprov;
SET NEW.postal_code=OLD.postal_code;
SET NEW.country=OLD.country;
END IF;
END

Related

mariadb always returning last row from stored procedure

I'm facing a problem very similar to this issue:
MySQL stored procedure only returns last row of data
Let me explain the scenario first.
I'm using phplist and I have created a custom attribute acting as "per user token"; it is used to create a custom unsubscribe links.
Sometimes I'm in the need to get such token manually knowing the user email address.
So, here is the two involved tables are (note: only relevand fields here):
phplist_user_user: id,email
phplist_user_user_attribute: attributeid,userid,value
With dbeaver ide, executing the following query I correctly get the user token:
SELECT value AS token FROM phplist_user_user_attribute WHERE attributeid=3 and userid=(SELECT MAX(`id`) FROM phplist_user_user WHERE `email`='useremail#domain.ext');
If I put this query inside a stored procedure instead it always returns the very last token inserted to the table, regardless the correctness of email:
CREATE DEFINER=`root`#`%` PROCEDURE `phplist`.`GetTokenFromEmail`(IN `email` VARCHAR(255))
BEGIN
SELECT value AS token
FROM phplist_user_user_attribute
WHERE attributeid=3
and userid=(SELECT MAX(`id`)
FROM phplist_user_user
WHERE `email`=email);
END
Checking at phplist_user_user_attribute table it equals to the very last row:
This is a standard/default phplist installation, so I'm probably doing something wrong with the procedure I don't really understand what.
Thank you for any help!
WHERE `email`=email
Both columns are taken from phplist_user_user table - so the condition value is always TRUE until phplist_user_user.email is NULL.
Remember - if query's rowsource contains more than one table (including a case of copies of the same table) then specify table name/alias for EACH column name.

Migrating from MySQL to SQL Server, How to Implement Triggers

So I have this trigger that I wrote for a MySQL environment, and which I now need to transfer to a SQL Server environment.
Being unfamiliar with Transact SQL, I have a little trouble translating from one to the other or creating an equivalent. Here is the simplified query:
CREATE TRIGGER <myTrigger> BEFORE INSERT ON <myTable>
IF NEW.<myColumnContainingBoolean> = TRUE THEN
SET NEW.<myColumnReferenceCode> = CONCAT(YEAR(NOW()),MONTH(NOW()),DAY(NOW()), 'indice');
ENDIF;
The goal is to add a reference number (today's date writted yyyymmdd + 'indice') according to the value of a boolean contained in the query, to summarize, if, at the time of the INSERT, the value of the boolean is on TRUE then we insert the code on this same line, otherwise we don't write a reference. Here is a maybe more explicit example :
Example
I have sincerely tried a lot of things, what seems to come closest to my request is this one (which, of course, does not work):
CREATE TRIGGER <myTrigger>
ON <myTable>
AFTER INSERT
AS
BEGIN
IF <myColumnContainingBoolean>
SET <myColumnReferenceCode> = CONCAT(YEAR(GETDATE()),MONTH(GETDATE()),DAY(GETDATE()), 'indice');
FROM inserted
END
GO
Ok, I guess we have to use a trigger (immense sigh).
Here's how you would do it in SQL Server:
CREATE TRIGGER <your schema>.<your table>_Insert ON <your schema>.<your table>
INSTEAD OF INSERT
AS
BEGIN
INSERT INTO <your schema>.<your table> (<your other columns>,<myColumnReferenceCode>)
SELECT
<your other columns>
,CASE
WHEN <myColumnContainingBoolean> = 1 THEN FORMAT(GETDATE(),'yyyyMMdd') + 'indice'
ELSE <myColumnReferenceCode>
END
FROM
inserted
END
GO
If you're using an auto-incremented (IDENTITY) column, make sure to leave it off your insert list inside the trigger.
Other observations: You could probably just make <myColumnReferenceCode> a date and store GETDATE() and get the same functionality, but I don't know all of your circumstances.

<b>admin </b> prevent himself to drop specific tables?

How can admin prevent himself to drop or truncate specific tables, because sometimes a table is accidently truncated or deleted, making view is not a good idea. if i make a trigger ,it is implemented in all tables in db. i just want to implement it in specific tables, in sql server ?
Create TRIGGER [TR_ProtectCriticalTables]
ON DATABASE
FOR
DROP_TABLE
AS
DECLARE #eventData XML,
#uname NVARCHAR(50),
#oname NVARCHAR(100),
#otext VARCHAR(MAX),
#etype NVARCHAR(100),
#edate DATETIME
SET #eventData = eventdata()
SELECT
#edate=GETDATE(),
#uname=#eventData.value('data(/EVENT_INSTANCE/UserName)[1]', 'SYSNAME'),
#oname=#eventData.value('data(/EVENT_INSTANCE/ObjectName)[1]', 'SYSNAME'),
#otext=#eventData.value('data(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]',
'VARCHAR(MAX)'),
#etype=#eventData.value('data(/EVENT_INSTANCE/EventType)[1]', 'nvarchar(100)')
IF #oname IN ('tbluser')-- You can give comma seperated list here
BEGIN
DECLARE #err varchar(100)
SET #err = 'Table ' + #oname + ' is super duper protected and cannot be dropped.'
RAISERROR (#err, 16, 1) ;
ROLLBACK;
END
GO
ENABLE TRIGGER [TR_ProtectCriticalTables] ON DATABASE
For disabling the truncate try this
EXEC sys.sp_cdc_enable_table
#source_schema = N'dbo',
#source_name = N'TestTable',
#role_name = NULL
GO
This may cause some other problems so please check before using.
Just remove the permissions dir that special admins. And create a second user which has the permissions for doing such dangerous jobs.
See also the GRANT and REVOKE SQL commands.
Please create user and create trigger for superadmin who has all permissions like :
CREATE TRIGGER reminder2
ON Customer
with execute as owner
AFTER DELETE
AS
truncate table Customer

Select entire [NEW] Row in Before Insert Trigger MySQL

I have a trigger containing this:
SET v1 = CONCAT_WS(',',NEW.ID, NEW.Name, NEW.Type, NEW.Value);
Can this be simplified into something like this to include the entire new row?:
SET v1 = CONCAT_WS(',',NEW.*);
(I've tried variations of the above however they causes syntax errors)
Thanks
No, there's no easy way to do this. You have to reference each column.
The only real workaround is to use the table metadata to help you generate the statement you want, and then include that statement in your procedure.
You wouldn't want to do this dynamically in the TRIGGER, even if it were possible.
SELECT CONCAT('NEW.`',GROUP_CONCAT(c.column_name
ORDER BY ORDINAL_POSITION SEPARATOR '`,NEW.`'),'`')
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'mytable'
That should get you a string that looks like:
NEW.`ID`,NEW.`Name`,NEW.`Type`,NEW.`Value`
And you can paste that into your trigger body. (Of course you could extend the CONCAT to generate the whole line.)
The downside is that when new columns are added to the table, those won't get automatically included; that will require a change in the trigger. If a column gets dropped or renamed, your trigger will start throwing exceptions; again requiring a fix to the trigger.
UPDATE
Q:How can I convert this string into a MySQL query?
#query = "CONCAT_WS(',','CCC','64',NEW.Record,NEW.ID,NEW.Name,NEW.User_ID,NEW.State_Record,NEW.Hash);"
I wouldn't convert that to a query. I would just use that as static line of code (with no double quotes) in the body of your trigger, just like the original statement you had in your TRIGGER.
SET v1 = CONCAT_WS(',','CCC','64',NEW.Record,NEW.ID,NEW.Name,NEW.User_ID,NEW.State_Record,NEW.Hash);
(It wasn't clear what you intended to do with that string.)
If you are trying to create a SELECT statement, you could try removing that semicolon from the end of the string, and prepending a SELECT keyword on it. But I don't think the NEW. references to the column values of the current row will be recognized in that context. That might happen, but you'd need to test.
If I needed to do something like that, I would do it using user variables,
SET #new_id = NEW.ID;
SET #new_name = NEW.Name;
SELECT #new_id, #new_name
It's not at all clear to me what you are going to do with the result set returned by a query like. If you are attempting to audit changes to the table, the normative pattern is to run an INSERT of the column values into an changelog table,
INSERT INTO mytable_changelog (ID, Name) VALUES (NEW.ID, NEW.Name);
It really depends on what you are trying to accomplish.

Is it possible to pass the NEW and the OLD tables from a trigger into a procedure in MySQL?

Is it possible to pass the NEW and the OLD tables from a trigger into a procedure in MySQL?
I suspect no, since there is no such a datatype as table that a procedure accepts.
Any workarounds possible?
Ideally it would look like this:
CREATE TRIGGER Product_log AFTER UPDATE ON Product
FOR EACH ROW BEGIN
call logChanges(OLD, NEW);
END;
You can explicitly pass each field:
CALL logChanges(OLD.colA, OLD.colB, NEW.colA, NEW.colB);
Or if logChanges must be sufficiently generic that it can handle such calls from different tables, one could concatenate the field values into a single string using a suitable delimiter (e.g. the unit separator):
CALL logChanges(CONCAT_WS(CHAR(31), OLD.colA, old.colB),
CONCAT_WS(CHAR(31), NEW.colA, NEW.colB));
Or if data types must be preserved, one could insert the records into a temporary from which logChanges reads.
it's not possible because there is no NEW or OLD table. The entire trigger is related to the table - the "new" and "old" refer to the rows and the values they contained before and after the event that was triggered. In other words, your example would be:
call logChanges(OLD.customername, NEW.customername)
You could also save all the OLD data in a history table (which I expect logchanges does anyways), basically being a clone of the production table something like this:
BEGIN
IF OLD.customer_name != NEW.customer_name
THEN
INSERT INTO myTable_chagne_history
(
customer_id ,
customer_name ,
another_field ,
edit_time
)
VALUES
(
OLD.customer_id,
OLD.customer_name,
OLD.another_field ,
NEW.time_edit_was_made
);
END IF;
END;