DBI now uses story->new_id().
[gruta.git] / Gruta / Source / DBI.pm
blob723f4c0170160def80d113223ac44aadf1b63514
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} && $args{today}) {
276 $sql .= ' AND date < ?';
277 push(@args, $args{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;
311 my @q = map { '%' . $_ . '%' } split(/\s+/, $query);
312 my $like = 'AND content LIKE ? ' x scalar(@q);
314 my $sth = $self->_prepare(
315 'SELECT id FROM stories WHERE topic_id = ? ' . $like .
316 'ORDER BY date DESC');
318 $self->_execute($sth, $topic_id, @q);
320 my @r = ();
322 while(my $r = $sth->fetchrow_arrayref()) {
323 push(@r, $r->[0]);
326 return @r;
330 sub stories_top_ten {
331 my $self = shift;
332 my $num = shift;
334 my $sql = 'SELECT hits, topic_id, id FROM stories ' .
335 'ORDER BY hits DESC LIMIT ?';
337 my $sth = $self->_prepare($sql);
338 $self->_execute($sth, $num);
340 my @r = ();
342 while (my @a = $sth->fetchrow_array()) {
343 push(@r, [ @a ]);
346 return @r;
350 sub search_stories_by_tag {
351 my $self = shift;
352 my @tags = @_;
354 my @r = ();
356 if (@tags) {
357 my $sql = 'SELECT DISTINCT topic_id, id FROM tags WHERE ' .
358 join(' OR ', map { 'tag = ?' } @tags);
360 my $sth = $self->_prepare($sql);
361 $self->_execute($sth, map { lc($_) } @tags);
363 while (my @a = $sth->fetchrow_array()) {
364 push(@r, [ @a ]);
368 return @r;
372 sub tags {
373 my $self = shift;
375 my @r = ();
377 my $sth = $self->_prepare(
378 'SELECT tag, count(tag) FROM tags GROUP BY tag ORDER BY 2 DESC, 1');
379 $self->_execute($sth);
381 while (my @a = $sth->fetchrow_array()) {
382 push(@r, [ @a ]);
385 return @r;
388 sub session { return _one( @_, 'Gruta::Data::DBI::Session' ); }
390 sub purge_old_sessions {
391 my $self = shift;
393 my $sth = $self->_prepare('DELETE FROM sids WHERE time < ?');
394 $self->_execute($sth, time() - (60 * 60 * 24));
396 return undef;
400 sub _insert {
401 my $self = shift;
402 my $obj = shift;
403 my $table = shift;
404 my $class = shift;
406 my $sth;
408 bless($obj, $class);
409 $obj->source($self);
411 if (not $sth = $self->{sth}->{insert}->{ref($obj)}) {
412 my $sql = 'INSERT INTO ' . $table .
413 ' (' . join(', ', $obj->fields()) . ')' .
414 ' VALUES (' . join(', ', map { '?' } $obj->fields()) . ')';
416 $sth = $self->{sth}->{insert}->{ref($obj)} = $self->_prepare($sql);
419 $self->_execute($sth, map { $obj->get($_) } $obj->fields());
421 return $obj;
424 sub insert_topic { $_[0]->_insert($_[1], 'topics', 'Gruta::Data::DBI::Topic'); }
425 sub insert_user { $_[0]->_insert($_[1], 'users', 'Gruta::Data::DBI::User'); }
428 sub insert_story {
429 my $self = shift;
430 my $story = shift;
432 if (not $story->get('id')) {
433 # alloc an id for the story
434 my $id = undef;
436 my $sth = $self->_prepare(
437 'SELECT 1 FROM stories WHERE topic_id = ? AND id = ?');
439 do {
440 $id = $story->new_id();
441 $self->_execute($sth, $story->get('topic_id'), $id);
443 } while $sth->fetchrow_arrayref();
445 $story->set('id', $id);
448 $self->_insert($story, 'stories', 'Gruta::Data::DBI::Story');
450 return $story;
453 sub insert_session { $_[0]->_insert($_[1], 'sids', 'Gruta::Data::DBI::Session'); }
455 sub create {
456 my $self = shift;
458 my $sql = '';
460 while(<DATA>) {
461 chomp;
463 if (/^;$/) {
464 $self->{dbh}->do($sql);
465 $sql = '';
467 else {
468 $sql .= $_;
474 sub new {
475 my $class = shift;
477 my $s = bless( { @_ }, $class);
479 $s->{dbh} = DBI->connect($s->{string},
480 $s->{user}, $s->{passwd}, { RaiseError => 1 });
482 return $s;
486 __DATA__
487 CREATE TABLE topics (
488 id VARCHAR PRIMARY KEY,
489 name VARCHAR,
490 editors VARCHAR,
491 max_stories INTEGER,
492 internal INTEGER
495 CREATE TABLE stories (
496 id VARCHAR NOT NULL,
497 topic_id VARCHAR NOT NULL,
498 title VARCHAR,
499 date VARCHAR,
500 userid VARCHAR,
501 format VARCHAR,
502 hits INTEGER,
503 ctime INTEGER,
504 content VARCHAR,
505 PRIMARY KEY (id, topic_id)
508 CREATE TABLE users (
509 id VARCHAR PRIMARY KEY,
510 username VARCHAR,
511 email VARCHAR,
512 password VARCHAR,
513 can_upload INTEGER,
514 is_admin INTEGER
517 CREATE TABLE sids (
518 id VARCHAR PRIMARY KEY,
519 time INTEGER,
520 user_id VARCHAR,
521 ip VARCHAR
524 CREATE TABLE tags (
525 id VARCHAR NOT NULL,
526 topic_id VARCHAR NOT NULL,
527 tag VARCHAR NOT NULL
530 CREATE INDEX stories_by_date ON stories (date)
532 CREATE INDEX stories_by_hits ON stories (hits)