<?xml version="1.0" encoding="UTF-8"?>
<essay xml:lang="en" version="5.0" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:gal="http://norman.walsh.name/rdf/gallery#" xmlns:foaf="http://xmlns.com/foaf/0.1/">
<info>
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
<title>SQL to XML</title><biblioid class="uri">http://norman.walsh.name/2009/09/26/sqltoxml</biblioid>
<volumenum>12</volumenum>
<issuenum>31</issuenum>
<pubdate>2009-09-26T13:01:23+01:00</pubdate>
<author>
      <personname>
<firstname>Norman</firstname>
	<surname>Walsh</surname>
</personname>
    </author>
<copyright>
      <year>2009</year>
      <holder>Norman Walsh</holder>
    </copyright>
<abstract>
<para>A number of Mac applications store information in SQLite databases.
Step one to do something useful with that data is to get it into XML.
</para>
</abstract>
<dc:subject rdf:resource="http://norman.walsh.name/knows/taxonomy#OSX"/>
<dc:subject rdf:resource="http://norman.walsh.name/knows/taxonomy#XML"/>
</info>

<para xml:id="p1">I hate having my data squirreled away in proprietary
or quasi-proprietary ways. If I can't get my information back out of
an app, I'd rather not use it.
When I started using the Mac, I switched to
<application>iCal</application> and
<application>AddressBook</application>: both can export data in standard
text formats which I can easily convert to XML.</para>

<para xml:id="p2">But exporting the data is a manual process (though I could
probably automate it with some clever AppleScript or something, I've
never tried). I build a number of views of my address book and
calendar data automatically so manual processes don't fit well into my 
workflow.</para>

<para xml:id="p3">It didn't take too long to figure out where
<application>iCal</application> stores my appointments or how to pull
them together. Having worked out where <application>iCal</application>
stores my appointments, I turned my attention to
<application>AddressBook</application>.</para>

<para xml:id="p4">Long story short: the address data is saved in a database
in <filename>~/Library/Application Support/AddressBook/AddressBook-v22.abcddb</filename>. After installing the <application>sqlite3</application> application
from MacPorts, I was able to extract a text dump. So far so good.</para>

<para xml:id="p5">Here, for
example, is a table definition and the first row of data in that
table:</para>

<programlisting>CREATE TABLE ZABCDRECORD ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER,
Z_OPT INTEGER, ZDISPLAYFLAGS INTEGER, ZMODIFICATIONDATEYEAR INTEGER,
ZCREATIONDATEYEAR INTEGER, ZADDRESSBOOKSOURCE INTEGER, ZISALL INTEGER,
ZME INTEGER, Z19_ME INTEGER, ZINFO INTEGER, ZBIRTHDAYYEAR INTEGER,
ZPRIVACYFLAGS INTEGER, ZNOTE INTEGER, ZADDRESSBOOKSOURCE1 INTEGER,
ZCONTACTINDEX INTEGER, ZSOURCEWHERECONTACTISME INTEGER, ZVERSION
INTEGER, ZSYNCCOUNT INTEGER, ZSHARECOUNT INTEGER, ZADDRESSBOOKSOURCE2
INTEGER, ZMODIFICATIONDATE TIMESTAMP, ZCREATIONDATE TIMESTAMP,
ZMODIFICATIONDATEYEARLESS FLOAT, ZCREATIONDATEYEARLESS FLOAT,
ZBIRTHDAY TIMESTAMP, ZBIRTHDAYYEARLESS FLOAT, ZUNIQUEID VARCHAR, ZNAME
VARCHAR, ZNAMENORMALIZED VARCHAR, ZTMPREMOTELOCATION VARCHAR, ZNAME1
VARCHAR, ZREMOTELOCATION VARCHAR, ZSERIALNUMBER VARCHAR, ZSUFFIX
VARCHAR, ZTITLE VARCHAR, ZTMPHOMEPAGE VARCHAR, ZNICKNAME VARCHAR,
ZORGANIZATION VARCHAR, ZMAIDENNAME VARCHAR, ZIDENTITYUNIQUEID VARCHAR,
ZPHONETICFIRSTNAME VARCHAR, ZDEPARTMENT VARCHAR, ZPHONETICLASTNAME
VARCHAR, ZMIDDLENAME VARCHAR, ZFIRSTNAME VARCHAR, ZIMAGEREFERENCE
VARCHAR, ZJOBTITLE VARCHAR, ZPHONETICMIDDLENAME VARCHAR, ZLASTNAME
VARCHAR, ZSORTINGFIRSTNAME VARCHAR, ZSORTINGLASTNAME VARCHAR,
ZCREATEDVERSION VARCHAR, ZLASTDOTMACACCOUNT VARCHAR, ZLASTSAVEDVERSION
VARCHAR, ZSYNCANCHOR VARCHAR, ZSEARCHELEMENTDATA BLOB,
ZMODIFIEDUNIQUEIDSDATA BLOB );
INSERT INTO "ZABCDRECORD" VALUES(1,18,287,NULL,NULL,2008,NULL,1,NULL,NULL,
3,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
239119683.670433,NULL,18281283.670433,NULL,NULL,
'93973926-7EF6-40F0-ADBD-8C7BBFC30FA1:ABSubscriptionRecord',NULL,
NULL,NULL,NULL,'local','B7303AAD-DA79-46E6-BC7D-91DAD82AEFB8',
NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL);</programlisting>

