Extract XML in MySql with dynamic XPATH - mysql

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...

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)

How to select string between delimiters and insert into other column in MySQL?

Let's say I have a table in a MySQL database with two columns first_string and second_string.
first_string contains this sentence: Hello! This is my first test. I like this test.
I want to get all between the delimiters my and test (including the delimiters itself) and insert it in second_string.
So second_string should be: my first test.
It's important to take the first available delimiters: I need my first test and not my first test. I like this test.
This is what I have so far:
SELECT SUBSTRING(#Text, CHARINDEX('my', #Text), CHARINDEX('test',#text) - CHARINDEX('my', #Text) + Len('test'));
But I don't know how to do the rest. And will that code always look for the first available delimiter?
I hope anybody can help me. Thanks!
Do you mean some thing like this ?
mysql> SET #Text='Hello! This is my first test. I like this test.';
Query OK, 0 rows affected (0,00 sec)
SET #Text='Hello! This is my first test. I like this test.';
SELECT
SUBSTRING(#Text, INSTR(#Text,'my')
, (INSTR(#Text,'test') - INSTR(#Text,'my') + CHAR_LENGTH('test')))
INTO #result;
SELECT #result;
Sample
mysql> SET #Text='Hello! This is my first test. I like this test.';
mysql> SELECT
-> SUBSTRING(#Text, INSTR(#Text,'my')
-> , (INSTR(#Text,'test') - INSTR(#Text,'my') + CHAR_LENGTH('test')))
-> INTO #result;
Query OK, 1 row affected (0,00 sec)
mysql> SELECT #result;
+---------------+
| #result |
+---------------+
| my first test |
+---------------+
1 row in set (0,00 sec)
mysql>

Dynamic SQL alternatives (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]>

MySQL select where like path

Setting a wordpress project to its staging environment, I have ran into an issue regarding paths that were set for the development environment, and don't fit the ones in staging.
So I need to update the paths in the database, from C:\xampp\htdocs\site.com to /var/www/site.com
At first, I tried replacing, the same way I replaced the urls:
update `wp_slider` set `url` = replace(`url`, 'http://local.', 'http://');
Then the paths:
update `wp_slider` set `path` = replace(`path`, 'C:\xampp\htdocs\site.com', '/var/www/site.com');
Which actually didn't work. Then I tried a SELECT to see what rows can I retrieve:
SELECT * FROM `wp_slider` WHERE `path` LIKE "%C:\xampp\htdocs\site.com%"
Which will return an empty result. What am I missing?
Forgot to mention, that I tried escaping the \ by doing \\ and I still get no result
A full path of what I'm trying to replace would be like: C:\xampp\htdocs\site.com/wp-content/plugins/slider/skins/slider\circle\circle.css
That's roughly the way to go:
mysql> SELECT REPLACE('C:\\xampp\\htdocs\\site.com\\foo\\bar.txt', 'C:\\xampp\\htdocs\\site.com', '/var/www/site.com');
+----------------------------------------------------------------------------------------------------------+
| REPLACE('C:\\xampp\\htdocs\\site.com\\foo\\bar.txt', 'C:\\xampp\\htdocs\\site.com', '/var/www/site.com') |
+----------------------------------------------------------------------------------------------------------+
| /var/www/site.com\foo\bar.txt |
+----------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
mysql>
If you get zero matches that's because your DB records do not contain what you think they do. Make sure you don't have blanks or control characters. If your MySQL client does not make it easy to spot such things, you can always use HEX():
SELECT path, HEX(path)
FROM wp_slider
WHERE path NOT LIKE "C:\\xampp\\htdocs\\site.com%"
Additionally, I'm not fully sure you can use \ as path separator in Unix systems. I suggest you replace it as well:
UPDATE wp_slider
SET path = replace(path, '\\', '/')
WHERE path IS NOT NULL
Update:
What I'm trying to explain is that your procedure is basically correct (except that escaping \ is not always optional):
mysql> CREATE TABLE wp_slider(
-> path VARCHAR(2083)
-> );
Query OK, 0 rows affected (0.06 sec)
mysql> INSERT INTO wp_slider (path) VALUES ('C:\\xampp\\htdocs\\site.com/wp-content/plugins/slider/skins/slider\\circle\\circle.cs
s');
Query OK, 1 row affected (0.04 sec)
mysql> UPDATE wp_slider SET path=REPLACE(path, 'C:\\xampp\\htdocs\\site.com', '/var/www/site.com');
Query OK, 1 row affected (0.03 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SELECT * FROM wp_slider;
+----------------------------------------------------------------------------+
| path |
+----------------------------------------------------------------------------+
| /var/www/site.com/wp-content/plugins/slider/skins/slider\circle\circle.css |
+----------------------------------------------------------------------------+
1 row in set (0.00 sec)
If you don't get matches it's because your database contains different data than you think, such as (but not restricted to) whitespace or control characters:
mysql> TRUNCATE TABLE wp_slider;
Query OK, 0 rows affected (0.03 sec)
mysql> INSERT INTO wp_slider (path) VALUES ('C:\xampp\htdocs\site.com/wp-content/plugins/slider/skins/slider\circle\circle.css');
Query OK, 1 row affected (0.02 sec)
mysql> UPDATE wp_slider SET path=REPLACE(path, 'C:\\xampp\\htdocs\\site.com', '/var/www/site.com');
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1 Changed: 0 Warnings: 0
mysql> SELECT * FROM wp_slider;
+------------------------------------------------------------------------------+
| path |
+------------------------------------------------------------------------------+
| C:xampphtdocssite.com/wp-content/plugins/slider/skins/slidercirclecircle.css |
+------------------------------------------------------------------------------+
1 row in set (0.00 sec)
In this last example, we forgot to escape \ when inserting and as a result we don't get a match when replacing because the input data is not what we thought it was.
You need to escape the backslashes: \\

Dynamically choosing a column in MySQL

I know how to pipe one MySQL query into another:
SELECT user_name FROM users WHERE user_id=( SELECT user_id FROM entries WHERE header="foo" );
Out of pure intellectual curiosity, how I dynamically choose a column or a table?
Ex:
SELECT (
SELECT column_name FROM column_names WHERE id = 1
) FROM (
SELECT table_name FROM table_names WHERE id = 1
);
Use a prepared statement:
mysql> SET #sql = CONCAT("SELECT ", (SELECT "NOW()"));
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT #sql;
+--------------+
| #sql |
+--------------+
| SELECT NOW() |
+--------------+
1 row in set (0.00 sec)
mysql> PREPARE stmt FROM #sql;
Query OK, 0 rows affected (0.00 sec)
Statement prepared
mysql> EXECUTE stmt;
+---------------------+
| NOW() |
+---------------------+
| 2009-04-06 23:08:31 |
+---------------------+
1 row in set (0.00 sec)
I'm pretty sure this is impossible with a regular query or view.
In answer to your first question, you should learn about how to do JOIN in SQL. A join is a fundamental operation in the SQL language. It's as important is understanding how to do a loop in other languages.
SELECT DISTINCT users.user_name
FROM users JOIN entries USING (user_id)
WHERE entries.header = 'foo';
Regarding your second question, no, you can't make table names or column names dynamic within a single statement.
However, you can write code in your application to build a SQL statement as a string, based on looking up column names and table names. Then execute the resulting string as a new SQL query.
You can do it by querying the information_schema.columns table.
Do this and check the results. I'm not sure what you're trying to do but that table contains anything related to your columns:
SELECT * FROM information_schema.`COLUMNS` C;
BTW, I don't know any way of doing this in a single query. You should get the columns information and then create a new query in your coding language, whatever that is.