Statistics
| Branch: | Tag: | Revision:

root / pithos / backends / lib / sqlite / node.py @ 2e662088

History | View | Annotate | Download (30.2 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
# 
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
# 
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
# 
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
# 
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
# 
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from time import time
35

    
36
from dbworker import DBWorker
37

    
38
from pithos.lib.filter import parse_filters
39

    
40

    
41
ROOTNODE  = 0
42

    
43
( SERIAL, NODE, HASH, SIZE, SOURCE, MTIME, MUSER, UUID, CLUSTER ) = range(9)
44

    
45
inf = float('inf')
46

    
47

    
48
def strnextling(prefix):
49
    """Return the first unicode string
50
       greater than but not starting with given prefix.
51
       strnextling('hello') -> 'hellp'
52
    """
53
    if not prefix:
54
        ## all strings start with the null string,
55
        ## therefore we have to approximate strnextling('')
56
        ## with the last unicode character supported by python
57
        ## 0x10ffff for wide (32-bit unicode) python builds
58
        ## 0x00ffff for narrow (16-bit unicode) python builds
59
        ## We will not autodetect. 0xffff is safe enough.
60
        return unichr(0xffff)
61
    s = prefix[:-1]
62
    c = ord(prefix[-1])
63
    if c >= 0xffff:
64
        raise RuntimeError
65
    s += unichr(c+1)
66
    return s
67

    
68
def strprevling(prefix):
69
    """Return an approximation of the last unicode string
70
       less than but not starting with given prefix.
71
       strprevling(u'hello') -> u'helln\\xffff'
72
    """
73
    if not prefix:
74
        ## There is no prevling for the null string
75
        return prefix
76
    s = prefix[:-1]
77
    c = ord(prefix[-1])
78
    if c > 0:
79
        s += unichr(c-1) + unichr(0xffff)
80
    return s
81

    
82

    
83
_propnames = {
84
    'serial'    : 0,
85
    'node'      : 1,
86
    'hash'      : 2,
87
    'size'      : 3,
88
    'source'    : 4,
89
    'mtime'     : 5,
90
    'muser'     : 6,
91
    'uuid'      : 7,
92
    'cluster'   : 8
93
}
94

    
95

    
96
class Node(DBWorker):
97
    """Nodes store path organization and have multiple versions.
98
       Versions store object history and have multiple attributes.
99
       Attributes store metadata.
100
    """
101
    
102
    # TODO: Provide an interface for included and excluded clusters.
103
    
104
    def __init__(self, **params):
105
        DBWorker.__init__(self, **params)
106
        execute = self.execute
107
        
108
        execute(""" pragma foreign_keys = on """)
109
        
110
        execute(""" create table if not exists nodes
111
                          ( node       integer primary key,
112
                            parent     integer default 0,
113
                            path       text    not null default '',
114
                            foreign key (parent)
115
                            references nodes(node)
116
                            on update cascade
117
                            on delete cascade ) """)
118
        execute(""" create unique index if not exists idx_nodes_path
119
                    on nodes(path) """)
120
        
121
        execute(""" create table if not exists policy
122
                          ( node   integer,
123
                            key    text,
124
                            value  text,
125
                            primary key (node, key)
126
                            foreign key (node)
127
                            references nodes(node)
128
                            on update cascade
129
                            on delete cascade ) """)
130
        
131
        execute(""" create table if not exists statistics
132
                          ( node       integer,
133
                            population integer not null default 0,
134
                            size       integer not null default 0,
135
                            mtime      integer,
136
                            cluster    integer not null default 0,
137
                            primary key (node, cluster)
138
                            foreign key (node)
139
                            references nodes(node)
140
                            on update cascade
141
                            on delete cascade ) """)
142
        
143
        execute(""" create table if not exists versions
144
                          ( serial     integer primary key,
145
                            node       integer,
146
                            hash       text,
147
                            size       integer not null default 0,
148
                            source     integer,
149
                            mtime      integer,
150
                            muser      text    not null default '',
151
                            uuid       text    not null default '',
152
                            cluster    integer not null default 0,
153
                            foreign key (node)
154
                            references nodes(node)
155
                            on update cascade
156
                            on delete cascade ) """)
157
        execute(""" create index if not exists idx_versions_node_mtime
158
                    on versions(node, mtime) """)
159
        execute(""" create index if not exists idx_versions_node_uuid
160
                    on versions(uuid) """)
161
        
162
        execute(""" create table if not exists attributes
163
                          ( serial integer,
164
                            domain text,
165
                            key    text,
166
                            value  text,
167
                            primary key (serial, domain, key)
168
                            foreign key (serial)
169
                            references versions(serial)
170
                            on update cascade
171
                            on delete cascade ) """)
172
        
173
        q = "insert or ignore into nodes(node, parent) values (?, ?)"
174
        execute(q, (ROOTNODE, ROOTNODE))
175
    
176
    def node_create(self, parent, path):
177
        """Create a new node from the given properties.
178
           Return the node identifier of the new node.
179
        """
180
        
181
        q = ("insert into nodes (parent, path) "
182
             "values (?, ?)")
183
        props = (parent, path)
184
        return self.execute(q, props).lastrowid
185
    
186
    def node_lookup(self, path):
187
        """Lookup the current node of the given path.
188
           Return None if the path is not found.
189
        """
190
        
191
        q = "select node from nodes where path = ?"
192
        self.execute(q, (path,))
193
        r = self.fetchone()
194
        if r is not None:
195
            return r[0]
196
        return None
197
    
198
    def node_get_properties(self, node):
199
        """Return the node's (parent, path).
200
           Return None if the node is not found.
201
        """
202
        
203
        q = "select parent, path from nodes where node = ?"
204
        self.execute(q, (node,))
205
        return self.fetchone()
206
    
207
    def node_get_versions(self, node, keys=(), propnames=_propnames):
208
        """Return the properties of all versions at node.
209
           If keys is empty, return all properties in the order
210
           (serial, node, hash, size, source, mtime, muser, uuid, cluster).
211
        """
212
        
213
        q = ("select serial, node, hash, size, source, mtime, muser, uuid, cluster "
214
             "from versions "
215
             "where node = ?")
216
        self.execute(q, (node,))
217
        r = self.fetchall()
218
        if r is None:
219
            return r
220
        
221
        if not keys:
222
            return r
223
        return [[p[propnames[k]] for k in keys if k in propnames] for p in r]
224
    
225
    def node_count_children(self, node):
226
        """Return node's child count."""
227
        
228
        q = "select count(node) from nodes where parent = ? and node != 0"
229
        self.execute(q, (node,))
230
        r = self.fetchone()
231
        if r is None:
232
            return 0
233
        return r[0]
234
    
235
    def node_purge_children(self, parent, before=inf, cluster=0):
236
        """Delete all versions with the specified
237
           parent and cluster, and return
238
           the hashes of versions deleted.
239
           Clears out nodes with no remaining versions.
240
        """
241
        
242
        execute = self.execute
243
        q = ("select count(serial), sum(size) from versions "
244
             "where node in (select node "
245
                            "from nodes "
246
                            "where parent = ?) "
247
             "and cluster = ? "
248
             "and mtime <= ?")
249
        args = (parent, cluster, before)
250
        execute(q, args)
251
        nr, size = self.fetchone()
252
        if not nr:
253
            return ()
254
        mtime = time()
255
        self.statistics_update(parent, -nr, -size, mtime, cluster)
256
        self.statistics_update_ancestors(parent, -nr, -size, mtime, cluster)
257
        
258
        q = ("select hash from versions "
259
             "where node in (select node "
260
                            "from nodes "
261
                            "where parent = ?) "
262
             "and cluster = ? "
263
             "and mtime <= ?")
264
        execute(q, args)
265
        hashes = [r[0] for r in self.fetchall()]
266
        q = ("delete from versions "
267
             "where node in (select node "
268
                            "from nodes "
269
                            "where parent = ?) "
270
             "and cluster = ? "
271
             "and mtime <= ?")
272
        execute(q, args)
273
        q = ("delete from nodes "
274
             "where node in (select node from nodes n "
275
                            "where (select count(serial) "
276
                                   "from versions "
277
                                   "where node = n.node) = 0 "
278
                            "and parent = ?)")
279
        execute(q, (parent,))
280
        return hashes
281
    
282
    def node_purge(self, node, before=inf, cluster=0):
283
        """Delete all versions with the specified
284
           node and cluster, and return
285
           the hashes of versions deleted.
286
           Clears out the node if it has no remaining versions.
287
        """
288
        
289
        execute = self.execute
290
        q = ("select count(serial), sum(size) from versions "
291
             "where node = ? "
292
             "and cluster = ? "
293
             "and mtime <= ?")
294
        args = (node, cluster, before)
295
        execute(q, args)
296
        nr, size = self.fetchone()
297
        if not nr:
298
            return ()
299
        mtime = time()
300
        self.statistics_update_ancestors(node, -nr, -size, mtime, cluster)
301
        
302
        q = ("select hash from versions "
303
             "where node = ? "
304
             "and cluster = ? "
305
             "and mtime <= ?")
306
        execute(q, args)
307
        hashes = [r[0] for r in self.fetchall()]
308
        q = ("delete from versions "
309
             "where node = ? "
310
             "and cluster = ? "
311
             "and mtime <= ?")
312
        execute(q, args)
313
        q = ("delete from nodes "
314
             "where node in (select node from nodes n "
315
                            "where (select count(serial) "
316
                                   "from versions "
317
                                   "where node = n.node) = 0 "
318
                            "and node = ?)")
319
        execute(q, (node,))
320
        return hashes
321
    
322
    def node_remove(self, node):
323
        """Remove the node specified.
324
           Return false if the node has children or is not found.
325
        """
326
        
327
        if self.node_count_children(node):
328
            return False
329
        
330
        mtime = time()
331
        q = ("select count(serial), sum(size), cluster "
332
             "from versions "
333
             "where node = ? "
334
             "group by cluster")
335
        self.execute(q, (node,))
336
        for population, size, cluster in self.fetchall():
337
            self.statistics_update_ancestors(node, -population, -size, mtime, cluster)
338
        
339
        q = "delete from nodes where node = ?"
340
        self.execute(q, (node,))
341
        return True
342
    
343
    def policy_get(self, node):
344
        q = "select key, value from policy where node = ?"
345
        self.execute(q, (node,))
346
        return dict(self.fetchall())
347
    
348
    def policy_set(self, node, policy):
349
        q = "insert or replace into policy (node, key, value) values (?, ?, ?)"
350
        self.executemany(q, ((node, k, v) for k, v in policy.iteritems()))
351
    
352
    def statistics_get(self, node, cluster=0):
353
        """Return population, total size and last mtime
354
           for all versions under node that belong to the cluster.
355
        """
356
        
357
        q = ("select population, size, mtime from statistics "
358
             "where node = ? and cluster = ?")
359
        self.execute(q, (node, cluster))
360
        return self.fetchone()
361
    
362
    def statistics_update(self, node, population, size, mtime, cluster=0):
363
        """Update the statistics of the given node.
364
           Statistics keep track the population, total
365
           size of objects and mtime in the node's namespace.
366
           May be zero or positive or negative numbers.
367
        """
368
        
369
        qs = ("select population, size from statistics "
370
              "where node = ? and cluster = ?")
371
        qu = ("insert or replace into statistics (node, population, size, mtime, cluster) "
372
              "values (?, ?, ?, ?, ?)")
373
        self.execute(qs, (node, cluster))
374
        r = self.fetchone()
375
        if r is None:
376
            prepopulation, presize = (0, 0)
377
        else:
378
            prepopulation, presize = r
379
        population += prepopulation
380
        size += presize
381
        self.execute(qu, (node, population, size, mtime, cluster))
382
    
383
    def statistics_update_ancestors(self, node, population, size, mtime, cluster=0):
384
        """Update the statistics of the given node's parent.
385
           Then recursively update all parents up to the root.
386
           Population is not recursive.
387
        """
388
        
389
        while True:
390
            if node == 0:
391
                break
392
            props = self.node_get_properties(node)
393
            if props is None:
394
                break
395
            parent, path = props
396
            self.statistics_update(parent, population, size, mtime, cluster)
397
            node = parent
398
            population = 0 # Population isn't recursive
399
    
400
    def statistics_latest(self, node, before=inf, except_cluster=0):
401
        """Return population, total size and last mtime
402
           for all latest versions under node that
403
           do not belong to the cluster.
404
        """
405
        
406
        execute = self.execute
407
        fetchone = self.fetchone
408
        
409
        # The node.
410
        props = self.node_get_properties(node)
411
        if props is None:
412
            return None
413
        parent, path = props
414
        
415
        # The latest version.
416
        q = ("select serial, node, hash, size, source, mtime, muser, uuid, cluster "
417
             "from versions "
418
             "where serial = (select max(serial) "
419
                             "from versions "
420
                             "where node = ? and mtime < ?) "
421
             "and cluster != ?")
422
        execute(q, (node, before, except_cluster))
423
        props = fetchone()
424
        if props is None:
425
            return None
426
        mtime = props[MTIME]
427
        
428
        # First level, just under node (get population).
429
        q = ("select count(serial), sum(size), max(mtime) "
430
             "from versions v "
431
             "where serial = (select max(serial) "
432
                             "from versions "
433
                             "where node = v.node and mtime < ?) "
434
             "and cluster != ? "
435
             "and node in (select node "
436
                          "from nodes "
437
                          "where parent = ?)")
438
        execute(q, (before, except_cluster, node))
439
        r = fetchone()
440
        if r is None:
441
            return None
442
        count = r[0]
443
        mtime = max(mtime, r[2])
444
        if count == 0:
445
            return (0, 0, mtime)
446
        
447
        # All children (get size and mtime).
448
        # XXX: This is why the full path is stored.
449
        q = ("select count(serial), sum(size), max(mtime) "
450
             "from versions v "
451
             "where serial = (select max(serial) "
452
                             "from versions "
453
                             "where node = v.node and mtime < ?) "
454
             "and cluster != ? "
455
             "and node in (select node "
456
                          "from nodes "
457
                          "where path like ? escape '\\')")
458
        execute(q, (before, except_cluster, self.escape_like(path) + '%'))
459
        r = fetchone()
460
        if r is None:
461
            return None
462
        size = r[1] - props[SIZE]
463
        mtime = max(mtime, r[2])
464
        return (count, size, mtime)
465
    
466
    def version_create(self, node, hash, size, source, muser, uuid, cluster=0):
467
        """Create a new version from the given properties.
468
           Return the (serial, mtime) of the new version.
469
        """
470
        
471
        q = ("insert into versions (node, hash, size, source, mtime, muser, uuid, cluster) "
472
             "values (?, ?, ?, ?, ?, ?, ?, ?)")
473
        mtime = time()
474
        props = (node, hash, size, source, mtime, muser, uuid, cluster)
475
        serial = self.execute(q, props).lastrowid
476
        self.statistics_update_ancestors(node, 1, size, mtime, cluster)
477
        return serial, mtime
478
    
479
    def version_lookup(self, node, before=inf, cluster=0):
480
        """Lookup the current version of the given node.
481
           Return a list with its properties:
482
           (serial, node, hash, size, source, mtime, muser, uuid, cluster)
483
           or None if the current version is not found in the given cluster.
484
        """
485
        
486
        q = ("select serial, node, hash, size, source, mtime, muser, uuid, cluster "
487
             "from versions "
488
             "where serial = (select max(serial) "
489
                             "from versions "
490
                             "where node = ? and mtime < ?) "
491
             "and cluster = ?")
492
        self.execute(q, (node, before, cluster))
493
        props = self.fetchone()
494
        if props is not None:
495
            return props
496
        return None
497
    
498
    def version_get_properties(self, serial, keys=(), propnames=_propnames):
499
        """Return a sequence of values for the properties of
500
           the version specified by serial and the keys, in the order given.
501
           If keys is empty, return all properties in the order
502
           (serial, node, hash, size, source, mtime, muser, uuid, cluster).
503
        """
504
        
505
        q = ("select serial, node, hash, size, source, mtime, muser, uuid, cluster "
506
             "from versions "
507
             "where serial = ?")
508
        self.execute(q, (serial,))
509
        r = self.fetchone()
510
        if r is None:
511
            return r
512
        
513
        if not keys:
514
            return r
515
        return [r[propnames[k]] for k in keys if k in propnames]
516
    
517
    def version_recluster(self, serial, cluster):
518
        """Move the version into another cluster."""
519
        
520
        props = self.version_get_properties(serial)
521
        if not props:
522
            return
523
        node = props[NODE]
524
        size = props[SIZE]
525
        oldcluster = props[CLUSTER]
526
        if cluster == oldcluster:
527
            return
528
        
529
        mtime = time()
530
        self.statistics_update_ancestors(node, -1, -size, mtime, oldcluster)
531
        self.statistics_update_ancestors(node, 1, size, mtime, cluster)
532
        
533
        q = "update versions set cluster = ? where serial = ?"
534
        self.execute(q, (cluster, serial))
535
    
536
    def version_remove(self, serial):
537
        """Remove the serial specified."""
538
        
539
        props = self.version_get_properties(serial)
540
        if not props:
541
            return
542
        node = props[NODE]
543
        hash = props[HASH]
544
        size = props[SIZE]
545
        cluster = props[CLUSTER]
546
        
547
        mtime = time()
548
        self.statistics_update_ancestors(node, -1, -size, mtime, cluster)
549
        
550
        q = "delete from versions where serial = ?"
551
        self.execute(q, (serial,))
552
        return hash
553
    
554
    def attribute_get(self, serial, domain, keys=()):
555
        """Return a list of (key, value) pairs of the version specified by serial.
556
           If keys is empty, return all attributes.
557
           Othwerise, return only those specified.
558
        """
559
        
560
        execute = self.execute
561
        if keys:
562
            marks = ','.join('?' for k in keys)
563
            q = ("select key, value from attributes "
564
                 "where key in (%s) and serial = ? and domain = ?" % (marks,))
565
            execute(q, keys + (serial, domain))
566
        else:
567
            q = "select key, value from attributes where serial = ? and domain = ?"
568
            execute(q, (serial, domain))
569
        return self.fetchall()
570
    
571
    def attribute_set(self, serial, domain, items):
572
        """Set the attributes of the version specified by serial.
573
           Receive attributes as an iterable of (key, value) pairs.
574
        """
575
        
576
        q = ("insert or replace into attributes (serial, domain, key, value) "
577
             "values (?, ?, ?, ?)")
578
        self.executemany(q, ((serial, domain, k, v) for k, v in items))
579
    
580
    def attribute_del(self, serial, domain, keys=()):
581
        """Delete attributes of the version specified by serial.
582
           If keys is empty, delete all attributes.
583
           Otherwise delete those specified.
584
        """
585
        
586
        if keys:
587
            q = "delete from attributes where serial = ? and domain = ? and key = ?"
588
            self.executemany(q, ((serial, domain, key) for key in keys))
589
        else:
590
            q = "delete from attributes where serial = ? and domain = ?"
591
            self.execute(q, (serial, domain))
592
    
593
    def attribute_copy(self, source, dest):
594
        q = ("insert or replace into attributes "
595
             "select ?, domain, key, value from attributes "
596
             "where serial = ?")
597
        self.execute(q, (dest, source))
598
    
599
    def _construct_filters(self, domain, filterq):
600
        if not domain or not filterq:
601
            return None, None
602
        
603
        subqlist = []
604
        append = subqlist.append
605
        included, excluded, opers = parse_filters(filterq)
606
        args = []
607
        
608
        if included:
609
            subq = "exists (select 1 from attributes where serial = v.serial and domain = ? and "
610
            subq += "(" + ' or '.join(('key = ?' for x in included)) + ")"
611
            subq += ")"
612
            args += [domain]
613
            args += included
614
            append(subq)
615
        
616
        if excluded:
617
            subq = "not exists (select 1 from attributes where serial = v.serial and domain = ? and "
618
            subq += "(" + ' or '.join(('key = ?' for x in excluded)) + ")"
619
            subq += ")"
620
            args += [domain]
621
            args += excluded
622
            append(subq)
623
        
624
        if opers:
625
            for k, o, v in opers:
626
                subq = "exists (select 1 from attributes where serial = v.serial and domain = ? and "
627
                subq += "key = ? and value %s ?" % (o,)
628
                subq += ")"
629
                args += [domain, k, v]
630
                append(subq)
631
        
632
        if not subqlist:
633
            return None, None
634
        
635
        subq = ' and ' + ' and '.join(subqlist)
636
        
637
        return subq, args
638
    
639
    def _construct_paths(self, pathq):
640
        if not pathq:
641
            return None, None
642
        
643
        subq = " and ("
644
        subq += ' or '.join(("n.path like ? escape '\\'" for x in pathq))
645
        subq += ")"
646
        args = tuple([self.escape_like(x) + '%' for x in pathq])
647
        
648
        return subq, args
649
    
650
    def _construct_size(self, sizeq):
651
        if not sizeq or len(sizeq) != 2:
652
            return None, None
653
        
654
        subq = ''
655
        args = []
656
        if sizeq[0]:
657
            subq += " and v.size >= ?"
658
            args += [sizeq[0]]
659
        if sizeq[1]:
660
            subq += " and v.size < ?"
661
            args += [sizeq[1]]
662
        
663
        return subq, args
664
    
665
    def latest_attribute_keys(self, parent, domain, before=inf, except_cluster=0, pathq=[]):
666
        """Return a list with all keys pairs defined
667
           for all latest versions under parent that
668
           do not belong to the cluster.
669
        """
670
        
671
        # TODO: Use another table to store before=inf results.
672
        q = ("select distinct a.key "
673
             "from attributes a, versions v, nodes n "
674
             "where v.serial = (select max(serial) "
675
                               "from versions "
676
                               "where node = v.node and mtime < ?) "
677
             "and v.cluster != ? "
678
             "and v.node in (select node "
679
                            "from nodes "
680
                            "where parent = ?) "
681
             "and a.serial = v.serial "
682
             "and a.domain = ? "
683
             "and n.node = v.node")
684
        args = (before, except_cluster, parent, domain)
685
        subq, subargs = self._construct_paths(pathq)
686
        if subq is not None:
687
            q += subq
688
            args += subargs
689
        self.execute(q, args)
690
        return [r[0] for r in self.fetchall()]
691
    
692
    def latest_version_list(self, parent, prefix='', delimiter=None,
693
                            start='', limit=10000, before=inf,
694
                            except_cluster=0, pathq=[], domain=None, filterq=[], sizeq=None):
695
        """Return a (list of (path, serial) tuples, list of common prefixes)
696
           for the current versions of the paths with the given parent,
697
           matching the following criteria.
698
           
699
           The property tuple for a version is returned if all
700
           of these conditions are true:
701
                
702
                a. parent matches
703
                
704
                b. path > start
705
                
706
                c. path starts with prefix (and paths in pathq)
707
                
708
                d. version is the max up to before
709
                
710
                e. version is not in cluster
711
                
712
                f. the path does not have the delimiter occuring
713
                   after the prefix, or ends with the delimiter
714
                
715
                g. serial matches the attribute filter query.
716
                   
717
                   A filter query is a comma-separated list of
718
                   terms in one of these three forms:
719
                   
720
                   key
721
                       an attribute with this key must exist
722
                   
723
                   !key
724
                       an attribute with this key must not exist
725
                   
726
                   key ?op value
727
                       the attribute with this key satisfies the value
728
                       where ?op is one of =, != <=, >=, <, >.
729
                
730
                h. the size is in the range set by sizeq
731
           
732
           The list of common prefixes includes the prefixes
733
           matching up to the first delimiter after prefix,
734
           and are reported only once, as "virtual directories".
735
           The delimiter is included in the prefixes.
736
           
737
           If arguments are None, then the corresponding matching rule
738
           will always match.
739
           
740
           Limit applies to the first list of tuples returned.
741
        """
742
        
743
        execute = self.execute
744
        
745
        if not start or start < prefix:
746
            start = strprevling(prefix)
747
        nextling = strnextling(prefix)
748
        
749
        q = ("select distinct n.path, v.serial "
750
             "from versions v, nodes n "
751
             "where v.serial = (select max(serial) "
752
                               "from versions "
753
                               "where node = v.node and mtime < ?) "
754
             "and v.cluster != ? "
755
             "and v.node in (select node "
756
                            "from nodes "
757
                            "where parent = ?) "
758
             "and n.node = v.node "
759
             "and n.path > ? and n.path < ?")
760
        args = [before, except_cluster, parent, start, nextling]
761
        
762
        subq, subargs = self._construct_paths(pathq)
763
        if subq is not None:
764
            q += subq
765
            args += subargs
766
        subq, subargs = self._construct_size(sizeq)
767
        if subq is not None:
768
            q += subq
769
            args += subargs
770
        subq, subargs = self._construct_filters(domain, filterq)
771
        if subq is not None:
772
            q += subq
773
            args += subargs
774
        else:
775
            q = q.replace("attributes a, ", "")
776
            q = q.replace("and a.serial = v.serial ", "")
777
        q += " order by n.path"
778
        
779
        if not delimiter:
780
            q += " limit ?"
781
            args.append(limit)
782
            execute(q, args)
783
            return self.fetchall(), ()
784
        
785
        pfz = len(prefix)
786
        dz = len(delimiter)
787
        count = 0
788
        fetchone = self.fetchone
789
        prefixes = []
790
        pappend = prefixes.append
791
        matches = []
792
        mappend = matches.append
793
        
794
        execute(q, args)
795
        while True:
796
            props = fetchone()
797
            if props is None:
798
                break
799
            path, serial = props
800
            idx = path.find(delimiter, pfz)
801
            
802
            if idx < 0:
803
                mappend(props)
804
                count += 1
805
                if count >= limit:
806
                    break
807
                continue
808
            
809
            if idx + dz == len(path):
810
                mappend(props)
811
                count += 1
812
                continue # Get one more, in case there is a path.
813
            pf = path[:idx + dz]
814
            pappend(pf)
815
            if count >= limit: 
816
                break
817
            
818
            args[3] = strnextling(pf) # New start.
819
            execute(q, args)
820
        
821
        return matches, prefixes
822
    
823
    def latest_uuid(self, uuid):
824
        """Return a (path, serial) tuple, for the latest version of the given uuid."""
825
        
826
        q = ("select n.path, v.serial "
827
             "from versions v, nodes n "
828
             "where v.serial = (select max(serial) "
829
                               "from versions "
830
                               "where uuid = ?) "
831
             "and n.node = v.node")
832
        self.execute(q, (uuid,))
833
        return self.fetchone()