MacLochlainns Weblog

Michael McLaughlin's Technical Blog

Site Admin

Archive for the ‘sql’ Category

Ruby-MySQL Program

with 6 comments

After you install Ruby and build the Rails framework, you need to create the mysql gem. This blog post shows you how to create the mysql gem and how to write a simple Ruby program that queries the MySQL database.

The first step creates the mysql gem for Ruby programming:

gem install mysql

It should show you the following:

Fetching: mysql-2.9.1.gem (100%)
Building native extensions.  This could take a while...
Successfully installed mysql-2.9.1
Parsing documentation for mysql-2.9.1
Installing ri documentation for mysql-2.9.1
Done installing documentation for mysql after 0 seconds
1 gem installed

After you install the mysql Ruby Gem, you can write and test a test.rb Ruby program that tests a MySQL database connection. The simplest complete code looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Include Ruby Gem libraries.
require 'rubygems'
require 'mysql'
 
begin
  # Create new database connection.
  db = Mysql.new('localhost','student','student','studentdb')
  # Print connected message.
  puts "Connected to the MySQL database server."
rescue Mysql::Error => e
  # Print the error.
  puts "ERROR #{e.errno} (#{e.sqlstate}): #{e.error}"
  puts "Can't connect to the MySQL database specified."
  # Signal an error.
  exit 1
ensure
  # Close the connection when it is open.
  db.close if db
end

You can run the program with the following syntax:

ruby test.rb

The program prints “Connected to the MySQL database server.” when there’s a student user with a student password that’s authorized to connect to the studentdb database. If any of the values are invalid when creating the connection, the program prints “Can’t connect to the MySQL database specified.”

Having tested the connection, the next query.rb program tests the connection by returning values from a query:

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
# Include Ruby Gem libraries.
require 'rubygems'
require 'mysql'
 
# Begin block.
begin
  # Create a new connection resource.
  db = Mysql.new('localhost','student','student','studentdb1')
  # Create a result set.
  rs = db.query('SELECT item_title FROM item')
  # Read through the result set hash.
  rs.each_hash do | row |
    puts "#{row['item_title']}"
  end
  # Release the result set resources.
  rs.free
rescue Mysql::Error => e
  # Print the error.
  puts "ERROR #{e.errno} (#{e.sqlstate}): #{e.error}"
  puts "Can't connect to MySQL database specified."
  # Signal an error.
  exit 1
ensure
  # Close the connection when it is open.
  db.close if db
end

You can test it with the following command-line syntax:

ruby query.rb

It returns a data set like this from the item table of my video store example:

+---------------------------------------+
| item_title                            |
+---------------------------------------+
| The Hunt for Red October              |
| Star Wars I                           |
| Star Wars II                          |
| Star Wars II                          |
| Star Wars III                         |
| The Chronicles of Narnia              |
| RoboCop                               |
| Pirates of the Caribbean              |
| The Chronicles of Narnia              |
| MarioKart                             |
| Splinter Cell                         |
| Need for Speed                        |
| The DaVinci Code                      |
| Cars                                  |
| Beau Geste                            |
| I Remember Mama                       |
| Tora! Tora! Tora!                     |
| A Man for All Seasons                 |
| Hook                                  |
| Around the World in 80 Days           |
| Harry Potter and the Sorcerer's Stone |
| Camelot                               |
+---------------------------------------+
22 rows in set (0.00 sec)

You need the ruby interpreter to run them. You can make the programs standalone operations by putting the following line as the first line in your Ruby programs.

1
#!/usr/bin/ruby

Then, you can run the program like this if they have read and execute privileges and are located in the present working directory where you issue the following command:

./mysql_query.rb

If you want to work with individual columns, please check this subsequent post that shows how you can access individual columns. As always, I hope this helps those trying to get things working.

After posting this somebody asked for books that could help them learn how to write Ruby programs. While books are nice and listed below, I’d start with the tryruby.org web site.

I’d recommend the following books because …

  • The Ruby Programming Language is 7 years old now and only covers Ruby 1.8 and 1.9, but its written by David Flanagan and the creator of the Ruby Programming Language – Yukihiro Matsumoto.
  • Programming Ruby 1.9 & 2.0: The Pragmatic Programmer’s Guide is more current and a well balanced approach at learning how to write Ruby programs.
  • The Well-Grounded Rubyist is the most current book and teaches you how to think about writing Ruby beyond just the syntax. As a Manning book, you can purchase the physical copy and automatically get a downloadable ebook. It’s certainly the best value for the money option provided you already know how to program in at least one other object-oriented programming language.

