Fedora Install LAMP
My students wanted an extra credit assignment, so I thought a LAMP configuration and test would be appropriate. The only problem was I hadn’t added it to their course VMware instance. So, here are the instructions to install Apache2, PHP, and MySQLi for a complete LAMP stack when MySQL is already installed.
The post builds on my Fedora Install of MySQL and MySQL Workbench on Fedora posts from last year. It also presumes that you’ve installed a studentdb database but you need to know how to do that let me know (but it hasn’t changed much from the example at the bottom of this old MySQL 5.1 blog post).
You install Apache2 with the following command as the root user, or with the sudo command as a sudoer-list user:
yum install httpd |
The following displays the results of starting the yum utility to install httpd, and you need to reply with a y to complete the installation:
Loaded plugins: langpacks, refresh-packagekit mysql-connectors-community | 2.5 kB 00:00 mysql-tools-community | 2.5 kB 00:00 mysql56-community | 2.5 kB 00:00 pgdg93 | 3.6 kB 00:00 updates/20/x86_64/metalink | 16 kB 00:00 updates | 4.9 kB 00:00 updates/20/x86_64/primary_db | 13 MB 00:04 (1/2): updates/20/x86_64/updateinfo | 1.9 MB 00:02 (2/2): updates/20/x86_64/pkgtags | 1.4 MB 00:01 Resolving Dependencies --> Running transaction check ---> Package httpd.x86_64 0:2.4.10-2.fc20 will be installed --> Processing Dependency: httpd-tools = 2.4.10-2.fc20 for package: httpd-2.4.10-2.fc20.x86_64 --> Processing Dependency: system-logos-httpd for package: httpd-2.4.10-2.fc20.x86_64 --> Running transaction check ---> Package fedora-logos-httpd.noarch 0:21.0.1-1.fc20 will be installed ---> Package httpd-tools.x86_64 0:2.4.10-2.fc20 will be installed --> Finished Dependency Resolution Dependencies Resolved ================================================================================ Package Arch Version Repository Size ================================================================================ Installing: httpd x86_64 2.4.10-2.fc20 updates 1.2 M Installing for dependencies: fedora-logos-httpd noarch 21.0.1-1.fc20 fedora 28 k httpd-tools x86_64 2.4.10-2.fc20 updates 79 k Transaction Summary ================================================================================ Install 1 Package (+2 Dependent packages) Total download size: 1.3 M Installed size: 4.0 M Is this ok [y/d/N]: y Downloading packages: (1/3): fedora-logos-httpd-21.0.1-1.fc20.noarch.rpm | 28 kB 00:00 (2/3): httpd-2.4.10-2.fc20.x86_64.rpm | 1.2 MB 00:01 (3/3): httpd-tools-2.4.10-2.fc20.x86_64.rpm | 79 kB 00:00 -------------------------------------------------------------------------------- Total 815 kB/s | 1.3 MB 00:01 Running transaction check Running transaction test Transaction test succeeded Running transaction (shutdown inhibited) Installing : httpd-tools-2.4.10-2.fc20.x86_64 1/3 Installing : fedora-logos-httpd-21.0.1-1.fc20.noarch 2/3 Installing : httpd-2.4.10-2.fc20.x86_64 3/3 Verifying : httpd-2.4.10-2.fc20.x86_64 1/3 Verifying : fedora-logos-httpd-21.0.1-1.fc20.noarch 2/3 Verifying : httpd-tools-2.4.10-2.fc20.x86_64 3/3 Installed: httpd.x86_64 0:2.4.10-2.fc20 Dependency Installed: fedora-logos-httpd.noarch 0:21.0.1-1.fc20 httpd-tools.x86_64 0:2.4.10-2.fc20 Complete! |
Next, you install php as the root user with the following command:
yum install php |
The following displays when you install php, and you need to reply with a y to complete the installation:
Loaded plugins: langpacks, refresh-packagekit Resolving Dependencies --> Running transaction check ---> Package php.x86_64 0:5.5.22-1.fc20 will be installed --> Processing Dependency: php-common(x86-64) = 5.5.22-1.fc20 for package: php-5.5.22-1.fc20.x86_64 --> Processing Dependency: php-cli(x86-64) = 5.5.22-1.fc20 for package: php-5.5.22-1.fc20.x86_64 --> Running transaction check ---> Package php-cli.x86_64 0:5.5.22-1.fc20 will be installed ---> Package php-common.x86_64 0:5.5.22-1.fc20 will be installed --> Processing Dependency: php-pecl-jsonc(x86-64) for package: php-common-5.5.22-1.fc20.x86_64 --> Running transaction check ---> Package php-pecl-jsonc.x86_64 0:1.3.6-1.fc20 will be installed --> Processing Dependency: /usr/bin/pecl for package: php-pecl-jsonc-1.3.6-1.fc20.x86_64 --> Processing Dependency: /usr/bin/pecl for package: php-pecl-jsonc-1.3.6-1.fc20.x86_64 --> Running transaction check ---> Package php-pear.noarch 1:1.9.5-6.fc20 will be installed --> Processing Dependency: php-xml for package: 1:php-pear-1.9.5-6.fc20.noarch --> Processing Dependency: php-posix for package: 1:php-pear-1.9.5-6.fc20.noarch --> Running transaction check ---> Package php-process.x86_64 0:5.5.22-1.fc20 will be installed ---> Package php-xml.x86_64 0:5.5.22-1.fc20 will be installed --> Finished Dependency Resolution Dependencies Resolved ================================================================================ Package Arch Version Repository Size ================================================================================ Installing: php x86_64 5.5.22-1.fc20 updates 2.6 M Installing for dependencies: php-cli x86_64 5.5.22-1.fc20 updates 3.9 M php-common x86_64 5.5.22-1.fc20 updates 1.0 M php-pear noarch 1:1.9.5-6.fc20 updates 343 k php-pecl-jsonc x86_64 1.3.6-1.fc20 updates 34 k php-process x86_64 5.5.22-1.fc20 updates 77 k php-xml x86_64 5.5.22-1.fc20 updates 247 k Transaction Summary ================================================================================ Install 1 Package (+6 Dependent packages) Total download size: 8.2 M Installed size: 32 M Is this ok [y/d/N]: y Downloading packages: (1/7): php-5.5.22-1.fc20.x86_64.rpm | 2.6 MB 00:03 (2/7): php-cli-5.5.22-1.fc20.x86_64.rpm | 3.9 MB 00:03 (3/7): php-common-5.5.22-1.fc20.x86_64.rpm | 1.0 MB 00:00 (4/7): php-pear-1.9.5-6.fc20.noarch.rpm | 343 kB 00:00 (5/7): php-pecl-jsonc-1.3.6-1.fc20.x86_64.rpm | 34 kB 00:00 (6/7): php-process-5.5.22-1.fc20.x86_64.rpm | 77 kB 00:00 (7/7): php-xml-5.5.22-1.fc20.x86_64.rpm | 247 kB 00:00 -------------------------------------------------------------------------------- Total 1.1 MB/s | 8.2 MB 00:07 Running transaction check Running transaction test Transaction test succeeded Running transaction (shutdown inhibited) Installing : php-cli-5.5.22-1.fc20.x86_64 1/7 Installing : php-process-5.5.22-1.fc20.x86_64 2/7 Installing : php-xml-5.5.22-1.fc20.x86_64 3/7 Installing : 1:php-pear-1.9.5-6.fc20.noarch 4/7 Installing : php-common-5.5.22-1.fc20.x86_64 5/7 Installing : php-pecl-jsonc-1.3.6-1.fc20.x86_64 6/7 Installing : php-5.5.22-1.fc20.x86_64 7/7 Verifying : php-5.5.22-1.fc20.x86_64 1/7 Verifying : php-common-5.5.22-1.fc20.x86_64 2/7 Verifying : php-cli-5.5.22-1.fc20.x86_64 3/7 Verifying : 1:php-pear-1.9.5-6.fc20.noarch 4/7 Verifying : php-process-5.5.22-1.fc20.x86_64 5/7 Verifying : php-xml-5.5.22-1.fc20.x86_64 6/7 Verifying : php-pecl-jsonc-1.3.6-1.fc20.x86_64 7/7 Installed: php.x86_64 0:5.5.22-1.fc20 Dependency Installed: php-cli.x86_64 0:5.5.22-1.fc20 php-common.x86_64 0:5.5.22-1.fc20 php-pear.noarch 1:1.9.5-6.fc20 php-pecl-jsonc.x86_64 0:1.3.6-1.fc20 php-process.x86_64 0:5.5.22-1.fc20 php-xml.x86_64 0:5.5.22-1.fc20 Complete! |
After installing the software, you can set the Apache server to start automatically with the following command:
chkconfig httpd on |
However, that command only starts the Apache server the next time you boot the server. You use the following command as the root user to start the Apache server:
apachectl start |
You can verify the installation with the following command as the root user:
ps -ef | grep httpd | grep -v grep |
It should return:
root 5433 1 0 17:03 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND apache 5434 5433 0 17:03 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND apache 5435 5433 0 17:03 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND apache 5436 5433 0 17:03 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND apache 5437 5433 0 17:03 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND apache 5438 5433 0 17:03 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND apache 5442 5433 0 17:03 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND |
and, then verify the listening port with the following command as the root user:
netstat -tulpn | grep :80 |
It should return the following when both the Apache server is listening on port 80 and the Oracle multi-protocol server is listening on port 8080:
tcp6 0 0 :::80 :::* LISTEN 5433/httpd tcp6 0 0 :::8080 :::* LISTEN 1505/tnslsnr |
After verifying the connection, you can test it by creating the traditional info.php program file in the /var/www/http directory. The file should contain the following:
1 2 3 | <?php phpinfo(); ?> |
You can test it by opening the Firefox browser and entering the following URL from the Fedora Linux image:
http://localhost/info.php |
It should display the typical diagnostic page. This verifies the configuration of the Apache and PHP servers. The next step verifies whether you have the mysqli library to connect to the MySQL database.
You create a mysqli_check.php script, like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <html>
<header>
<title>Static Query Object Sample</title>
<style type="text/css">
/* HTML element styles. */
table {background:white;border-style:solid;border-width:3px;border-color:black;border-collapse:collapse;}
th {text-align:center;font-style:bold;background:lightgray;border:solid 1px gray;}
td {border:solid 1px gray;}
/* Class tag element styles. */
.ID {min-width:50px;text-align:right;}
.Label {min-width:200px;text-align:left;}
</style>
</header>
<body>
<?php
if (!function_exists('mysqli_init') && !extension_loaded('mysqli')) {
print 'mysqli not installed.'; }
else {
print 'mysqli installed.'; }
?>
</script>
</body>
</html> |
You can test it with the following URL from the local browser:
http://localhost/mysqli_check.php |
If it’s installed you can skip the next step, but if not you need to run yum in expert mode as follows (the check for php-mysql isn’t really necessary because it’s too old a version but good practice):
[root@localhost etc]# yum shell Loaded plugins: langpacks, refresh-packagekit > remove php-mysql No Match for argument: php-mysql > install php-mysqlnd > run --> Running transaction check ---> Package php-mysqlnd.x86_64 0:5.5.22-1.fc20 will be installed --> Processing Dependency: php-pdo(x86-64) = 5.5.22-1.fc20 for package: php-mysqlnd-5.5.22-1.fc20.x86_64 --> Running transaction check ---> Package php-pdo.x86_64 0:5.5.22-1.fc20 will be installed --> Finished Dependency Resolution ================================================================================ Package Arch Version Repository Size ================================================================================ Installing: php-mysqlnd x86_64 5.5.22-1.fc20 updates 293 k Installing for dependencies: php-pdo x86_64 5.5.22-1.fc20 updates 141 k Transaction Summary ================================================================================ Install 1 Package (+1 Dependent package) Total download size: 433 k Installed size: 1.4 M Is this ok [y/d/N]: y Downloading packages: (1/2): php-mysqlnd-5.5.22-1.fc20.x86_64.rpm | 293 kB 00:00 (2/2): php-pdo-5.5.22-1.fc20.x86_64.rpm | 141 kB 00:00 -------------------------------------------------------------------------------- Total 427 kB/s | 433 kB 00:01 Running transaction check Running transaction test Transaction test succeeded Running transaction (shutdown inhibited) Installing : php-pdo-5.5.22-1.fc20.x86_64 1/2 Installing : php-mysqlnd-5.5.22-1.fc20.x86_64 2/2 Verifying : php-pdo-5.5.22-1.fc20.x86_64 1/2 Verifying : php-mysqlnd-5.5.22-1.fc20.x86_64 2/2 Installed: php-mysqlnd.x86_64 0:5.5.22-1.fc20 Dependency Installed: php-pdo.x86_64 0:5.5.22-1.fc20 Finished Transaction > quit |
You should note that this also installed PDO. One caveat, before you rerun the mysqli_check.php script from a browser, you need to restart the Apache server. You can do that as the root user with the following syntax:
apachectl restart |
You can retest it with the following URL from the local browser:
http://localhost/mysqli_check.php |
At this point you should have everything installed to test your connection the MySQL database. As mentioned, this example extends my instructions for installing MySQL on the Fedora instance.
The following query.php file tests your ability to connect to the MySQL database with the mysqli driver, and it uses the studentdb and video store example from my Oracle Database 11g and MySQL 5.6 Developer Handbook:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | <html>
<header>
<title>Static Query Object Sample</title>
<style type="text/css">
/* HTML element styles. */
table {background:white;border-style:solid;border-width:3px;border-color:black;border-collapse:collapse;}
th {text-align:center;font-style:bold;background:lightgray;border:solid 1px gray;}
td {border:solid 1px gray;}
/* Class tag element styles. */
.ID {min-width:50px;text-align:right;}
.Label {min-width:200px;text-align:left;}
</style>
</header>
<body>
<?php
// Assign credentials to connection.
$mysqli = new mysqli("localhost", "student", "student", "studentdb");
// Check for connection error and print message.
if ($mysqli->connect_errno) {
print $mysqli->connect_error."<br />";
print "Connection not established ...<br />";
}
else {
// Declare a static query.
$query = "SELECT au.system_user_id, au.system_user_name FROM system_user au" ;
// Loop through a result set until completed.
do {
// Attempt query and exit with failure before processing.
if (!$stmt = $mysqli->query($query)) {
// Print failure to resolve query message.
print $mysqli->error."<br />";
print "Failed to resolve query ...<br />";
}
else {
// Print the opening HTML table tag.
print '<table><tr><th class="ID">ID</th><th class="Label">User Role Name</th></tr>';
// Fetch a row for processing.
while( $row = $stmt->fetch_row() ) {
// Print the opening HTML row tag.
print "<tr>";
// Loop through the row's columns.
for ($i = 0;$i < $mysqli->field_count;$i++) {
// Handle column one differently.
if ($i == 0)
print '<td class="ID">'.$row[$i]."</td>";
else
print '<td class="Label">'.$row[$i]."</td>";
}
// Print the closing HTML row tag.
print "</tr>";
}
}
} while( $mysqli->next_result());
// Print the closing HTML table tag.
print "</table>";
// Release connection resource.
$mysqli->close(); }
?>
</script>
</body>
</html> |
This should display the following in the browser:
You can see how to open port 80 for the Apache server in this blog post. If you want to work with blob data types, you’ll also need to use yum to install the php-gd library. You can read my LAMP php-gd library blog post to learn how to install the libraries. As always, I hope a step-by-step approach without assumptions helps those learning MySQL.
Lowercase Table Names
A student posed the question about why table names are case sensitive. That’s because case sensitive table names are the default installation, as qualified in the MySQL documentation. You can verify that with the following query:
SELECT CASE WHEN @@lower_case_table_names = 1 THEN 'Case insensitive tables' ELSE 'Case sensitive tables.' END AS "Table Name Status"; |
The default value returned on Linux is:
+------------------------+ | Table Name Status | +------------------------+ | Case sensitive tables. | +------------------------+ 1 row in set (0.00 sec) |
The default value for the lower_case_table_names value on the Windows OS is 1 not 0 because you can inadvertently create a lowercase and case sensitive table when you write an INSERT statement and use a lowercase table name. I’ve provided that detail in a reply comment to this blog post.
You can change the default by adding the following parameter in the my.cnf file on Linux or the my.ini file on Windows:
# Make all tables case insensitive. lower_case_table_names=1 |
This lets you enter tables in upper or mixed case, and stores them in the data catalog as lowercase table names.
PostgreSQL Composites
PostgreSQL like Oracle supports record data types but unlike Oracle, PostgreSQL doesn’t support collections of record data types. Here’s an example of how to define a PostgreSQL composite data type, and how to use it as a column’s data type.
CREATE TYPE address_type AS ( street_address VARCHAR , city VARCHAR , state VARCHAR , zip_code VARCHAR ); |
Then, you define an ADDRESS table, like:
CREATE TABLE address ( address_id SERIAL , address_struct ADDRESS_TYPE ); |
You can now insert rows like:
-- Insert the first row. INSERT INTO address ( address_struct ) VALUES (('52 Hubble Street','Lexington','KY','40511-1225')); -- Insert the second row. INSERT INTO address ( address_struct ) VALUES (('54 Hubble Street','Lexington','KY','40511-1225')); |
Then, you can query them like this:
SELECT * FROM address; |
It returns:
address_id | address_struct
------------+----------------------------------------------
1 | ("52 Hubble Street",Lexington,KY,40511-1225)
2 | ("54 Hubble Street",Lexington,KY,40511-1225)
(2 rows) |
You must use parentheses around the ADDRESS_STRUCT column to query individual items, like:
SELECT address_id , (address_struct).street_address , (address_struct).city , (address_struct).state , (address_struct).zip_code FROM address; |
It returns output like a table:
address_id | street_address | city | state | zip_code
------------+------------------+-----------+-------+------------
1 | 52 Hubble Street | Lexington | KY | 40511-1225
2 | 54 Hubble Street | Lexington | KY | 40511-1225
(2 rows) |
While you can define a table that holds an array of a composite type, there’s no syntax that appears to work with an array of a composite type. I hope this helps those interested in implementing record structures in PostgreSQL.
PostgreSQL Auto IDs
PostgreSQL’s approach to automatic numbering is as simple as Oracle but different than MySQL, and Microsoft SQL Server. For example, you have a two-step process with Oracle, PostgreSQL, MySQL, and Microsoft SQL Server. First, you create an Oracle table with the GENERATED AS IDENTITY clause, a PostgreSQL table with the SERIAL data type, a MySQL table with the AUTO_INCREMENT clause, and a Microsoft SQL Server table with the IDENTITY(1,1) clause. Then, you need to write an INSERT statement for Oracle, MySQL, or Microsoft SQL Server like:
- Oracle’s
INSERTstatement excludes the auto-incrementing column from the list of columns or provides aNULLvalue in theVALUES-list. You can then assign theRETURNING INTOresult from anINSERTstatement to a session-level (bind) variable. - MySQL’s
INSERTstatement excludes the auto-incrementing column from the list of columns or provides aNULLvalue in theVALUES-list. You can then assign theLAST_INSERT_ID()function value to a session-level variable, and populate a foreign key column. - Microsoft SQL Server’s
INSERTstatement excludes the auto-incrementing column from the list of columns or provides aNULLvalue in theVALUES-list. You can then assign theSCOPE_IDENTITY()function’s value to a session-level variable, and populate a foreign key column.
PostgreSQL differs because it works differently between the SQL and PL/pgSQL contexts. Let’s look at how you link the insert of data into two tables in both contexts.
The following PostgreSQL syntax creates an ADDRESS table with an auto incrementing ADDRESS_ID column that uses a SERIAL data type, which acts like an auto numbering column:
/* Create a customer table. */ CREATE TABLE customer ( customer_id SERIAL CONSTRAINT customer_pk PRIMARY KEY , first_name VARCHAR(20) , last_name VARCHAR(20)); /* Create an address table. */ CREATE TABLE address ( address_id SERIAL CONSTRAINT address_pk PRIMARY KEY , customer_id INTEGER , street_address VARCHAR(40) , city VARCHAR(30) , state VARCHAR(8) , zip_code VARCHAR(10)); |
If you want to insert one row into the CUSTOMER table and a related row in the ADDRESS table. You have two possible approaches. One works in both the SQL and PL/pgSQL contexts. That mechanism requires you to use a scalar subquery to capture the foreign key value of the CUSTOMER_ID column in the ADDRESS table, like this:
/* Insert into customer table. */ INSERT INTO customer ( first_name, last_name ) VALUES ('F. Scott','Fitzgerald'); /* Insert into address table. */ INSERT INTO address ( customer_id , street_address , city , state , zip_code ) VALUES ((SELECT customer_id FROM customer WHERE first_name = 'F. Scott' AND last_name = 'Fitzgerald') ,'599 Summit Avenue' ,'St. Paul' ,'Minnesota' ,'55102'); |
The RETURNING INTO clause of PostgreSQL only works in a PL/pgSQL context, like this:
DO $$ DECLARE lv_customer_id INTEGER; BEGIN /* Insert into customer table. */ INSERT INTO customer ( first_name, last_name ) VALUES ('Madeleine','Smith') RETURNING customer_id INTO lv_customer_id; /* Insert into address table. */ INSERT INTO address ( customer_id , street_address , city , state , zip_code ) VALUES ( lv_customer_id ,'7 Blythswood Square' ,'Glasgow' ,'Scotland' ,'G2 4BG'); /* Manage any exceptions. */ EXCEPTION WHEN OTHERS THEN RAISE NOTICE '% %', SQLERRM, SQLSTATE; END$$; |
You query the auto generated values and data from the INSERT statement to the CUSTOMER table with a scalar subquery against the natural key (the FIRST_NAME and LAST_NAME columns) from the ADDRESS table. The following is an example of such a query:
SELECT * FROM customer c INNER JOIN address a ON c.customer_id = a.customer_id; |
It returns:
customer_id | first_name | last_name | address_id | customer_id | street_address | city | state | zip_code
-------------+------------+------------+------------+-------------+---------------------+----------+-----------+----------
1 | F. Scott | Fitzgerald | 1 | 1 | 599 Summit Avenue | St. Paul | Minnesota | 55102
2 | Madeleine | Smith | 2 | 2 | 7 Blythswood Square | Glasgow | Scotland | G2 4BG
(2 rows) |
My take is that the RETURNING column_value INTO local_value clause is a better approach than using Oracle’s .NEXTVAL and .CURRVAL values. I also think the RETURNING INTO clause is a better approach than using MySQL’s LAST_INSERT_ID() or Microsoft SQL Server’s SCOPE_IDENTITY().
Initially, I felt it was odd that the PostgreSQL disallows the RETURNING INTO clause in a SQL context, because it allows the syntax in a PL/pgSQL context. After some reflection the choice makes more sense because most developers work within a procedural context when they use transactions across two or more tables. PL/pgSQL is PostgreSQL’s procedural context from managing transactions across two or more tables.
As always, I hope this helps.
i-Blazon Prime
I replaced my OtterBox Defender with an i-Blazon Prime case when upgrading to the iPhone 6. It appeared a good option, and for 4 months it was fine. Yesterday, the latch broke and my iPhone fell to the ground as a walked. Fortunately, my iPhone landed flat, the i-Blazon plastic case absorbed the impact, and my iPhone was undamaged.
Here’s a photograph of the damaged case:
While I wanted to order only the belt portion of the case, that’s not possible like it is with the OtterBox Defender. So, I ordered a new i-Blazon. My plan is to monitor the belt case. If it cracks again, I’ll protect the telephone and I’ll know to move back to the OtterBox Defender.
SQL Server XQuery
I promised my students an example of writing xquery statements in Microsoft SQL Server. This post builds on two earlier posts. The first qualifies how to build a marvel table with source data, and the second qualifies how you can create an XML Schema Collection and insert relational data into an XML structure.
You can query a sequence with xquery as follows:
DECLARE @x xml; SET @x = N''; SELECT @x.query('(1,2,(10,11,12,13,14,15)),-6'); |
It returns:
1 2 10 11 12 13 14 15 -6 |
You can query a sequence with an xquery FLOWR statement. FLOWR stands for: FOR, LET, ORDER BY, WHERE, and RETURN. A sample xquery with a FLOWER statement is:
DECLARE @x xml; SET @x = N''; SELECT @x.query('for $i in ((1,2,(10,11,12,13,14,15)),-6) order by $i return $i'); |
It returns:
-6 1 2 10 11 12 13 14 15 |
You can query the entire node tree with the following xquery statement because it looks for the occurrence of any node with the /* search string:
DECLARE @x xml; SET @x = N'<marvel> <avenger_name>Captain America</avenger_name> </marvel>'; SELECT @x.query('/*'); |
You can query the avenger_name elements from the marvel_xml table with the following syntax:
SELECT xml_table.query('/marvel/avenger_name') FROM marvel_xml; |
It returns the following set of avenger_name elements:
<avenger_name>Hulk</avenger_name> <avenger_name>Iron Man</avenger_name> <avenger_name>Black Widow</avenger_name> <avenger_name>Thor</avenger_name> <avenger_name>Captain America</avenger_name> <avenger_name>Hawkeye</avenger_name> <avenger_name>Winter Soldier</avenger_name> <avenger_name>Iron Patriot</avenger_name> |
You can query the fourth avenger_name element from the marvel_xml table with the following xquery statement:
SELECT xml_table.query('/marvel[4]/avenger_name') FROM marvel_xml; |
It returns the following avenger_name element:
<avenger_name>Thor</avenger_name> |
You can use the value() function to verify an avenger_name element exists:
SELECT CASE WHEN xml_table.value('(/marvel[4]/avenger_name)','nvarchar') = 'T' THEN 'Exists' END AS "Verified" FROM marvel_xml WHERE id = 3; |
The query returns the Exists string when it finds a valid avenger_name element. You have a number of other tools to query results sets from the XML node tree.
I hope this helps my students and anybody interested in writing xquery-enable queries.
Insert into XML Column
Working through Chapter 7 of the Querying Microsoft SQL Server 2012 book for Microsoft’s Exam 70-461, I found the XML examples incomplete for my students. I decided to put together a post on how to create:
- An XML Schema Collection type.
- A table that uses an XML Schema Collection as a column’s data type.
- An example on how you can transfer the contents of a table into the XML Schema Collection.
This post assumes you understand the basics about XML structures. If you’re unfamiliar with XML, please note that everything within it is case sensitive unlike SQL. You raise exceptions when the case of your XML fails to match the case of your XML Schema Collection definitions. I raised the following exception by using a Marvel element tag in title case when the XML Schema Collection uses a lowercase marvel element tag:
Msg 6913, Level 16, State 1, Line 2 XML Validation: Declaration not found for element 'Marvel'. Location: /*:Marvel[1] |
The basic marvel table is defined in this earlier blog post. To ensure you don’t run into conflicts with previously existing objects, you can delete the marvel table with the following syntax:
1 2 | IF OBJECT_ID('studentdb.marvel_xml','U') IS NOT NULL DROP TABLE marvel_xml; |
There is no predefined function that lets you conditionally drop the XML Schema Collection from the data base. The alternative is to query the Microsoft SQL Server data catalog for the existence of a row before dropping the XML Schema Collection, like this:
1 2 3 4 | IF EXISTS (SELECT * FROM sys.xml_schema_collections WHERE name = 'MarvelXMLTable') DROP XML SCHEMA COLLECTION MarvelXMLTable; ELSE SELECT 'Not found.'; |
You can now create the MarvelXMLTable XML Schema Collection with the following syntax:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | CREATE XML SCHEMA COLLECTION MarvelXMLTable AS N'<?xml version="1.0" encoding="UTF-16"?> <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:sqltypes="http://schemas.microsoft.com/sqlserver/2004/sqltypes"> <xsd:import namespace="http://schemas.microsoft.com/sqlserver/2004/sqltypes" schemaLocation="http://schemas.microsoft.com/sqlserver/2004/sqltypes/sqltypes.xsd" /> <xsd:element name="Marvel"> <xsd:complexType> <xsd:sequence> <xsd:element name="marvel_id" type="sqltypes:int" /> <xsd:element name="avenger_name" type="sqltypes:nvarchar" /> <xsd:element name="first_name" type="sqltypes:nvarchar" /> <xsd:element name="last_name" type="sqltypes:nvarchar" /> </xsd:sequence> </xsd:complexType> </xsd:element> </xsd:schema>'; |
After creating the XML Schema Collection, you can confirm whether it works correctly with the following statements. It will fail when you use semicolons on the DECLARE or SET lines, so avoid them as shown below:
1 2 3 4 5 6 7 8 | DECLARE @marvel AS XML(MarvelXMLTable) SET @marvel = '<marvel> <marvel_id>1</marvel_id> <avenger_name>Falcon</avenger_name> <first_name>Sam</first_name> <last_name>Wilson</last_name> </marvel>' SELECT @marvel; |
After creating and verifying the integrity of the XML Schema Collection, you can create a marvel_xml table. The xml_table column of the marvel_xml table uses a strongly-typed XML type, as shown:
1 2 3 | CREATE TABLE marvel_xml ( id INT IDENTITY(1,1) CONSTRAINT marvel_xml_pk PRIMARY KEY , xml_table XML(MarvelXMLTable)); |
You can write an INSERT statement with single element like this:
1 2 3 4 5 6 7 8 9 | INSERT INTO marvel_xml ( xml_table ) VALUES ('<Marvel> <marvel_id>9</marvel_id> <avenger_name>Falcon</avenger_name> <first_name>Sam</first_name> <last_name>Wilson</last_name> </Marvel>'); |
You can insert two elements with an INSERT statement like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | INSERT INTO marvel_xml ( xml_table ) VALUES ('<Marvel> <marvel_id>9</marvel_id> <avenger_name>Falcon</avenger_name> <first_name>Sam</first_name> <last_name>Wilson</last_name> </Marvel> <Marvel> <marvel_id>10</marvel_id> <avenger_name>Scarlet Witch</avenger_name> <first_name>Wanda</first_name> <last_name>Witch</last_name> </Marvel>'); |
There’s a lot of typing to insert XML literal values. It a lot easier to transfer relational data from a table to an XML type. Unfortunately, Microsoft didn’t make it very easy. The FOR XML AUTO doesn’t work for two reasons. First, the FOR XML AUTO clause doesn’t render the correct XML structure. Second, the INSERT statement disallows nested SELECT queries that include the FOR XML AUTO clause.
Here’s the way you insert relational data into an XML type column:
1 2 3 4 5 6 7 8 9 10 11 12 13 | DECLARE @xml_elements NVARCHAR(MAX); SET @xml_elements = N''; WITH x AS (SELECT CONCAT(N'<marvel>' ,N'<marvel_id>',marvel_id,N'</marvel_id>' ,N'<avenger_name>',avenger_name,N'</avenger_name>' ,N'<first_name>',first_name,N'</first_name>' ,N'<last_name>',last_name,N'</last_name>' ,N'</marvel>') AS element FROM marvel) SELECT @xml_elements += element FROM x; INSERT INTO marvel_xml (xml_table) VALUES (@xml_elements); |
Line 1 declares an xml_element session variable. Line 2 initializes the xml_element session variable as an empty string. The WITH clause on lines 3 through 10 creates a Common Table Expression (CTE) with the valid XML structure for all rows in the marvel table. The nested SELECT statement on line 11 returns data from the CTE and adds returned row to the session-level string variable. Finally, the INSERT statement on lines 12 and 13 inserts the XML table into the xml_table column.
You can then query the table with the following statement:
SELECT xml_table FROM marvel_xml; |
Click on the return type in the SQL Server Management Studio (SSMS), and the XML structure will expand to show this:
<marvel> <marvel_id>1</marvel_id> <avenger_name>Hulk</avenger_name> <first_name>Bruce</first_name> <last_name>Banner</last_name> </marvel> <marvel> <marvel_id>2</marvel_id> <avenger_name>Iron Man</avenger_name> <first_name>Tony</first_name> <last_name>Stark</last_name> </marvel> <marvel> <marvel_id>3</marvel_id> <avenger_name>Black Widow</avenger_name> <first_name>Natasha</first_name> <last_name>Romanoff</last_name> </marvel> <marvel> <marvel_id>4</marvel_id> <avenger_name>Thor</avenger_name> <first_name>Thor</first_name> <last_name>Odinsson</last_name> </marvel> <marvel> <marvel_id>5</marvel_id> <avenger_name>Captain America</avenger_name> <first_name>Steve</first_name> <last_name>Rogers</last_name> </marvel> <marvel> <marvel_id>6</marvel_id> <avenger_name>Hawkeye</avenger_name> <first_name>Clint</first_name> <last_name>Barton</last_name> </marvel> <marvel> <marvel_id>7</marvel_id> <avenger_name>Winter Soldier</avenger_name> <first_name>Bucky</first_name> <last_name>Barnes</last_name> </marvel> <marvel> <marvel_id>8</marvel_id> <avenger_name>Iron Patriot</avenger_name> <first_name>James</first_name> <last_name>Rhodey</last_name> </marvel> |
As always, I hope this post benefits those who read it.
Functions disallow NDS
My students asked if you could embed an OFFSET x ROWS FETCH NEXT y ROWS ONLY clause in a SQL Server T-SQL user-defined function. The answer is no, it isn’t Oracle (yes, you can do that in Oracle Database 12c with an NDS statement). There’s an example in Chapter 2 of my Oracle Database 12c PL/SQL Programming book if you’re interested. I also demonstrate a different approach to SQL Server T-SQL table functions in this older post. However, an attempt to add the clause to a SQL Server T-SQL function, like this:
CREATE FUNCTION studentdb.getBatch (@rows AS INT ,@offset AS INT) RETURNS @output TABLE ( marvel_id INT , avenger_name VARCHAR(30) , first_name VARCHAR(20) , last_name VARCHAR(20)) AS BEGIN /* Insert the results into the table variable. */ INSERT @output SELECT marvel_id , avenger_name , first_name , last_name FROM studentdb.marvel OFFSET (@offset - 1) ROWS FETCH NEXT @rows ROWS ONLY; /* Return the table variable from the function. */ RETURN; END; |
Throws the following errors trying to compile the function:
Msg 102, Level 15, State 1, Procedure getBatch, Line 16 Incorrect syntax near '@offset'. Msg 153, Level 15, State 2, Procedure getBatch, Line 16 Invalid usage of the option NEXT in the FETCH statement. |
If you have a strong background in Oracle and can sort through the dynamic SQL syntax for T-SQL, you might try re-writing the function to use the EXEC SP_EXECUTESQL @var; command. That rewrite that attempts to use NDS (Native Dynamic SQL) would look like this:
CREATE FUNCTION studentdb.getBatch (@rows AS INT ,@offset AS INT) RETURNS @output TABLE ( marvel_id INT , avenger_name VARCHAR(30) , first_name VARCHAR(20) , last_name VARCHAR(20)) AS BEGIN DECLARE /* Declare a variable for a dynamic SQL statement. */ @stmt VARCHAR(400); /* Assign the SQL statement to a variable. */ SET @stmt = N'SELECT marvel_id ' + N', avenger_name ' + N', first_name ' + N', last_name ' + N'FROM studentdb.marvel ' + N'OFFSET ' + (@offset - 1) + N' ' + N'ROWS FETCH NEXT ' + @rows + N' ROWS ONLY;'; BEGIN /* Insert the results into the table variable. */ INSERT @output EXEC sp_executesql @stmt; END; /* Return the table variable from the function. */ RETURN; END; |
Throws the following exception because you can’t use dynamic dispatch inside a T-SQL function:
Msg 443, Level 16, State 14, Procedure getBatch, Line 23 Invalid use of a side-effecting operator 'INSERT EXEC' within a function. |
On the other hand you can rewrite the statement with a BETWEEN operator and it works somewhat like an OFFSET and FETCH operation. That refactored function would be written as follows:
CREATE FUNCTION studentdb.getBatch (@rowa AS INT ,@rowb AS INT) RETURNS @output TABLE ( marvel_id INT , avenger_name VARCHAR(30) , first_name VARCHAR(20) , last_name VARCHAR(20)) AS BEGIN /* Insert the results into the table variable. */ INSERT @output SELECT marvel_id , avenger_name , first_name , last_name FROM studentdb.marvel WHERE marvel_id BETWEEN @rowa AND @rowb; /* Return the table variable from the function. */ RETURN; END; |
It doesn’t raise an exception. You can call the table function like this:
SELECT * FROM getBatch(2,3); |
It returns the two rows for Iron Man and Black Widow. As always, I hope this helps.
If you want to create the test case, here’s the script you need:
SELECT 'Conditionally drop studentdb.marvel table.' AS "Statement"; IF OBJECT_ID('studentdb.marvel','U') IS NOT NULL DROP TABLE studentdb.marvel; SELECT 'Create studentdb.marvel table.' AS "Statement"; CREATE TABLE studentdb.marvel ( marvel_id INT NOT NULL IDENTITY(1,1) CONSTRAINT marvel_pk PRIMARY KEY , avenger_name VARCHAR(30) NOT NULL , first_name VARCHAR(20) NOT NULL , last_name VARCHAR(20) NOT NULL); /* Insert the rows. */ INSERT INTO studentdb.marvel (avenger_name, first_name, last_name) VALUES ('Hulk','Bruce','Banner'); INSERT INTO studentdb.marvel (avenger_name, first_name, last_name) VALUES ('Iron Man','Tony','Stark'); INSERT INTO studentdb.marvel (avenger_name, first_name, last_name) VALUES ('Black Widow','Natasha','Romanoff'); INSERT INTO studentdb.marvel (avenger_name, first_name, last_name) VALUES ('Thor','Thor','Odinsson'); INSERT INTO studentdb.marvel (avenger_name, first_name, last_name) VALUES ('Captain America','Steve','Rogers'); INSERT INTO studentdb.marvel (avenger_name, first_name, last_name) VALUES ('Hawkeye','Clint','Barton'); INSERT INTO studentdb.marvel (avenger_name, first_name, last_name) VALUES ('Winter Soldier','Bucky','Barnes'); INSERT INTO studentdb.marvel (avenger_name, first_name, last_name) VALUES ('Iron Patriot','James','Rhodey'); /* Query the contents of the MARVEL table. */ SELECT * FROM studentdb.marvel; |
Filtering String Dates
A question came up about how to verify dates from a string without throwing a casting error because of a non-conforming date. You can throw a number of exceptions, and I wrote a function to filter bad string formats like the DD-MON-RR or DD-MON-YYYY.
The first one is for a day between 1 and the last day of month, which is:
ORA-01847: day of month must be between 1 and last day of month |
An incorrect string for a month, raises the following error:
ORA-01843: not a valid month |
A date format mask longer than a DD-MON-RR or DD-MON-YYYY raises the following exception:
ORA-01830: date format picture ends before converting entire input string |
The verify_date function checks for non-conforming DD-MON-RR and DD-MON-YYYY date masks, and substitutes a SYSDATE value for a bad date entry:
CREATE OR REPLACE FUNCTION verify_date ( pv_date_in VARCHAR2) RETURN DATE IS /* Local return variable. */ lv_date DATE; BEGIN /* Check for a DD-MON-RR or DD-MON-YYYY string. */ IF REGEXP_LIKE(pv_date_in,'^[0-9]{2,2}-[ADFJMNOS][ACEOPU][BCGLNPRTVY]-([0-9]{2,2}|[0-9]{4,4})$') THEN /* Case statement checks for 28 or 29, 30, or 31 day month. */ CASE /* Valid 31 day month date value. */ WHEN SUBSTR(pv_date_in,4,3) IN ('JAN','MAR','MAY','JUL','AUG','OCT','DEC') AND TO_NUMBER(SUBSTR(pv_date_in,1,2)) BETWEEN 1 AND 31 THEN lv_date := pv_date_in; /* Valid 30 day month date value. */ WHEN SUBSTR(pv_date_in,4,3) IN ('APR','JUN','SEP','NOV') AND TO_NUMBER(SUBSTR(pv_date_in,1,2)) BETWEEN 1 AND 30 THEN lv_date := pv_date_in; /* Valid 28 or 29 day month date value. */ WHEN SUBSTR(pv_date_in,4,3) = 'FEB' THEN /* Verify 2-digit or 4-digit year. */ IF (LENGTH(pv_date_in) = 9 AND MOD(TO_NUMBER(SUBSTR(pv_date_in,8,2)) + 2000,4) = 0 OR LENGTH(pv_date_in) = 11 AND MOD(TO_NUMBER(SUBSTR(pv_date_in,8,4)),4) = 0) AND TO_NUMBER(SUBSTR(pv_date_in,1,2)) BETWEEN 1 AND 29 THEN lv_date := pv_date_in; ELSE /* Not a leap year. */ IF TO_NUMBER(SUBSTR(pv_date_in,1,2)) BETWEEN 1 AND 28 THEN lv_date := pv_date_in; ELSE lv_date := SYSDATE; END IF; END IF; ELSE /* Assign a default date. */ lv_date := SYSDATE; END CASE; ELSE /* Assign a default date. */ lv_date := SYSDATE; END IF; /* Return date. */ RETURN lv_date; END; / |
You can check valid dates with a DD-MON-RR format:
SELECT verify_date('28-FEB-10') AS "Non-Leap Year" , verify_date('29-FEB-12') AS "Leap Year" , verify_date('31-MAR-14') AS "31-Day Year" , verify_date('30-APR-14') AS "30-Day Year" FROM dual; |
You can check valid dates with a DD-MON-YYYY format:
SELECT verify_date('28-FEB-2010') AS "Non-Leap Year" , verify_date('29-FEB-2012') AS "Leap Year" , verify_date('31-MAR-2014') AS "31-Day Year" , verify_date('30-APR-2014') AS "30-Day Year" FROM dual; |
They both return:
Non-Leap Leap YEAR 31-DAY YEAR 30-DAY YEAR ----------- --------- ----------- ----------- 28-FEB-10 29-FEB-12 31-MAR-14 30-APR-14 |
You can check badly formatted dates with the following query:
SELECT verify_date('28-FEB-2010') AS "Non-Leap Year" , verify_date('29-FEB-2012') AS "Leap Year" , verify_date('31-MAR-2014') AS "31-Day Year" , verify_date('30-APR-2014') AS "30-Day Year" FROM dual; |
You can screen for an alphanumeric string with the following expression:
SELECT 'Valid alphanumeric string literal' AS "Statement" FROM dual WHERE REGEXP_LIKE('Some Mythical String $200','([:alnum:]|[:punct:]|[:space:])*'); |
You can screen for a numeric literal as a string with the following expression:
SELECT 'Valid numeric literal' AS "Statement" FROM dual WHERE REGEXP_LIKE('123.00','([:digit:]|[:punct:])'); |
As always, I hope this helps those who need this type of solution.
A PL/pgSQL Function
Somebody wanted to know how to write a basic PostgreSQL PL/pgSQL function that returned a full name whether or not the middle name was provided. That’s pretty simple. There are principally two ways to write that type of concatenation function. One uses formal parameter names and the other uses positional values in lieu of the formal parameter names.
The two ways enjoy two techniques (SQL language and PL/pgSQL language), which gives us four possible solutions. I’ve also provided a conditional drop statement for the full_name function. If you’re new to PostgreSQL the DROP statement might make you scratch your head because you’re wondering why you need to use the formal parameter list. The DROP statement needs the parameter list because PostgeSQL lets you overload schema/database functions and procedures.
The code is for a named parameter lists using the SQL language is:
DROP FUNCTION IF EXISTS full_name ( IN pv_first_name text , IN pv_middle_name text , IN pv_full_name text); CREATE FUNCTION full_name ( IN pv_first_name text , IN pv_middle_name text , IN pv_last_name text , OUT pv_full_name text) AS 'SELECT pv_first_name || CASE WHEN pv_middle_name IS NOT NULL THEN '' '' || pv_middle_name || '' '' ELSE '' '' END || pv_last_name' LANGUAGE SQL; |
The code is for a positional parameter lists using the SQL language is:
DROP FUNCTION IF EXISTS full_name ( IN text , IN text , IN text); CREATE FUNCTION full_name ( IN text , IN text , IN text , OUT text) AS 'SELECT $1 || CASE WHEN $2 IS NOT NULL THEN '' '' || $2 || '' '' ELSE '' '' END || $3' LANGUAGE SQL; |
You would re-write the function in the PL/pgSQL language as follows (please note the named parameter list):
CREATE FUNCTION full_name ( IN pv_first_name text , IN pv_middle_name text , IN pv_last_name text) RETURNS text AS $$ DECLARE lv_output text; BEGIN IF pv_middle_name IS NULL THEN lv_output = CONCAT(pv_first_name, N' ', pv_last_name); ELSE lv_output = CONCAT(pv_first_name, N' ', pv_middle_name, N' ', pv_first_name); END IF; RETURN lv_output; END $$ LANGUAGE plpgsql IMMUTABLE; |
You can test either version of the program with the following two queries from the pseudo table dual, which isn’t require in the SELECT statement:
SELECT full_name('Henry',NULL,'Pym') AS "Ant-Man" UNION ALL SELECT full_name('Henry','''Hank''','Pym') AS "Ant-Man"; |
It prints:
Ant-Man text ---------------- Henry Pym Henry 'Hank' Pym |
As always, I hope this helps those looking for how to accomplish a concatenation function in PostgreSQL.

