Dynamic SQL alternatives (MySql) - mysql

On current MySql DB server, i have two schemas: "Friends", "Places". Entire DB is organized around stored procedures which are being called from outside. Maybe it's good approach, maybe it's bad but it's not related to this problem i'm having. In this case DB has to be separated from any outside software using it (as i'm only in charge of DB).
Some stored procedures from "Friends" schema refer to tables from "Places" and vice versa. Now, if for example i wanna setup new set of schemas, on the same server, but for another "client" like this:
Friends_clientOne
Places_clientOne
Friends_clientTwo
Places_clientTwo
I'm having a problem - stored procedures referring tables from another schema won't know which schema name to use. Checking and modifying each and every procedure to suite appropriate schema name every time new set is created is not an option. Dynamic SQL is totally new for me - what are other options? How can i, for example, do this:
(stored procedure inside schema Friends_clientOne):
Select * from Places_<getCurrentSchemaSuffix>.someTable;
Please tell me MySql is flexible enough for this :( What about Percona?

The closest thing to what you're describing is a builtin function DATABASE() (http://dev.mysql.com/doc/refman/5.7/en/information-functions.html#function_database), which returns the current default database.
The default database isn't necessarily the one that a given table belongs to. It's the database most recently named in a USE <databasename> statement. If you can rely on your application always using the database you mean for that table to belong to, then you can use that function.
However, no SQL implementation allows you to change the table name dynamically during query execution. You can name tables only before doing the prepare of a statement, and hard-coding it into the query. There is no syntax for making the table name variable.
So you'll have to use dynamic SQL even if you use the DATABASE() function.
Percona Server is no different from stock Oracle MySQL for this problem.
Your options for this problem are:
Stop using multiple schemas for each client. Put all of the data for each client into a single schema. This seems simplest.
Design the stored procedures to be unique to each client. You said you didn't want to do this. But for what it's worth, we do this in the stored procedures and triggers in the customer databases I manage at my current job. It's not that bad. We have a "template" version of the CREATE statements for each trigger or procedure, with a placeholder token for the customer ID. When we create a new customer's database, we copy that template code and make a substitution on the customer ID placeholder, then run it.
Put each of your clients' data into their own exclusive instance of MySQL Server. This way you can have multiple schemas per client, but the schema names don't need to be distinct for each client. You can run multiple instances on one server host, they just have to be configured with distinct datadir, port, sock_file, and other log files. Though I have seen this solution used, I don't recommend it, because it has a lot of resource overhead, and it's hard to manage.
Learn to use dynamic SQL.

You can use PREPARED Statement in the Procs like this:
DELIMITER //
CREATE PROCEDURE getPlace (OUT param1 char)
BEGIN
SELECT CONCAT("Select * from Places_", SUBSTRING_INDEX(DATABASE(), '_', -1),".someTable;") INTO #sql;
PREPARE getPlaces from #sql;
EXECUTE getPlaces;
DEALLOCATE PREPARE getPlaces;
END;
//
DELIMITER ;
sample
MariaDB [mysql]> CREATE DATABASE Friends_clientOne;
Query OK, 1 row affected (0.00 sec)
MariaDB [mysql]> CREATE DATABASE Friends_clientTwo;
Query OK, 1 row affected (0.00 sec)
MariaDB [mysql]> CREATE DATABASE Places_clientOne;
Query OK, 1 row affected (0.00 sec)
MariaDB [mysql]> CREATE DATABASE Places_clientTWO;
Query OK, 1 row affected (0.00 sec)
MariaDB [mysql]> CREATE TABLE Places_clientOne.someTable (name varchar(32));
Query OK, 0 rows affected (0.02 sec)
MariaDB [mysql]> CREATE TABLE Places_clientTwo.someTable (name varchar(32));
Query OK, 0 rows affected (0.02 sec)
MariaDB [mysql]> INSERT INTO Places_clientOne.someTable VALUES('text in Places_clientOne.someTable');
Query OK, 1 row affected, 1 warning (0.00 sec)
MariaDB [mysql]> INSERT INTO Places_clientTwo.someTable VALUES('text in Places_clientTwo.someTable');
Query OK, 1 row affected, 1 warning (0.01 sec)
MariaDB [mysql]> use Friends_clientOne;
Database changed
MariaDB [Friends_clientOne]> DELIMITER //
MariaDB [Friends_clientOne]> CREATE PROCEDURE getPlace (OUT param1 char)
-> BEGIN
-> SELECT CONCAT("Select * from Places_", SUBSTRING_INDEX(DATABASE(), '_', -1),".someTable;") INTO #sql;
-> PREPARE getPlaces from #sql;
-> EXECUTE getPlaces;
-> DEALLOCATE PREPARE getPlaces;
-> END;
-> //
Query OK, 0 rows affected (0.03 sec)
MariaDB [Friends_clientOne]> DELIMITER ;
MariaDB [(none)]> use Friends_clientTwo;
Database changed
MariaDB [Friends_clientTwo]> DELIMITER //
MariaDB [Friends_clientTwo]>
MariaDB [Friends_clientTwo]> CREATE PROCEDURE getPlace (OUT param1 char)
-> BEGIN
-> SELECT CONCAT("Select * from Places_", SUBSTRING_INDEX(DATABASE(), '_', -1),".someTable;") INTO #sql;
-> PREPARE getPlaces from #sql;
-> EXECUTE getPlaces;
-> DEALLOCATE PREPARE getPlaces;
-> END;
-> //
Query OK, 0 rows affected (0.02 sec)
MariaDB [Friends_clientTwo]> DELIMITER ;
MariaDB [Friends_clientTwo]> call getPlace(#r);
+----------------------------------+
| name |
+----------------------------------+
| text in Places_clientTwo.someTab |
+----------------------------------+
1 row in set (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
MariaDB [Friends_clientTwo]> use Friends_clientOne;
Database changed
MariaDB [Friends_clientOne]> call getPlace(#r);
+----------------------------------+
| name |
+----------------------------------+
| text in Places_clientOne.someTab |
+----------------------------------+
1 row in set (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
MariaDB [Friends_clientOne]>

Related

MySQL query - get a list of ALL WP admin email addresses for all WP sites on server

I have a lot of WP websites set up on my server, and I'm trying to get a list of admin email addresses registered for all of the WP sites.
The server uses WHM/cPanel, and MariaDB.
I'd hoped WordPress Toolkit could do this out of the box, but it doesn't seem to be able to.
I'm an SQL noob, and I've only gotten as far as this for my SQL query, for a single DB:
SELECT option_value
FROM `database_name`.`wp_options`
WHERE option_name="admin_email"
Another problem is, lots of the WP databases on the server don't use the standard wp_ table prefix, but instead use a random string, so I also need a way of using a wildcard in the table name like
*_options
So my 2 problems are:
Make the query loop across ALL databases on the server
make the query use a wildcard in the table name
Is this possible?
Yes. Its two queries, first to generate the SQL, and then execute it.
Example setup:
MariaDB [(none)]> create database rr;
Query OK, 1 row affected (0.000 sec)
MariaDB [(none)]> create database ss;
Query OK, 1 row affected (0.001 sec)
MariaDB [(none)]> create table ss.wp_options(option_name varchar(30), option_value varchar(30));
Query OK, 0 rows affected (0.003 sec)
MariaDB [(none)]> create table rr.rr_options(option_name varchar(30), option_value varchar(30));
Query OK, 0 rows affected (0.003 sec)
MariaDB [(none)]> insert into ss.wp_options values ('admin_email', 'me#ss');
Query OK, 1 row affected (0.003 sec)
MariaDB [(none)]> insert into rr.rr_options values ('admin_email', 'me#rr');
Query OK, 1 row affected (0.002 sec)
Then use the information_schema.TABLES to generate your query using UNION ALL to concatinate them:
SELECT group_concat(
concat('select "', TABLE_SCHEMA, '" as db, option_value from ', TABLE_SCHEMA, '.', TABLE_NAME, ' WHERE option_name="admin_email"')
SEPARATOR ' UNION ALL ') INTO #sql
FROM information_schema.TABLES
WHERE TABLE_NAME LIKE '%_options';
Just to show what we generate:
MariaDB [(none)]> select #sql\G
*************************** 1. row ***************************
#sql: select "ss" as db, option_value from ss.wp_options WHERE option_name="admin_email"
UNION ALL select "rr" as db, option_value from rr.rr_options WHERE option_name="admin_email"
Execute immediate runs a query, like a prepared statement without the setup and run, shutdown steps:
MariaDB [(none)]> execute immediate #sql;
+----+--------------+
| db | option_value |
+----+--------------+
| ss | me#ss |
| rr | me#rr |
+----+--------------+
2 rows in set (0.002 sec)

Create a MYSQL stored proc to create table in a different database

I am trying to create a stored procedure that can be used across multiple databases but not have to duplicate the stored procedure across all databases.
Example: have stored procedure in BIZ_ADM that can be run and have parameters passed into it that would created the tables and views in another database, BIZ_OPS
CALL CreateNewEntity1 ('KTC_SHARED', 'TEST1', 'SHARED');
DELIMITER $$
USE KTC_SHARED$$
DROP PROCEDURE IF EXISTS CreateNewEntity1$$
CREATE DEFINER=root#192.168.0.% PROCEDURE CreateNewEntity1(IN databaseNameSTR VARCHAR (64), IN entityNameSTR VARCHAR(64), IN extSTR VARCHAR(64))
BEGIN
SET #sqlSTR = CONCAT("CREATE TABLE TBL_", entityNameSTR, " (standard fields, etc)
I can do everything if it is to be created in the current database, but does not work when trying to create in a different database. Hopefully this is clear enough to understand what I am trying to do.
Thanks in advance for any help you can provide.
Copied from CLI window:
Server version: 8.0.21 MySQL Community Server - GPL
Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> USE test
Database changed
mysql> CREATE DATABASE test1;
Query OK, 1 row affected (0.15 sec)
mysql> DELIMITER ;;
mysql> CREATE PROCEDURE test1.createtable (IN db VARCHAR(64), IN tb VARCHAR(64))
-> BEGIN
-> SET #sql:=CONCAT('CREATE TABLE ', db, '.', tb, '(id INT, val INT);');
-> PREPARE stmt FROM #sql;
-> EXECUTE stmt;
-> DROP PREPARE stmt;
-> END; ;;
Query OK, 0 rows affected (0.19 sec)
mysql> DELIMITER ;
mysql> SHOW TABLES;
Empty set (0.09 sec)
mysql> CALL test1.createtable('test', 'test');
Query OK, 0 rows affected (0.66 sec)
mysql> SHOW TABLES FROM test;
+----------------+
| Tables_in_test |
+----------------+
| test |
+----------------+
1 row in set (0.11 sec)
mysql>

How to pass variables into MySql Procedure creation

I want to be able to create some stored Procedures in different databases - so I want to be able to pass in the database name into the stored procedure creation statement.
SET #SourceDBName='dev';
DELIMITER //
CREATE PROCEDURE test ()
BEGIN
USE #SourceDBName;
SELECT * FROM agreed_relation;
END //
DELIMITER ;
How can I pass #SourceDBName into the CREATE PROCEDURE statement?
to pass in the database name into a stored procedure you must declare it in the procedure like this:
DELIMITER //
CREATE PROCEDURE test (IN idbname VARCHAR(20))
BEGIN
SELECT * FROM agreed_relation WHERE dbname = idbname;
END //
DELIMITER ;
After you create a Table like this where 'dbname' field it's the one your procedure will call:
create table agreed_relation
(
dbname varchar(30) not null
);
After insert some values you call the procedure and pass any dbname to the SELECT statament:
insert into agreed_relation(dbname) values('Oracle');
insert into agreed_relation(dbname) values('Mysql');
insert into agreed_relation(dbname) values('Mongodb');
Calling procedure and passing some value:
CALL test('Mysql');
14.1.16 CREATE PROCEDURE and CREATE FUNCTION Syntax
...
USE statements within stored routines are not permitted. When a
routine is invoked, an implicit USE db_name is performed (and undone
when the routine terminates). The causes the routine to have the given
default database while it executes. References to objects in databases
other than the routine default database should be qualified with the
appropriate database name.
...
An option to create the stored procedure in different databases is:
FIle: /path/to/file/myProcedure.sql
DROP PROCEDURE IF EXISTS `test`;
DELIMITER //
CREATE PROCEDURE `test`()
BEGIN
SELECT * FROM `agreed_relation`;
END//
DELIMITER ;
MySQL Command-Line:
mysql> USE `dev`;
Database changed
mysql> \. /path/to/file/myProcedure.sql
Query OK, 0 rows affected (0.00 sec)
mysql> USE `db1`;
Database changed
mysql> \. /path/to/file/myProcedure.sql
Query OK, 0 rows affected (0.00 sec)
mysql> USE `db2`;
Database changed
mysql> \. /path/to/file/myProcedure.sql
Query OK, 0 rows affected (0.00 sec)
mysql> USE `dev`;
Database changed
mysql> CALL `test`;
Empty set (0.01 sec)
Query OK, 0 rows affected (0.01 sec)
mysql> USE `db1`;
Database changed
mysql> CALL `test`;
Empty set (0.01 sec)
Query OK, 0 rows affected (0.01 sec)
mysql> USE `db2`;
Database changed
mysql> CALL `test`;
Empty set (0.01 sec)
Query OK, 0 rows affected (0.01 sec)
You can also create the stored procedure in a single database and pass the database name as a parameter to qualified the required objects and use 14.5 Prepared SQL Statement Syntax to execute it:
mysql> USE `dev`;
Database changed
mysql> DROP PROCEDURE IF EXISTS `test`;
Query OK, 0 rows affected (0.00 sec)
mysql> DELIMITER //
mysql> CREATE PROCEDURE `test`(`db_name` VARCHAR(64))
-> BEGIN
-> SET `db_name` := TRIM('\'' FROM QUOTE(`db_name`));
-> SET #`query` := CONCAT('SELECT * FROM `', `db_name`, '`.`agreed_relation`');
-> PREPARE `stmt` FROM #`query`;
-> EXECUTE `stmt`;
-> DEALLOCATE PREPARE `stmt`;
-> END//
Query OK, 0 rows affected (0.00 sec)
mysql> DELIMITER ;
mysql> CALL `test`('dev');
Empty set (0.01 sec)
Query OK, 0 rows affected (0.01 sec)
mysql> CALL `test`('db1');
Empty set (0.01 sec)
Query OK, 0 rows affected (0.01 sec)
mysql> CALL `test`('db2');
Empty set (0.01 sec)
Query OK, 0 rows affected (0.01 sec)

Extract XML in MySql with dynamic XPATH

I have two tables in a legacy database. One of them contains a field containing some xml. This other table contains the tags that constitutes the xml.
For example consider a table with a list of languages (e.g. en, fr, it) and a table with a field like
<en>Something</en><fr>Quelque chose</fr><it>Qualcosa</it>
I would like to extract all the translations. I have a query that goes like
SELECT GROUP_CONCAT(extractvalue(table.field, languages.sigla))
FROM table, languages
GROUP BY table.id
But I get the following error
[HY000][1105] Only constant XPATH queries are supported
I guess this is a limitation of MySql (I'm usign version 5.6). Is there any other way to obtain what I'm looking for?
One option you can try is (adjust as needed):
mysql> SELECT
-> GROUP_CONCAT('SELECT ExtractValue(#`xml`, \'', `der`.`lang`, '\') `lang`' SEPARATOR ' UNION ALL ') INTO #`query`
-> FROM (
-> SELECT 'en' `lang`
-> UNION
-> SELECT 'fr'
-> UNION
-> SELECT 'it'
-> ) `der`;
Query OK, 1 row affected (0.00 sec)
mysql> SET #`xml` := '<en>Something</en><fr>Quelque chose</fr><it>Qualcosa</it>';
Query OK, 0 rows affected (0.00 sec)
mysql> SET #`query` := CONCAT('SELECT GROUP_CONCAT(`der`.`lang`)
'> FROM (', #`query`, ') `der`');
Query OK, 0 rows affected (0.00 sec)
mysql> PREPARE `stmt` FROM #`query`;
Query OK, 0 rows affected (0.00 sec)
Statement prepared
mysql> EXECUTE `stmt`;
+----------------------------------+
| GROUP_CONCAT(`der`.`lang`) |
+----------------------------------+
| Something,Quelque chose,Qualcosa |
+----------------------------------+
1 row in set (0.00 sec)
mysql> DEALLOCATE PREPARE `stmt`;
Query OK, 0 rows affected (0.00 sec)
I faced the same problem and found an easy workaround in that blog post : http://sql-debug.blogspot.com/2012/05/extractvalue-only-constant-xpath.html
The solution is really surprising, but as silly as it can seem, it works perfectly...
It consists in creating a function that only "wraps" the ExtractValue function, in order to give it the xpath as an already generated string.
delimiter ##
create function exv(xml text, xpath text) returns text charset utf8
begin
return cast(extractvalue(xml, xpath) as char);
end ##
delimiter ;
And then, just replace extractvalue with exv in the query you tried to run when you got this [HY000][1105] error.
Of course, this workaround has a performance cost...

MySQL Stored procedure with local variable not working in LINUX

I'm struggling with a simple stored procedure, that I've reduced to this:
CREATE PROCEDURE RemoveDuplicateModules()
BEGIN
SET #myvar=1;
End;
When I run this in the MySQL CLI I get: parameter #myvar has not been created. I'm struggling cause in windows it works fine!
mysql --version = 5.6.33
Works for me (debian, mysql 5.6.25):
mysql> delimiter //
mysql> CREATE PROCEDURE RemoveDuplicateModules()
-> BEGIN
-> SET #myvar=1;
-> End;
-> //
Query OK, 0 rows affected (0,66 sec)
mysql> call RemoveDuplicateModules();
Query OK, 0 rows affected (0,00 sec)
mysql> select #myvar ;
+--------+
| #myvar |
+--------+
| 1 |
+--------+
1 row in set (0,02 sec)
Seems that the problem here was not that the procedure I posted was failing, but since it was being executed in a larger context, a big file with a lot of code from a lot of people, a previous lack of a commit made my procedure to fail. Anyway, thanks for the help!