Written by maclochlainn

April 11th, 2015 at 2:35 am

APEX Create Table

with 2 comments

The following walks you through how you sign on to a STUDENT Workspace with Oracle’s APEX product. It shows you how to create a new table with the Object Browser tool.

You can find instructions on how to create your own STUDENT Workspace in this blog post. Overall, Oracle APEX is a valuable tool to learn and master.

UseStudentAPEX01

  1. You start the process by accessing the Oracle Database 11g APEX, which you can access at http://localhost:8080/apex by default on the server. If you’ve got a static IP address for your instance, you can replace localhost with the IP address or hostname for the IP address.

    • Workspace: STUDENT
    • Username:  ADMIN
    • Password:  STUDENT

UseStudentAPEX02

  1. After you login to the STUDENT workspace, you have four options. They are the: Application Builder, SQL Workshop, Team Development, and Administration. You start the process by accessing the Oracle Database 11g APEX, which you can access at http://localhost:8080/apex by default on the server. If you’ve got a static IP address for your instance, you can replace localhost with the IP address or hostname for the IP address. Click on the Object Browser icon to proceed.

UseStudentAPEX02A

  1. Clicking the SQL Workshop icon takes you to the second level menu. You click the Object Browser icon to create a database object.

OracleAPEXCT01

  1. After clicking the Object Browser icon, you see the screen at the left. Click the Create button to create a table.

OracleAPEXCT02

  1. After clicking the Create button, you see the screen at the left. Click the type of database object that you want to create. In our case, we click the Table hypertext to start the create table workflow.

OracleAPEXCT03

  1. After clicking the Table hyperlink, you see the Create Table screen at the left. Enter the column names, choose their data types and set the scale and precision. You should also check the Not Null checkbox when you want a column to be mandatory. Click the Next button to continue the create table workflow.

OracleAPEXCT04

  1. After entering the column names, you should choose the data types, enter the scale and precision, and check the NOT NULL checkbox to make appropriate columns mandatory by applying NOT NULL database constraints. If you run out of entry rows, you can click the Add Column button to add new rows. Click the Next button to continue the create table workflow when you’ve defined the columns.

OracleAPEXCT05

  1. After defining the column names, you should choose whether the primary key will use a new sequence or an existing sequence. You also have the ability to not assign a primary key value or simply leave it unpopulated when inserting new rows. The example creates an IMAGE_PK primary key constraint on the IMAGE_ID column, and declares an IMAGE_SEQ sequence value. Click the Next button to continue the create table workflow when you’ve defined the primary key constraint and any new sequence value for the primary key column.

OracleAPEXCTFK1

  1. After defining the primary key constraint, you can define foreign key column constraints. You enter a foreign key constraint name, choose between a Disallow Delete, Cascade Delete, or Set Null on Delete rule, select the foreign key column, the foreign key’s referenced table and column. Click the Add button to continue the create table workflow.

OracleAPEXCTFK2

  1. After defining a foreign key constraint, you can see the constraint that you created. Then, you can define another foreign key column constraints. You repeat the steps from the prior steps to add another foreign key constraint. Click the Add button to create a second foreign key constraint and complete the create table workflow.

OracleAPEXCTFK3

  1. After defining a second foreign key constraint, you see the following two foreign key constraints. Click the Next button to complete the create table workflow.

OracleAPEXTCUK01

  1. After defining all the foreign key constraints, you can create check and unique constraints. You check a radio button for a check or unique constraint, and then you select the columns for the constraint’s key. Click the /Add button to create any check or unique constraints as part of the create table workflow.

OracleAPEXTCUK02

  1. After defining all check and unique key constraints, you can see them in the Constraints box. Click the Next button to complete the create table workflow.

