DBI::create() can always be executed.
[gruta.git] / Gruta / Source / DBI.pm
blobd6e576b8d286bff5fcc73dedee30a99593ef1232
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 $t =~ s/^\s+//;
151 $t =~ s/\s+$//;
153 $self->source->_execute($sth,
154 $self->get('id'),
155 $self->get('topic_id'),
156 lc($t) );
159 else {
160 # read from database
161 my $sth = $self->source->_prepare(
162 'SELECT tag FROM tags WHERE topic_id = ? AND id = ?');
163 $self->source->_execute($sth, $self->get('topic_id'), $self->get('id'));
165 while (my $r = $sth->fetchrow_arrayref()) {
166 push(@ret, $r->[0]);
170 return @ret;
173 package Gruta::Data::DBI::Topic;
175 use base 'Gruta::Data::Topic';
176 use base 'Gruta::Data::DBI::BASE';
178 sub table { return 'topics'; }
180 package Gruta::Data::DBI::User;
182 use base 'Gruta::Data::User';
183 use base 'Gruta::Data::DBI::BASE';
185 sub table { return 'users'; }
187 package Gruta::Data::DBI::Session;
189 use base 'Gruta::Data::Session';
190 use base 'Gruta::Data::DBI::BASE';
192 sub table { return 'sids'; }
194 package Gruta::Source::DBI;
196 sub _assert { return $_[0]; }
198 sub _all {
199 my $self = shift;
200 my $table = shift;
202 my @ret = ();
204 my $sth = $self->_prepare("SELECT id FROM $table");
205 $self->_execute($sth);
207 while(my @r = $sth->fetchrow_array()) {
208 push(@ret, $r[0]);
211 return @ret;
214 sub _one {
215 my $self = shift;
216 my $id = shift;
217 my $class = shift;
219 my $o = ${class}->new( id => $id );
220 return $o->load( $self );
224 sub topic { return _one( @_, 'Gruta::Data::DBI::Topic' ); }
225 sub topics { return $_[0]->_all('topics'); }
227 sub user { return _one( @_, 'Gruta::Data::DBI::User' ); }
228 sub users { return $_[0]->_all('users'); }
230 sub story {
231 my $self = shift;
232 my $topic_id = shift;
233 my $id = shift;
235 my $s = Gruta::Data::DBI::Story->new( topic_id => $topic_id, id => $id );
236 return $s->load( $self );
240 sub stories {
241 my $self = shift;
242 my $topic_id = shift;
244 my @ret = ();
246 my $sth = $self->_prepare("SELECT id FROM stories WHERE topic_id = ?");
247 $self->_execute($sth, $topic_id);
249 while(my @r = $sth->fetchrow_array()) {
250 push(@ret, $r[0]);
253 return @ret;
257 sub stories_by_date {
258 my $self = shift;
259 my $topic_id = shift;
260 my %args = @_;
262 $args{offset} += 0;
263 $args{offset} = 0 if $args{offset} < 0;
265 my @args = ( $topic_id );
266 my $sql = 'SELECT id FROM stories WHERE topic_id = ?';
268 if ($args{from}) {
269 $sql .= ' AND date > ?';
270 push(@args, $args{from});
273 if ($args{to}) {
274 $sql .= ' AND date < ?';
275 push(@args, $args{to});
278 if (!$args{future}) {
279 $sql .= ' AND date <= ?';
280 push(@args, Gruta::Data::today());
283 $sql .= ' ORDER BY date DESC';
285 if ($args{num} || $args{offset}) {
287 $sql .= ' LIMIT ?';
288 push(@args, $args{num} || -1);
290 if ($args{offset}) {
291 $sql .= ' OFFSET ?';
292 push(@args, $args{offset});
296 my $sth = $self->_prepare($sql);
297 $self->_execute($sth, @args);
299 my @r = ();
301 while(my $r = $sth->fetchrow_arrayref()) {
302 push(@r, $r->[0]);
305 return @r;
309 sub search_stories {
310 my $self = shift;
311 my $topic_id = shift;
312 my $query = shift;
313 my $future = shift;
315 my @q = map { '%' . $_ . '%' } split(/\s+/, $query);
316 my $cond = 'AND content LIKE ? ' x scalar(@q);
318 unless ($future) {
319 $cond .= 'AND date <= ? ';
320 push(@q, Gruta::Data::today());
323 my $sql = 'SELECT id FROM stories WHERE topic_id = ? ' . $cond .
324 'ORDER BY date DESC';
326 my $sth = $self->_prepare($sql);
328 $self->_execute($sth, $topic_id, @q);
330 my @r = ();
332 while(my $r = $sth->fetchrow_arrayref()) {
333 push(@r, $r->[0]);
336 return @r;
340 sub stories_top_ten {
341 my $self = shift;
342 my $num = shift;
344 my $sql = 'SELECT hits, topic_id, id FROM stories ' .
345 'ORDER BY hits DESC LIMIT ?';
347 my $sth = $self->_prepare($sql);
348 $self->_execute($sth, $num);
350 my @r = ();
352 while (my @a = $sth->fetchrow_array()) {
353 push(@r, [ @a ]);
356 return @r;
360 sub search_stories_by_tag {
361 my $self = shift;
362 my $tag = shift;
363 my $future = shift;
365 my @tags = map { lc($_) } split(/\s*,\s*/, $tag);
367 my @r = ();
369 if (@tags) {
370 my $sql;
372 if ($future) {
373 $sql = 'SELECT DISTINCT topic_id, id FROM tags WHERE ' .
374 join(' OR ', map { 'tag = ?' } @tags);
376 else {
377 $sql = 'SELECT DISTINCT tags.topic_id, tags.id FROM tags, stories WHERE ' .
378 'tags.topic_id = stories.topic_id AND tags.id = stories.id AND ' .
379 "stories.date <= '" . Gruta::Data::today() . "' AND (" .
380 join(' OR ', map { 'tag = ?' } @tags) . ')';
383 my $sth = $self->_prepare($sql);
384 $self->_execute($sth, map { lc($_) } @tags);
386 while (my @a = $sth->fetchrow_array()) {
387 push(@r, [ @a ]);
391 return @r;
395 sub tags {
396 my $self = shift;
398 my @r = ();
400 my $sth = $self->_prepare(
401 'SELECT tag, count(tag) FROM tags GROUP BY tag');
402 $self->_execute($sth);
404 while (my @a = $sth->fetchrow_array()) {
405 push(@r, [ @a ]);
408 return @r;
411 sub session { return _one( @_, 'Gruta::Data::DBI::Session' ); }
413 sub purge_old_sessions {
414 my $self = shift;
416 my $sth = $self->_prepare('DELETE FROM sids WHERE time < ?');
417 $self->_execute($sth, time() - (60 * 60 * 24));
419 return undef;
423 sub _insert {
424 my $self = shift;
425 my $obj = shift;
426 my $table = shift;
427 my $class = shift;
429 my $sth;
431 bless($obj, $class);
432 $obj->source($self);
434 if (not $sth = $self->{sth}->{insert}->{ref($obj)}) {
435 my $sql = 'INSERT INTO ' . $table .
436 ' (' . join(', ', $obj->fields()) . ')' .
437 ' VALUES (' . join(', ', map { '?' } $obj->fields()) . ')';
439 $sth = $self->{sth}->{insert}->{ref($obj)} = $self->_prepare($sql);
442 $self->_execute($sth, map { $obj->get($_) } $obj->fields());
444 return $obj;
447 sub insert_topic { $_[0]->_insert($_[1], 'topics', 'Gruta::Data::DBI::Topic'); }
448 sub insert_user { $_[0]->_insert($_[1], 'users', 'Gruta::Data::DBI::User'); }
451 sub insert_story {
452 my $self = shift;
453 my $story = shift;
455 if (not $story->get('id')) {
456 # alloc an id for the story
457 my $id = undef;
459 my $sth = $self->_prepare(
460 'SELECT 1 FROM stories WHERE topic_id = ? AND id = ?');
462 do {
463 $id = $story->new_id();
464 $self->_execute($sth, $story->get('topic_id'), $id);
466 } while $sth->fetchrow_arrayref();
468 $story->set('id', $id);
471 $self->_insert($story, 'stories', 'Gruta::Data::DBI::Story');
473 return $story;
476 sub insert_session { $_[0]->_insert($_[1], 'sids', 'Gruta::Data::DBI::Session'); }
478 sub create {
479 my $self = shift;
481 eval {
482 my $sth = $self->_prepare('SELECT 1 FROM users');
485 if (! $@) {
486 return $self;
489 my $sql = '';
491 while(<DATA>) {
492 chomp;
494 if (/^;$/) {
495 $self->{dbh}->do($sql);
496 $sql = '';
498 else {
499 $sql .= $_;
503 return $self;
507 sub new {
508 my $class = shift;
510 my $s = bless( { @_ }, $class);
512 $s->{dbh} = DBI->connect($s->{string},
513 $s->{user}, $s->{passwd}, { RaiseError => 1 });
515 return $s;
519 __DATA__
520 CREATE TABLE topics (
521 id VARCHAR PRIMARY KEY,
522 name VARCHAR,
523 editors VARCHAR,
524 max_stories INTEGER,
525 internal INTEGER
528 CREATE TABLE stories (
529 id VARCHAR NOT NULL,
530 topic_id VARCHAR NOT NULL,
531 title VARCHAR,
532 date CHAR(14),
533 date2 CHAR(14),
534 userid VARCHAR,
535 format VARCHAR,
536 hits INTEGER DEFAULT 0,
537 ctime INTEGER,
538 content VARCHAR,
539 PRIMARY KEY (id, topic_id)
542 CREATE TABLE users (
543 id VARCHAR PRIMARY KEY,
544 username VARCHAR,
545 email VARCHAR,
546 password VARCHAR,
547 can_upload INTEGER,
548 is_admin INTEGER,
549 xdate CHAR(14)
552 CREATE TABLE sids (
553 id VARCHAR PRIMARY KEY,
554 time INTEGER,
555 user_id VARCHAR,
556 ip VARCHAR
559 CREATE TABLE tags (
560 id VARCHAR NOT NULL,
561 topic_id VARCHAR NOT NULL,
562 tag VARCHAR NOT NULL
565 CREATE INDEX stories_by_date ON stories (date)
567 CREATE INDEX stories_by_hits ON stories (hits)
569 CREATE INDEX tags_by_tag ON tags (tag)