Hierarchy Queries
Creating a Transitive Closure to Optimize Rollups
Data Warehouse Extract Transform and Load (ETL) processing requires loading hierarchy descriptions. These include chart-of-accounts (COA), organization, geography as examples. These hierarchies are not easily dictated like a time hierarchy; these are discovered from hierarchies in other reporting applications.
A hierarchy is a transitive relationship between a parent and a number of children. In a relational database this is most often modeled as children with a foreign key references to their common parent.
If this transitive relationship is of arbitrary depth, it is notoriously difficult to process in SQL. Each query requires an arbitrary number of joins to locate all parents of a child or all children of a parent. The usual work-around is to limit the depth (for example to 8 levels); this limits the queries to at most 8 joins.
Another common solution is to create a "transitive closure". This "closes" or pre-joins all levels of the hierarchy, creating all possible paths. This transitive closure can be used for all queries using a single SELECT to find all parents of a child or all children of a parent. We will assign "subtree range numbers" to each node during the transitive closure. These subtree range numbers are based on the Heap data structure described in the Cormen/Leiserson/Rivest/Stein: Introduction to Algorithms.
The only constraint on the transitive closure is that it is kept in synchronization with the actual parent-child relationships. When a hierarchy is changed, the transitive closure must be recomputed.
This document presents overall requirements, design of the transitive closure algorith, and the implementation of both transitive closure and the data generator.
This demonstration creates a transitive closure on hierarchical data kept in a relational database. The parent-child table is scanned to create the transitive closure in a separate table.
Proper subtree range numbers have the following properties. Assume a three level tree of g, p1, p2, and c1a, c1b. Each node has two attributes, l and h.
The tree has the following form.
g / \ p1 p2 / \ c1a c1b
The following strict ordering applies among the subtree numbers:
g.l <= p1.l <= c1a.l <= c1a.h < c1b.l <= c1b.h <= p1.h < p2.l <= p2.h <= g.h
The parent-child relationship can be summarized as p.l <= all children <= p.h.
This leads to very simple queries for locating parents or children:
By adding an additional attribute of level number, we can locate the immediate parent or child of a node, using the relationship parent.level+1 = child.level. Given the immediate parent, we can locate siblings (all nodes of our level that share our immediate parent).
Two separate programs are defined. One creates the database and then loads a hierarchy into the pc table. For a quick supply of sample data, a file directory structure is scanned to load a hierarchy. The other program scans the parent-child table and creates the transitive closure.
The essence of transitive closure is a traversal of the parent-child tree. This can be based on the Visitor design pattern. The Visitor patterns suggests that we design a class to contain the structure being visited and a separate family of related classes for the operations that will be performed on the various elements of this structure.
The structure that will be visited is the parent-child hierarchy represented in the relational database. We define a TreeTable class to contain the basic operations for working with this structure. This class also provides the basic visit methods. The are a variety of alternative orderings to the elements of a structure. In our case, we are interested in traversal that visits each parent node before visiting all the children and after visiting all the children. This could be called a pre-and-post-order depth-first traversal.
There are a wide variety of possible operations that can be performed when traversing the hierarchy. We will focus on two: reporting and assigning subtree range numbers. The reporting operation simply prints each node in the hierarchy. The assignment of subtree range numbers is done by saving the first number on descending to a child node, and the last number on ascending from a child node. This is used to set the number of a parent node.
A simple extension allows selective renumbering of a portion of a tree. This must be done after an insert; it can be done to balance numbers after a delete. The procedure is as follows.
The main program can have the following general form.
f= TreeTable("hier.db")
s= SetNumbers(5)
f.prep(s)
f.visit(s)
f.finish(s)
A TreeTable instance, f includes a database connection to the named database. A SetNumbers visitor instance, s is used to assign subtree numbers. The prep method clears the transitive closure result table. The visit step applies the visitor, s, to each node in the tree. The finish step commits the result.
This small demonstration was implemented as two Python programs: hierarchyDB.py and transclose.py. The hierarchyDB.py loads a hierarchy database by examining a random disk directory. The transclose.py updates that hierarchy database by creating the transitive closure information.
The hierarchyDB.py program creates test data. It defines the SampleDB class which contains methods to create and load a database. The main driver uses an instance of this class to create and load the database.
This program has the following structure.
"hierarchyDB.py" (1) =
→DB Shell Escape (4)
→DB DOC String (2)
→DB CVS Cruft and pyweb generator warning (5)
→DB Imports (3)
→DB SampleDB class defines methods to create and load a database (6)
→DB Main Driver to create and load our test database (9)
◊ "hierarchyDB.py" (1).
Overheads includes the following: the shell escape, the doc string, any CVS cruft, and the imports.
The doc string identifies the module source.
DB DOC String (2) =
"""HierarchyDB.py
Create a hierarchy database by traversing a directory tree.
"""
◊ DB DOC String (2). Used by hierarchyDB.py (1).
The following imports are used by this module. The os module provides information on a directory tree, used to populate the database. The time module is used to decode the time stamps on each file. The pprint module is used to create nice listings of complex Python data structures. The sqlite module provides a simple relational database.
DB Imports (3) =
import sys, os, time, pprint
from pysqlite2 import dbapi2 as sqlite
◊ DB Imports (3). Used by hierarchyDB.py (1).
These are other overheads of a Python program: the Shebang line, also know as the Shell Escape, and some CVS Cruft in case this is checked into CVS or SVN or similar tool.
DB Shell Escape (4) =
#!/usr/bin/env python
◊ DB Shell Escape (4). Used by hierarchyDB.py (1).
DB CVS Cruft and pyweb generator warning (5) =
__version__ = """$Revision$"""
### DO NOT EDIT THIS FILE!
### It was created by ..\pyWeb-1.4\pyweb.py, __version__='$Revision$'.
### From source transclose.w modified Fri Jan 05 17:20:22 2007.
### In working directory 'G:\Projects\transclose'.
◊ DB CVS Cruft and pyweb generator warning (5). Used by hierarchyDB.py (1).
The SampleDB class performs all of the processing to create and populate a database. This class has the following instance variables and methods.
DB SampleDB class defines methods to create and load a database (6) =
class SampleDB( object ):
"""Manage a simple database for transitive closure demonstration."""
def __init__( self, name_="hierarchy.db" ):
self.dbName= name_
self.connection= None
self.sequence= 0
def execute( self, statement, bindTuple=() ):
"""Execute a simple non-query statement."""
cursor= self.connection.cursor()
try:
cursor.execute( statement, bindTuple )
except sqlite.OperationalError, e:
print e
cursor.close()
def query( self, statement ):
"""Execute a simple query."""
cursor= self.connection.cursor()
cursor.execute( statement )
for row in cursor.fetchall():
print row
cursor.close()
→DB SampleDB Create the empty database (7)
→DB SampleDB Load the database by visiting a disk structure (8)
◊ DB SampleDB class defines methods to create and load a database (6). Used by hierarchyDB.py (1).
Creating an empty database is a matter of creating the parent-child table, pc, and the table to contain the transitive closure information, trans.
.DB SampleDB Create the empty database (7) =
def create( self ):
"""Create the database and schema."""
self.connection= sqlite.connect(self.dbName)
self.execute( """CREATE TABLE pc( key integer,
dirName varchar, fileName varchar, mtime varchar, size integer )""" )
self.execute( """CREATE TABLE trans( key integer,
level integer, low integer, high integer )""" )
self.connection.commit()
#self.query( """PRAGMA database_list""" )
self.connection.close()
print "Create Complete",self.dbName
◊ DB SampleDB Create the empty database (7). Used by DB SampleDB class defines methods to create and load a database (6) :: hierarchyDB.py (1).
Loading the essential parent-child table, trans involves a little hand-shake with Python's os.path.walk function. The os.path.walk function does a callback with each directory in the tree, and a list of all files in that directory. The SampleDB.visit method is the required callback function, used by os.path.walk.
This callback function gathers some basic file system information, and inserts that information into the pc table. It collects modification time and size of each file.
DB SampleDB Load the database by visiting a disk structure (8) =
def load( self, fromDirectory ):
"""Load the parent-child tree into the pc table."""
self.connection= sqlite.connect(self.dbName)
# traverse a directory tree to create some sample data
os.path.walk( fromDirectory, SampleDB.visit, self )
self.connection.commit()
self.query( """select * from pc""" )
self.connection.commit()
self.connection.close()
print "Load Complete",self.dbName
def visit( self, dirname, names ):
"""A quick hack that would fail if a directory name matched a file name."""
#print dirname
#pprint.pprint( names )
path,base=os.path.split(dirname)
for f in names:
mTime= os.path.getmtime( os.path.join(dirname,f) )
if mTime <= 0: continue # Skip files with odd timestamps
mTimeStr= time.asctime( time.localtime( mTime ) )
size= os.path.getsize( os.path.join(dirname,f) )
self.sequence += 1
self.execute( """insert into pc(key,dirName,fileName,mtime,size)
values(?,?,?,?,?)""",
(self.sequence,base,f,mTimeStr,size) )
◊ DB SampleDB Load the database by visiting a disk structure (8). Used by DB SampleDB class defines methods to create and load a database (6) :: hierarchyDB.py (1).
Note that there is a subtle bug in database design and the load method. This design has a file using the parent directory's name as the foreign key. It should use the assigned surrogate key instead. File and directory names are not guaranteed to be unique, and the surrogate key is essential to correctly constructing a tree representing directories.
The main driver creates a database and loads it. It sets s to an instance of SampleDB. It then invokes the create method to create a new, empty database. It invokes the load method to populate this database using the L: drive or ~/Documents directory structure.
DB Main Driver to create and load our test database (9) =
def main():
s= SampleDB("hier.db")
s.create()
rootDirectory= ".."
s.load(rootDirectory)
if __name__ == "__main__":
main()
◊ DB Main Driver to create and load our test database (9). Used by hierarchyDB.py (1).
The transclose.py program demonstrates the transitive closure algorithm. This program defines the basic TreeTable class that manages a parent-child tree structure in a relational table. The program also defines a Visitor class hierarchy that can be used to construct a transitive closure of a tree structure, assigning subtree numbers to speed up processing.
This program has the following structure.
"transclose.py" (10) =
→TC Shell Escape (13)
→TC DOC String (11)
→TC CVS Cruft and pyweb generator warning (14)
→TC Imports (12)
→TC Visitor base class definition (15)
→TC TreeTable class to process a parent-child relationship table (16)
→TC ShowDirectory subclass of Visitor to report on a structure (18)
→TC SetNumbers subclass of Visitor to assign numbers (19)
→TC Main Driver (20)
◊ "transclose.py" (10).
These are some standard overheads of Python programs.
This is the doc string which appears in the source.
TC DOC String (11) =
"""Transitive Closure Demo.
This application does a transitive closure on a hierarchy.
This will do a depth-first traversal of the data, assigning node numbers in such
a way that for a given node, n, the following are true.
If c is a child of n, then n.low <= c.low <= c.high <= n.high.
If p is a parent of n, then p.low <= n.low <= n.high <= p.high.
This allows simple SQL queries to locate all children or all parents of a given node.
>"""
◊ TC DOC String (11). Used by transclose.py (10).
The following imports are used by this module. The os module is used to separate the database name into path and filename components. The sqlite module provides a simple relational database.
TC Imports (12) =
import os
from pysqlite2 import dbapi2 as sqlite
◊ TC Imports (12). Used by transclose.py (10).
These are other overheads of a Python program: the Shebang line, also know as the Shell Escape, and some CVS Cruft in case this is checked into CVS or SVN or similar tool.
TC Shell Escape (13) =
#!/usr/local/bin/python
◊ TC Shell Escape (13). Used by transclose.py (10).
TC CVS Cruft and pyweb generator warning (14) =
__version__ = """$Revision$"""
### DO NOT EDIT THIS FILE!
### It was created by ..\pyWeb-1.4\pyweb.py, __version__='$Revision$'.
### From source transclose.w modified Fri Jan 05 17:20:22 2007.
### In working directory 'G:\Projects\transclose'.
◊ TC CVS Cruft and pyweb generator warning (14). Used by transclose.py (10).
The Visitor base class provides the superclass for all TreeTable visitors.
This class has the following instance variables.
TC Visitor base class definition (15) =
class Visitor( object ):
def __init__( self, step=1 ):
self.step= step
self.count= 0
self.stack= [0]
def prep( self, connection ):
"""Override this to prepare for visiting."""
pass
def finish( self, connection ):
"""Override this to finalize after visiting."""
pass
def depth( self ):
return len(self.stack)
def start( self ):
return self.stack[-1]
def end( self ):
return self.count+self.step # leave a gap after my last child
def leaf( self, *args ):
self.count += self.step
def enter( self, *args ):
self.count += self.step # me
self.stack.append( self.count+self.step ) # my first child
return 1 # to descend
def exit( self, *args ):
self.count += self.step
self.stack.pop()
◊ TC Visitor base class definition (15). Used by transclose.py (10).
The TreeTable class defines several functions for working with a tree defined in a relational table.
Additionally, there are reporting methods, shown below, that show typical queries based on the transitive closure information built by the SetNumbers visitor.
TC TreeTable class to process a parent-child relationship table (16) =
class TreeTable( object ):
def __init__( self, name_=r"hierarchy.db" ):
self.dbName= name_
self.connection= sqlite.connect(self.dbName)
def hasChildren( self, fromDir="" ):
c= self.connection.cursor()
c.execute( """SELECT count(*) FROM pc WHERE dirName=?""", (fromDir,) )
r= c.fetchone()[0]
c.close()
#print fromDir, "hasChildren=", r
return r
def prep( self, visitor ):
visitor.prep( self.connection )
def finish( self, visitor ):
visitor.finish( self.connection )
def visit( self, visitor, fromDir="" ):
#print "visiting",fromDir
c= self.connection.cursor()
c.execute( """SELECT key, fileName, mtime, size
FROM pc
WHERE dirName=?
ORDER BY 2""", (fromDir,) )
#print fromDir,":"
for row in c.fetchall():
key, fileName, mtime, size = row
if self.hasChildren(fileName):
filter= visitor.enter( self.connection, fromDir, key, fileName )
if filter:
self.visit( visitor, fileName )
visitor.exit( self.connection, fromDir, key, fileName )
else:
visitor.leaf( self.connection, fromDir, key, fileName, mtime, size )
c.close()
→TC Queries using the transitive closure information (17)
◊ TC TreeTable class to process a parent-child relationship table (16). Used by transclose.py (10).
TC Queries using the transitive closure information (17) =
def targets( self, aName ):
sqlTarget= """
SELECT trans.low, trans.high
FROM pc, trans
WHERE pc.fileName=? AND pc.key=trans.key"""
tgt= self.connection.cursor()
tgt.execute( sqlTarget, (aName,) )
tgtList= list( tgt.fetchall() )
tgt.close()
#print 'targets:', aName, '=', tgtList
return tgtList
def parentsOf( self, aName ):
sqlParents= """
SELECT *
FROM pc, trans
WHERE ? BETWEEN trans.low AND trans.high
AND trans.low <> trans.high
AND pc.key = trans.key
ORDER BY trans.level ASC"""
for f,_ in self.targets(aName):
print aName, f, 'has path:'
par= self.connection.cursor()
par.execute( sqlParents, (f,) )
for p in par.fetchall():
print p
par.close()
def childrenOf( self, aName ):
sqlChildren= """
SELECT *
FROM pc, trans
WHERE trans.low BETWEEN ? AND ?
AND pc.key = trans.key
ORDER BY level ASC"""
for fl,fh in self.targets(aName):
print aName, 'has children:'
ch= self.connection.cursor()
ch.execute( sqlChildren, (fl,fh) )
for p in ch.fetchall():
print p
ch.close()
◊ TC Queries using the transitive closure information (17). Used by TC TreeTable class to process a parent-child relationship table (16) :: transclose.py (10).
The ShowDirectory class overrides functions in the Visitor base class to write an indented report of the tree structure.
TC ShowDirectory subclass of Visitor to report on a structure (18) =
class ShowDirectory( Visitor ):
def enter( self, connection, parent, key, child ):
Visitor.enter( self )
print self.depth()*' ', child, self.count
return 1
def exit( self, connection, parent, key, child ):
print self.depth()*' ', child, self.start(), self.end()
Visitor.exit( self )
def leaf( self, connection, parent, key, child, mtime, size ):
Visitor.leaf( self )
print self.depth()*' ', child, self.count, size, mtime
◊ TC ShowDirectory subclass of Visitor to report on a structure (18). Used by transclose.py (10).
The SetNumbers class overrides functions in the Visitor base class to assign subtree range numbers to the tree structure.
TC SetNumbers subclass of Visitor to assign numbers (19) =
class SetNumbers( Visitor ):
def exit( self, connection, parent, key, child ):
c= connection.cursor()
c.execute( """INSERT INTO trans(key,level,low,high)
values(?,?,?,?)""",
(key,self.depth(),self.start(),self.end()) )
c.close()
Visitor.exit( self )
def leaf( self, connection, parent, key, child, mtime, size ):
Visitor.leaf( self )
c= connection.cursor()
c.execute( """INSERT INTO trans(key,level,low,high)
values(?,?,?,?)""",
(key,self.depth(),self.count,self.count) )
c.close()
def prep( self, connection ):
c= connection.cursor()
c.execute( """delete from trans""" )
c.close()
connection.commit()
def finish( self, connection ):
connection.commit()
cursor= connection.cursor()
cursor.execute( """select * from trans""" )
for row in cursor.fetchall():
print row
cursor.close()
◊ TC SetNumbers subclass of Visitor to assign numbers (19). Used by transclose.py (10).
The main driver is broken into two functions. The main function assigns numbers to the tree. The report function uses the assigned subtree numbers to report on selected elements of the tree.
The main function creates a TreeTable instance, f. It also creates a SetNumbers visitor, s, which populates the trans table with the transitive closure of the tree in the tree table. The basic visit procedure is to call the TreeTable's prep, visit and finish for the given visitor.
The report function creates a TreeTable instance, f. It requests the parents of "__init__.py"; this file occurs several times, with separate different lists of parents. It requests the children of "test".
TC Main Driver (20) =
def main():
f= TreeTable("hier.db")
print "directory tree"
s= SetNumbers(5)
f.prep(s)
f.visit(s,"..")
f.finish(s)
def report():
f= TreeTable("hier.db")
print "reports"
f.parentsOf( "pyweb.css" )
f.childrenOf( "test" )
if __name__ == "__main__":
main()
report()
◊ TC Main Driver (20). Used by transclose.py (10).
Create Complete hier.db
(1, u'..', u'htmlparse', u'Mon Sep 12 15:05:40 2005', 0)
(2, u'..', u'pyWeb-1.4', u'Mon Sep 12 15:13:46 2005', 0)
...
(10, u'..', u'transclose', u'Fri Jan 05 10:55:10 2007', 0)
(11, u'htmlparse', u'CMD.EXE.lnk', u'Wed Nov 02 13:32:54 2005', 425)
(12, u'htmlparse', u'htmlparse.tws', u'Mon Jun 21 15:17:02 2004', 14761)
(13, u'htmlparse', u'htmlparsetree-1.3.zip', u'Thu Sep 11 20:17:14 2003', 54795)
...
(78, u'pyWeb-1.4', u'example.html', u'Thu Mar 03 17:57:22 2005', 788)
(79, u'pyWeb-1.4', u'example.w', u'Thu Mar 03 17:57:22 2005', 253)
(80, u'pyWeb-1.4', u'index.html', u'Thu Mar 03 17:57:22 2005', 247674)
...
(388, u'pyWeb-1.5', u'test', u'Fri Jan 05 10:50:38 2007', 0)
...
(394, u'test', u'example.w', u'Thu Mar 03 17:57:22 2005', 253)
(395, u'test', u'source.log', u'Thu Mar 03 17:57:42 2005', 160)
(396, u'test', u'test0.w', u'Thu Mar 03 17:57:42 2005', 300)
(397, u'test', u'test1.w', u'Thu Mar 03 17:57:42 2005', 158)
...
(433, u'transclose', u'hier.db', u'Fri Jan 05 17:05:20 2007', 3072)
(434, u'transclose', u'hierarchy.log', u'Fri Jan 05 17:05:20 2007', 0)
...
Load Complete hier.db
reports
pyweb.css 2260 has path:
(1, u'..', u'htmlparse', u'Mon Sep 12 15:05:40 2005', 0, 1, 2, 1215, 2390)
pyweb.css 2615 has path:
(2, u'..', u'pyWeb-1.4', u'Mon Sep 12 15:13:46 2005', 0, 2, 2, 2405, 2770)
pyweb.css 2570 has path:
(2, u'..', u'pyWeb-1.4', u'Mon Sep 12 15:13:46 2005', 0, 2, 2, 2405, 2770)
(92, u'pyWeb-1.4', u'pyweb Folder', u'Mon Sep 12 15:13:28 2005', 0, 92, 3, 2555, 2590)
pyweb.css 2890 has path:
(9, u'..', u'pyWeb-1.5', u'Fri Jan 05 10:50:36 2007', 0, 9, 2, 2780, 3110)
pyweb.css 5005 has path:
(10, u'..', u'transclose', u'Fri Jan 05 10:55:10 2007', 0, 10, 2, 3140, 5040)
test has children:
(388, u'pyWeb-1.5', u'test', u'Fri Jan 05 10:50:38 2007', 0, 388, 3, 2925, 3080)
(394, u'test', u'example.w', u'Thu Mar 03 17:57:22 2005', 253, 394, 3, 2935, 2935)
(395, u'test', u'source.log', u'Thu Mar 03 17:57:42 2005', 160, 395, 3, 2945, 2945)
(396, u'test', u'test0.w', u'Thu Mar 03 17:57:42 2005', 300, 396, 3, 2950, 2950)
(397, u'test', u'test1.w', u'Thu Mar 03 17:57:42 2005', 158, 397, 3, 2955, 2955)
(398, u'test', u'test2.w', u'Thu Mar 03 17:57:42 2005', 127, 398, 3, 2960, 2960)
(399, u'test', u'test3.w', u'Thu Mar 03 17:57:42 2005', 204, 399, 3, 2965, 2965)
(400, u'test', u'test4.w', u'Thu Mar 03 17:57:42 2005', 171, 400, 3, 2970, 2970)
(401, u'test', u'test5.w', u'Thu Mar 03 17:57:42 2005', 206, 401, 3, 2975, 2975)
(402, u'test', u'test6.tex', u'Thu Mar 03 17:57:42 2005', 1194, 402, 3, 2980, 2980)
(403, u'test', u'test6.tmp', u'Thu Mar 03 17:57:44 2005', 36, 403, 3, 2985, 2985)
(404, u'test', u'test6.w', u'Thu Mar 03 17:57:44 2005', 212, 404, 3, 2990, 2990)
(405, u'test', u'test7.tex', u'Thu Mar 03 17:57:44 2005', 149, 405, 3, 2995, 2995)
(406, u'test', u'test7.w', u'Thu Mar 03 17:57:44 2005', 163, 406, 3, 3000, 3000)
(407, u'test', u'test7a.tex', u'Thu Mar 03 17:57:44 2005', 22, 407, 3, 3005, 3005)
(408, u'test', u'test7a.w', u'Thu Mar 03 17:57:44 2005', 22, 408, 3, 3010, 3010)
(409, u'test', u'test8.tex', u'Thu Mar 03 17:57:44 2005', 111, 409, 3, 3015, 3015)
(410, u'test', u'test8.w', u'Thu Mar 03 17:57:44 2005', 163, 410, 3, 3020, 3020)
(411, u'test', u'test8a.w', u'Thu Mar 03 17:57:46 2005', 90, 411, 3, 3025, 3025)
(412, u'test', u'test9.tex', u'Thu Mar 03 17:57:46 2005', 276, 412, 3, 3030, 3030)
(413, u'test', u'test9.w', u'Thu Mar 03 17:57:46 2005', 184, 413, 3, 3035, 3035)
(565, u'test', u'__init__.py', u'Tue May 07 20:39:16 2002', 2164, 565, 3, 2925, 2925)
(566, u'test', u'__init__.pyc', u'Sat Mar 15 14:29:40 2003', 1147, 566, 3, 2930, 2930)
(567, u'test', u'gfstest.py', u'Sat May 11 07:18:50 2002', 6538, 567, 3, 2940, 2940)
(568, u'test', u'test_gadfly.py', u'Sat May 18 20:54:24 2002', 43432, 568, 3, 3040, 3040)
(569, u'test', u'test_gadfly.pyc', u'Sat Mar 15 14:29:40 2003', 49301, 569, 3, 3045, 3045)
(570, u'test', u'test_kjbuckets.py', u'Tue May 07 20:39:16 2002', 9790, 570, 3, 3060, 3060)
(571, u'test', u'test_kjbuckets.pyc', u'Sat Mar 15 14:29:40 2003', 10009, 571, 3, 3065, 3065)
(572, u'test', u'test_kjParseBuild.py', u'Fri May 10 21:59:04 2002', 8087, 572, 3, 3050, 3050)
(573, u'test', u'test_kjParseBuild.pyc', u'Sat Mar 15 14:29:40 2003', 7689, 573, 3, 3055, 3055)
(574, u'test', u'test_sqlgrammar.py', u'Tue May 07 20:39:16 2002', 1968, 574, 3, 3070, 3070)
(575, u'test', u'test_sqlgrammar.pyc', u'Sat Mar 15 14:29:42 2003', 1779, 575, 3, 3075, 3075)
test has children:
(394, u'test', u'example.w', u'Thu Mar 03 17:57:22 2005', 253, 394, 4, 4815, 4815)
(395, u'test', u'source.log', u'Thu Mar 03 17:57:42 2005', 160, 395, 4, 4825, 4825)
(396, u'test', u'test0.w', u'Thu Mar 03 17:57:42 2005', 300, 396, 4, 4830, 4830)
(397, u'test', u'test1.w', u'Thu Mar 03 17:57:42 2005', 158, 397, 4, 4835, 4835)
(398, u'test', u'test2.w', u'Thu Mar 03 17:57:42 2005', 127, 398, 4, 4840, 4840)
(399, u'test', u'test3.w', u'Thu Mar 03 17:57:42 2005', 204, 399, 4, 4845, 4845)
(400, u'test', u'test4.w', u'Thu Mar 03 17:57:42 2005', 171, 400, 4, 4850, 4850)
(401, u'test', u'test5.w', u'Thu Mar 03 17:57:42 2005', 206, 401, 4, 4855, 4855)
(402, u'test', u'test6.tex', u'Thu Mar 03 17:57:42 2005', 1194, 402, 4, 4860, 4860)
(403, u'test', u'test6.tmp', u'Thu Mar 03 17:57:44 2005', 36, 403, 4, 4865, 4865)
(404, u'test', u'test6.w', u'Thu Mar 03 17:57:44 2005', 212, 404, 4, 4870, 4870)
(405, u'test', u'test7.tex', u'Thu Mar 03 17:57:44 2005', 149, 405, 4, 4875, 4875)
(406, u'test', u'test7.w', u'Thu Mar 03 17:57:44 2005', 163, 406, 4, 4880, 4880)
(407, u'test', u'test7a.tex', u'Thu Mar 03 17:57:44 2005', 22, 407, 4, 4885, 4885)
(408, u'test', u'test7a.w', u'Thu Mar 03 17:57:44 2005', 22, 408, 4, 4890, 4890)
(409, u'test', u'test8.tex', u'Thu Mar 03 17:57:44 2005', 111, 409, 4, 4895, 4895)
(410, u'test', u'test8.w', u'Thu Mar 03 17:57:44 2005', 163, 410, 4, 4900, 4900)
(411, u'test', u'test8a.w', u'Thu Mar 03 17:57:46 2005', 90, 411, 4, 4905, 4905)
(412, u'test', u'test9.tex', u'Thu Mar 03 17:57:46 2005', 276, 412, 4, 4910, 4910)
(413, u'test', u'test9.w', u'Thu Mar 03 17:57:46 2005', 184, 413, 4, 4915, 4915)
(458, u'gadfly-1.0.0', u'test', u'Fri Jan 05 10:55:56 2007', 0, 458, 4, 4805, 4960)
(565, u'test', u'__init__.py', u'Tue May 07 20:39:16 2002', 2164, 565, 4, 4805, 4805)
(566, u'test', u'__init__.pyc', u'Sat Mar 15 14:29:40 2003', 1147, 566, 4, 4810, 4810)
(567, u'test', u'gfstest.py', u'Sat May 11 07:18:50 2002', 6538, 567, 4, 4820, 4820)
(568, u'test', u'test_gadfly.py', u'Sat May 18 20:54:24 2002', 43432, 568, 4, 4920, 4920)
(569, u'test', u'test_gadfly.pyc', u'Sat Mar 15 14:29:40 2003', 49301, 569, 4, 4925, 4925)
(570, u'test', u'test_kjbuckets.py', u'Tue May 07 20:39:16 2002', 9790, 570, 4, 4940, 4940)
(571, u'test', u'test_kjbuckets.pyc', u'Sat Mar 15 14:29:40 2003', 10009, 571, 4, 4945, 4945)
(572, u'test', u'test_kjParseBuild.py', u'Fri May 10 21:59:04 2002', 8087, 572, 4, 4930, 4930)
(573, u'test', u'test_kjParseBuild.pyc', u'Sat Mar 15 14:29:40 2003', 7689, 573, 4, 4935, 4935)
(574, u'test', u'test_sqlgrammar.py', u'Tue May 07 20:39:16 2002', 1968, 574, 4, 4950, 4950)
(575, u'test', u'test_sqlgrammar.pyc', u'Sat Mar 15 14:29:42 2003', 1779, 575, 4, 4955, 4955)
Created by ..\pyWeb-1.4\pyweb.py at Fri Jan 05 17:20:25 2007.
pyweb.__version__ '$Revision$'.
Source transclose.w modified Fri Jan 05 17:20:22 2007.
Working directory 'G:\Projects\transclose'.