OracleAPEXCTC01

  1. After defining all items about the table, you can see the SQL to create the IMAGE table and its constraints. You can copy the SQL into a file for later use when writing a re-runnable script. Click the Create button to complete the create table workflow and create the table.

     

    The following are the contents of the script for the actions you’ve defined:

    CREATE table "IMAGE" (
        "IMAGE_ID"         NUMBER NOT NULL,
        "FILE_NAME"        VARCHAR2(60) NOT NULL,
        "MIME_TYPE"        NUMBER NOT NULL,
        "ITEM_IMAGE"       BLOB,
        "CREATED_BY"       NUMBER NOT NULL,
        "CREATION_DATE"    DATE NOT NULL,
        "LAST_UPDATED_BY"  NUMBER NOT NULL,
        "LAST_UPDATE_DATE" DATE NOT NULL,
        constraint  "IMAGE_PK" primary key ("IMAGE_ID")
    )
    /
    
    CREATE sequence "IMAGE_SEQ" 
    /
    
    CREATE trigger "BI_IMAGE"  
      before insert on "IMAGE"              
      for each row 
    begin  
      if :NEW."IMAGE_ID" is null then
        select "IMAGE_SEQ".nextval into :NEW."IMAGE_ID" from dual;
      end if;
    end;
    /   
    
    ALTER TABLE "IMAGE" ADD CONSTRAINT "IMAGE_FK1" 
    FOREIGN KEY ("CREATED_BY")
    REFERENCES "SYSTEM_USER" ("SYSTEM_USER_ID")
    
    /
    ALTER TABLE "IMAGE" ADD CONSTRAINT "IMAGE_FK2" 
    FOREIGN KEY ("LAST_UPDATED_BY")
    REFERENCES "SYSTEM_USER" ("SYSTEM_USER_ID")
    
    /
    alter table "IMAGE" add
    constraint "IMAGE_UK1" 
    unique ("FILE_NAME","MIME_TYPE")
    /   
    

OracleAPEXTable

  1. After creating the table, trigger, sequence, and constraints, you can see the table definition. You also have the ability to modify the table. At this point, you can create another structure or you can click the Home or SQL Workshop menu choice.

As always, I hope this helps those looking to learn new things and approaches.

Written by maclochlainn

April 7th, 2015 at 10:21 pm

Open Fedora Port 80

with 4 comments

After installing the LAMP stack on Fedora, you need to open port 80 in the Firewall to access the PHP programs on the Fedora instance from external servers. You can open a firewall port by launching the firewall-config application as the root user with the following syntax:

firewall-config

The firewall-config utility opens the following dialog:

FedoraFirewall1

Click on the Ports tab, and you’ll see the following:

FedoraFirewall2

Skip this step if you only want to set the runtime privilege to the port. Click on the Runtime tab and change it to Permanent if you want the port to be accessible when you reboot your OS.

FedoraFirewallPermanent

Click on Add button to add a port exception, and you’ll see the following:

FedoraFirewall3

Enter Port 80 for the Apache server unless you used a different value for the Apache server’s listener port. If you’re not sure open the /etc/httpd/conf/httpd.conf file and check for the following line (default value shown):

Listen 80

Click the OK button to set the port exception. Then, you can connect to the Linux machine with the IP address, a DNS name, or a name you resolve in your local hosts file, like:

http://192.168.2.1/query.php

You can find the IP address of your Fedora image by inspecting the /etc/hosts file or capture a DHCP assigned address with the following command as the root user (or with sudo as a valid sudoer user):

ifconfig -a

It should return the following image, which is based on the data stored in MySQL’s studentdb database, as qualified in yesterday’s blog post:

ExternalWebPage

I hope this helps those setting up a LAMP instance to work with the MySQL database.

Written by maclochlainn

March 29th, 2015 at 12:35 am

Lowercase Table Names

with 6 comments

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.

Written by maclochlainn

March 22nd, 2015 at 11:53 am

PostgreSQL Composites

without comments

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.

Written by maclochlainn

March 16th, 2015 at 2:02 am

PostgreSQL Auto IDs

with 2 comments

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:

  1. Oracle’s INSERT statement excludes the auto-incrementing column from the list of columns or provides a NULL value in the VALUES-list. You can then assign the RETURNING INTO result from an INSERT statement to a session-level (bind) variable.
  2. MySQL’s INSERT statement excludes the auto-incrementing column from the list of columns or provides a NULL value in the VALUES-list. You can then assign the LAST_INSERT_ID() function value to a session-level variable, and populate a foreign key column.
  3. Microsoft SQL Server’s INSERT statement excludes the auto-incrementing column from the list of columns or provides a NULL value in the VALUES-list. You can then assign the SCOPE_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.

Written by maclochlainn

March 15th, 2015 at 9:37 pm

SQL Server XQuery

with 2 comments

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.

Written by maclochlainn

February 19th, 2015 at 1:54 am

Insert into XML Column

with one comment

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.

Written by maclochlainn

February 18th, 2015 at 2:22 am

Functions disallow NDS

with 2 comments

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;

Written by maclochlainn

February 14th, 2015 at 5:20 pm

Filtering String Dates

with 2 comments

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.

Written by maclochlainn

February 2nd, 2015 at 12:53 am