<para xml:id="p6">Next, I wrote
<link xlink:href="examples/sqltoxml">150 or so lines</link>
of <wikipedia>Perl</wikipedia> to convert the
text into XML.</para>

<para xml:id="p7">The XML is designed to be a totally straightforward
representation of the table structure of the database. From the preceding
SQL statements, <application>sqltoxml</application> produces:</para>

<programlisting>&lt;table name='ZABCDRECORD'&gt;
&lt;columns&gt;
  &lt;column name='Z_PK' type='INTEGER'/&gt;
  &lt;column name='Z_ENT' type='INTEGER'/&gt;
  &lt;column name='Z_OPT' type='INTEGER'/&gt;
  &lt;column name='ZDISPLAYFLAGS' type='INTEGER'/&gt;
  &lt;column name='ZMODIFICATIONDATEYEAR' type='INTEGER'/&gt;
  &lt;column name='ZCREATIONDATEYEAR' type='INTEGER'/&gt;
  &lt;column name='ZADDRESSBOOKSOURCE' type='INTEGER'/&gt;
  &lt;column name='ZISALL' type='INTEGER'/&gt;
  &lt;column name='ZME' type='INTEGER'/&gt;
  &lt;column name='Z19_ME' type='INTEGER'/&gt;
  &lt;column name='ZINFO' type='INTEGER'/&gt;
  &lt;column name='ZBIRTHDAYYEAR' type='INTEGER'/&gt;
  &lt;column name='ZPRIVACYFLAGS' type='INTEGER'/&gt;
  &lt;column name='ZNOTE' type='INTEGER'/&gt;
  &lt;column name='ZADDRESSBOOKSOURCE1' type='INTEGER'/&gt;
  &lt;column name='ZCONTACTINDEX' type='INTEGER'/&gt;
  &lt;column name='ZSOURCEWHERECONTACTISME' type='INTEGER'/&gt;
  &lt;column name='ZVERSION' type='INTEGER'/&gt;
  &lt;column name='ZSYNCCOUNT' type='INTEGER'/&gt;
  &lt;column name='ZSHARECOUNT' type='INTEGER'/&gt;
  &lt;column name='ZADDRESSBOOKSOURCE2' type='INTEGER'/&gt;
  &lt;column name='ZMODIFICATIONDATE' type='TIMESTAMP'/&gt;
  &lt;column name='ZCREATIONDATE' type='TIMESTAMP'/&gt;
  &lt;column name='ZMODIFICATIONDATEYEARLESS' type='FLOAT'/&gt;
  &lt;column name='ZCREATIONDATEYEARLESS' type='FLOAT'/&gt;
  &lt;column name='ZBIRTHDAY' type='TIMESTAMP'/&gt;
  &lt;column name='ZBIRTHDAYYEARLESS' type='FLOAT'/&gt;
  &lt;column name='ZUNIQUEID' type='VARCHAR'/&gt;
  &lt;column name='ZNAME' type='VARCHAR'/&gt;
  &lt;column name='ZNAMENORMALIZED' type='VARCHAR'/&gt;
  &lt;column name='ZTMPREMOTELOCATION' type='VARCHAR'/&gt;
  &lt;column name='ZNAME1' type='VARCHAR'/&gt;
  &lt;column name='ZREMOTELOCATION' type='VARCHAR'/&gt;
  &lt;column name='ZSERIALNUMBER' type='VARCHAR'/&gt;
  &lt;column name='ZSUFFIX' type='VARCHAR'/&gt;
  &lt;column name='ZTITLE' type='VARCHAR'/&gt;
  &lt;column name='ZTMPHOMEPAGE' type='VARCHAR'/&gt;
  &lt;column name='ZNICKNAME' type='VARCHAR'/&gt;
  &lt;column name='ZORGANIZATION' type='VARCHAR'/&gt;
  &lt;column name='ZMAIDENNAME' type='VARCHAR'/&gt;
  &lt;column name='ZIDENTITYUNIQUEID' type='VARCHAR'/&gt;
  &lt;column name='ZPHONETICFIRSTNAME' type='VARCHAR'/&gt;
  &lt;column name='ZDEPARTMENT' type='VARCHAR'/&gt;
  &lt;column name='ZPHONETICLASTNAME' type='VARCHAR'/&gt;
  &lt;column name='ZMIDDLENAME' type='VARCHAR'/&gt;
  &lt;column name='ZFIRSTNAME' type='VARCHAR'/&gt;
  &lt;column name='ZIMAGEREFERENCE' type='VARCHAR'/&gt;
  &lt;column name='ZJOBTITLE' type='VARCHAR'/&gt;
  &lt;column name='ZPHONETICMIDDLENAME' type='VARCHAR'/&gt;
  &lt;column name='ZLASTNAME' type='VARCHAR'/&gt;
  &lt;column name='ZSORTINGFIRSTNAME' type='VARCHAR'/&gt;
  &lt;column name='ZSORTINGLASTNAME' type='VARCHAR'/&gt;
  &lt;column name='ZCREATEDVERSION' type='VARCHAR'/&gt;
  &lt;column name='ZLASTDOTMACACCOUNT' type='VARCHAR'/&gt;
  &lt;column name='ZLASTSAVEDVERSION' type='VARCHAR'/&gt;
  &lt;column name='ZSYNCANCHOR' type='VARCHAR'/&gt;
  &lt;column name='ZSEARCHELEMENTDATA' type='BLOB'/&gt;
  &lt;column name='ZMODIFIEDUNIQUEIDSDATA' type='BLOB'/&gt;
