New argument to DBI search_stories().
[gruta.git] / Gruta / Source / DBI.pm
blob87b3c88f9de09ab75739d5d53b36c7bfc0288386
1 package Gruta::Source::DBI;
3 use strict;
4 use warnings;
5 use Carp;
7 use DBI;
8 use Gruta::Data;
10 sub _prepare {
11 my $self = shift;
12 my $sql = shift;
14 my $sth = $self->{dbh}->prepare($sql) or
15 croak $self->{dbh}->errstr;
17 return $sth;
20 sub _execute {
21 my $self = shift;
22 my $sth = shift;
24 return $sth->execute( @_ ) or croak $self->{dbh}->errstr;
28 package Gruta::Data::DBI::BASE;
30 sub pk { return qw(id); }
32 sub load {
33 my $self = shift;
34 my $driver = shift;
36 $self->source( $driver );
38 my $sth;
40 if (not $sth = $self->source->{sth}->{select}->{ref($self)}) {
41 my $sql = 'SELECT ' . join(', ', $self->fields()) .
42 ' FROM ' . $self->table() .
43 ' WHERE ' . join(' AND ', map { "$_ = ?" } $self->pk());
45 $sth = $self->source->{sth}->{select}->{ref($self)} =
46 $self->source->_prepare($sql);
49 $self->source->_execute($sth, map { $self->get($_) } $self->pk());
51 my $r = $sth->fetchrow_hashref();
53 if (not $r) {
54 return undef;
57 foreach my $k ($self->fields()) {
58 $self->set($k, $r->{$k});
61 return $self;
65 sub save {
66 my $self = shift;
67 my $driver = shift;
69 $self->source( $driver ) if $driver;
71 my $sth;
73 if (not $sth = $self->source->{sth}->{update}->{ref($self)}) {
74 my $sql = 'UPDATE ' . $self->table() .
75 ' SET ' . join(', ', map { "$_ = ?" } $self->fields()) .
76 ' WHERE ' . join(' AND ', map { "$_ = ?" } $self->pk());
78 $sth = $self->source->{sth}->{update}->{ref($self)} =
79 $self->source->_prepare($sql);
82 $self->source->_execute($sth,
83 (map { $self->get($_) } $self->fields()),
84 (map { $self->get($_) } $self->pk())
87 return $self;
91 sub delete {
92 my $self = shift;
93 my $driver = shift;
95 $self->source( $driver ) if $driver;
97 my $sth;
99 if (not $sth = $self->source->{sth}->{delete}->{ref($self)}) {
100 my $sql = 'DELETE FROM ' . $self->table() .
101 ' WHERE ' . join(' AND ', map { "$_ = ?" } $self->pk());
103 $sth = $self->source->{sth}->{delete}->{ref($self)} =
104 $self->source->_prepare($sql);
107 $self->source->_execute($sth,
108 (map { $self->get($_) } $self->pk())
111 return $self;
115 package Gruta::Data::DBI::Story;
117 use base 'Gruta::Data::Story';
118 use base 'Gruta::Data::DBI::BASE';
120 sub table { return 'stories'; }
121 sub pk { return qw(id topic_id); }
123 sub touch {
124 my $self = shift;
126 my $sth = $self->source->_prepare(
127 'UPDATE stories SET hits = hits + 1 WHERE topic_id = ? AND id = ?');
128 $self->source->_execute($sth, $self->get('topic_id'), $self->get('id'));
130 return $self;
133 sub tags {
134 my $self = shift;
135 my @ret = ();
137 if (scalar(@_)) {
138 my @tags = @_;
140 # first, delete all tags for this story
141 my $sth = $self->source->_prepare(
142 'DELETE FROM tags WHERE topic_id = ? AND id = ?');
143 $self->source->_execute($sth, $self->get('topic_id'), $self->get('id'));
145 # second, add all the new ones
146 $sth = $self->source->_prepare(
147 'INSERT INTO tags (id, topic_id, tag) VALUES (?, ?, ?)');
149 foreach my $t (@tags) {
150 $self->source->_execute($sth,
151 $self->get('id'),
152 $self->get('topic_id'),
153 lc($t) );
156 else {
157 # read from database
158 my $sth = $self->source->_prepare(
159 'SELECT tag FROM tags WHERE topic_id = ? AND id = ?');
160 $self->source->_execute($sth, $self->get('topic_id'), $self->get('id'));
162 while (my $r = $sth->fetchrow_arrayref()) {
163 push(@ret, $r->[0]);
167 return @ret;
170 package Gruta::Data::DBI::Topic;
172 use base 'Gruta::Data::Topic';
173 use base 'Gruta::Data::DBI::BASE';
175 sub table { return 'topics'; }
177 package Gruta::Data::DBI::User;
179 use base 'Gruta::Data::User';
180 use base 'Gruta::Data::DBI::BASE';
182 sub table { return 'users'; }
184 package Gruta::Data::DBI::Session;
186 use base 'Gruta::Data::Session';
187 use base 'Gruta::Data::DBI::BASE';
189 sub table { return 'sids'; }
191 package Gruta::Source::DBI;
193 sub _assert { return $_[0]; }
195 sub _all {
196 my $self = shift;
197 my $table = shift;
199 my @ret = ();
201 my $sth = $self->_prepare("SELECT id FROM $table");
202 $self->_execute($sth);
204 while(my @r = $sth->fetchrow_array()) {
205 push(@ret, $r[0]);
208 return @ret;
211 sub _one {
212 my $self = shift;
213 my $id = shift;
214 my $class = shift;
216 my $o = ${class}->new( id => $id );
217 return $o->load( $self );
221 sub topic { return _one( @_, 'Gruta::Data::DBI::Topic' ); }
222 sub topics { return $_[0]->_all('topics'); }
224 sub user { return _one( @_, 'Gruta::Data::DBI::User' ); }
225 sub users { return $_[0]->_all('users'); }
227 sub story {
228 my $self = shift;
229 my $topic_id = shift;
230 my $id = shift;
232 my $s = Gruta::Data::DBI::Story->new( topic_id => $topic_id, id => $id );
233 return $s->load( $self );
237 sub stories {
238 my $self = shift;
239 my $topic_id = shift;
241 my @ret = ();
243 my $sth = $self->_prepare("SELECT id FROM stories WHERE topic_id = ?");
244 $self->_execute($sth, $topic_id);
246 while(my @r = $sth->fetchrow_array()) {
247 push(@ret, $r[0]);
250 return @ret;
254 sub stories_by_date {
255 my $self = shift;
256 my $topic_id = shift;
257 my %args = @_;
259 $args{offset} += 0;
260 $args{offset} = 0 if $args{offset} < 0;
262 my @args = ( $topic_id );
263 my $sql = 'SELECT id FROM stories WHERE topic_id = ?';
265 if ($args{from}) {
266 $sql .= ' AND date > ?';
267 push(@args, $args{from});
270 if ($args{to}) {
271 $sql .= ' AND date < ?';
272 push(@args, $args{to});
275 if (!$args{future}) {
276 $sql .= ' AND date <= ?';
277 push(@args, Gruta::Data::today());
280 $sql .= ' ORDER BY date DESC';
282 if ($args{num} || $args{offset}) {
284 $sql .= ' LIMIT ?';
285 push(@args, $args{num} || -1);
287 if ($args{offset}) {
288 $sql .= ' OFFSET ?';
289 push(@args, $args{offset});
293 my $sth = $self->_prepare($sql);
294 $self->_execute($sth, @args);
296 my @r = ();
298 while(my $r = $sth->fetchrow_arrayref()) {
299 push(@r, $r->[0]);
302 return @r;
306 sub search_stories {
307 my $self = shift;
308 my $topic_id = shift;
309 my $query = shift;
310 my $future = shift;
312 my @q = map { '%' . $_ . '%' } split(/\s+/, $query);
313 my $cond = 'AND content LIKE ? ' x scalar(@q);
315 unless ($future) {
316 $cond .= 'AND date <= ';
317 push(@q, Gruta::Data::today());
320 my $sth = $self->_prepare(
321 'SELECT id FROM stories WHERE topic_id = ? ' . $cond .
322 'ORDER BY date DESC');
324 $self->_execute($sth, $topic_id, @q);
326 my @r = ();
328 while(my $r = $sth->fetchrow_arrayref()) {
329 push(@r, $r->[0]);
332 return @r;
336 sub stories_top_ten {
337 my $self = shift;
338 my $num = shift;
340 my $sql = 'SELECT hits, topic_id, id FROM stories ' .
341 'ORDER BY hits DESC LIMIT ?';
343 my $sth = $self->_prepare($sql);
344 $self->_execute($sth, $num);
346 my @r = ();
348 while (my @a = $sth->fetchrow_array()) {
349 push(@r, [ @a ]);
352 return @r;
356 sub search_stories_by_tag {
357 my $self = shift;
358 my @tags = @_;
360 my @r = ();
362 if (@tags) {
363 my $sql = 'SELECT DISTINCT topic_id, id FROM tags WHERE ' .
364 join(' OR ', map { 'tag = ?' } @tags);
366 my $sth = $self->_prepare($sql);
367 $self->_execute($sth, map { lc($_) } @tags);
369 while (my @a = $sth->fetchrow_array()) {
370 push(@r, [ @a ]);
374 return @r;
378 sub tags {
379 my $self = shift;
381 my @r = ();
383 my $sth = $self->_prepare(
384 'SELECT tag, count(tag) FROM tags GROUP BY tag ORDER BY 2 DESC, 1');
385 $self->_execute($sth);
387 while (my @a = $sth->fetchrow_array()) {
388 push(@r, [ @a ]);
391 return @r;
394 sub session { return _one( @_, 'Gruta::Data::DBI::Session' ); }
396 sub purge_old_sessions {
397 my $self = shift;
399 my $sth = $self->_prepare('DELETE FROM sids WHERE time < ?');
400 $self->_execute($sth, time() - (60 * 60 * 24));
402 return undef;
406 sub _insert {
407 my $self = shift;
408 my $obj = shift;
409 my $table = shift;
410 my $class = shift;
412 my $sth;
414 bless($obj, $class);
415 $obj->source($self);
417 if (not $sth = $self->{sth}->{insert}->{ref($obj)}) {
418 my $sql = 'INSERT INTO ' . $table .
419 ' (' . join(', ', $obj->fields()) . ')' .
420 ' VALUES (' . join(', ', map { '?' } $obj->fields()) . ')';
422 $sth = $self->{sth}->{insert}->{ref($obj)} = $self->_prepare($sql);
425 $self->_execute($sth, map { $obj->get($_) } $obj->fields());
427 return $obj;
430 sub insert_topic { $_[0]->_insert($_[1], 'topics', 'Gruta::Data::DBI::Topic'); }
431 sub insert_user { $_[0]->_insert($_[1], 'users', 'Gruta::Data::DBI::User'); }
434 sub insert_story {
435 my $self = shift;
436 my $story = shift;
438 if (not $story->get('id')) {
439 # alloc an id for the story
440 my $id = undef;
442 my $sth = $self->_prepare(
443 'SELECT 1 FROM stories WHERE topic_id = ? AND id = ?');
445 do {
446 $id = $story->new_id();
447 $self->_execute($sth, $story->get('topic_id'), $id);
449 } while $sth->fetchrow_arrayref();
451 $story->set('id', $id);
454 $self->_insert($story, 'stories', 'Gruta::Data::DBI::Story');
456 return $story;
459 sub insert_session { $_[0]->_insert($_[1], 'sids', 'Gruta::Data::DBI::Session'); }
461 sub create {
462 my $self = shift;
464 my $sql = '';
466 while(<DATA>) {
467 chomp;
469 if (/^;$/) {
470 $self->{dbh}->do($sql);
471 $sql = '';
473 else {
474 $sql .= $_;
480 sub new {
481 my $class = shift;
483 my $s = bless( { @_ }, $class);
485 $s->{dbh} = DBI->connect($s->{string},
486 $s->{user}, $s->{passwd}, { RaiseError => 1 });
488 return $s;
492 __DATA__
493 CREATE TABLE topics (
494 id VARCHAR PRIMARY KEY,
495 name VARCHAR,
496 editors VARCHAR,
497 max_stories INTEGER,
498 internal INTEGER
501 CREATE TABLE stories (
502 id VARCHAR NOT NULL,
503 topic_id VARCHAR NOT NULL,
504 title VARCHAR,
505 date CHAR(14),
506 userid VARCHAR,
507 format VARCHAR,
508 hits INTEGER DEFAULT 0,
509 ctime INTEGER,
510 content VARCHAR,
511 PRIMARY KEY (id, topic_id)
514 CREATE TABLE users (
515 id VARCHAR PRIMARY KEY,
516 username VARCHAR,
517 email VARCHAR,
518 password VARCHAR,
519 can_upload INTEGER,
520 is_admin INTEGER,
521 xdate CHAR(14)
524 CREATE TABLE sids (
525 id VARCHAR PRIMARY KEY,
526 time INTEGER,
527 user_id VARCHAR,
528 ip VARCHAR
531 CREATE TABLE tags (
532 id VARCHAR NOT NULL,
533 topic_id VARCHAR NOT NULL,
534 tag VARCHAR NOT NULL
537 CREATE INDEX stories_by_date ON stories (date)
539 CREATE INDEX stories_by_hits ON stories (hits)
541 CREATE INDEX tags_by_tag ON tags (tag)