&lt;/columns&gt;
&lt;rows&gt;
  &lt;row&gt;
    &lt;Z_PK&gt;1&lt;/Z_PK&gt;
    &lt;Z_ENT&gt;18&lt;/Z_ENT&gt;
    &lt;Z_OPT&gt;287&lt;/Z_OPT&gt;
    &lt;ZCREATIONDATEYEAR&gt;2008&lt;/ZCREATIONDATEYEAR&gt;
    &lt;ZISALL&gt;1&lt;/ZISALL&gt;
    &lt;ZINFO&gt;3&lt;/ZINFO&gt;
    &lt;ZCREATIONDATE&gt;239119683.670433&lt;/ZCREATIONDATE&gt;
    &lt;ZCREATIONDATEYEARLESS&gt;18281283.670433&lt;/ZCREATIONDATEYEARLESS&gt;
    &lt;ZUNIQUEID&gt;93973926-7EF6-40F0-ADBD-8C7BBFC30FA1:ABSubscriptionRecord&lt;/ZUNIQUEID&gt;
    &lt;ZREMOTELOCATION&gt;local&lt;/ZREMOTELOCATION&gt;
    &lt;ZSERIALNUMBER&gt;B7303AAD-DA79-46E6-BC7D-91DAD82AEFB8&lt;/ZSERIALNUMBER&gt;
  &lt;/row&gt;
  &lt;!-- ... --&gt;
&lt;/rows&gt;
&lt;/table&gt;</programlisting>

<para xml:id="p8">As you can see, I've made no effort to maintain some aspects of the
database (like the primary key), I've simply dropped NULL fields, and
I'm relying on the field names to be valid XML NCNames. There are clearly
other, equally reasonable, design choices that I could have made.</para>

<para xml:id="p9">The resulting XML is the bare minimum needed to switch to XML
tools for subsequent downstream processing (turning address book tables into VCards,
for example). But it gets the job done.</para>

<para xml:id="p10">And maybe it'll come in handy for someone else.</para>

</essay>

