File Coverage

blib/lib/Karas.pm
Criterion Covered Total %
statement 143 259 55.2
branch 24 80 30.0
condition 12 42 28.5
subroutine 28 38 73.6
pod 17 22 77.2
total 224 441 50.7


line stmt bran cond sub pod time code
1             package Karas;
2 9     9   307913 use strict;
  9         23  
  9         314  
3 9     9   61 use warnings;
  9         18  
  9         224  
4 9     9   202 use 5.010001;
  9         35  
  9         492  
5             our $VERSION = '0.07';
6 9     9   44 use Carp ();
  9         17  
  9         334  
7             use Class::Accessor::Lite 0.05 (
8 9         80 rw => [qw/query_builder default_row_class owner_pid connection_manager row_class_map/],
9 9     9   8992 );
  9         12508  
10 9         67 use Class::Trigger qw(
11             BEFORE_INSERT
12              
13             BEFORE_REPLACE
14              
15             BEFORE_BULK_INSERT
16              
17             BEFORE_INSERT_ON_DUPLICATE
18              
19             BEFORE_UPDATE_ROW
20             AFTER_UPDATE_ROW
21             BEFORE_UPDATE_DIRECT
22             AFTER_UPDATE_DIRECT
23              
24             BEFORE_DELETE_ROW
25             BEFORE_DELETE_WHERE
26             AFTER_DELETE_ROW
27             AFTER_DELETE_DIRECT
28 9     9   10125 );
  9         18528  
29              
30 9     9   10469 use Module::Load ();
  9         16556  
  9         207  
31 9     9   8805 use Data::Page::NoTotalEntries;
  9         6122  
  9         276  
32              
33 9     9   7827 use DBIx::TransactionManager;
  9         33094  
  9         288  
34 9     9   8853 use DBIx::Handler;
  9         131148  
  9         301  
35              
36 9     9   6331 use Karas::Row;
  9         28  
  9         330  
37 9     9   5016 use Karas::QueryBuilder;
  9         34  
  9         31231  
38              
39             sub new {
40 10     10 1 52091 my $class = shift;
41 10 50       80 my %args = @_==1 ? %{$_[0]} : @_;
  0         0  
42 10 50       57 unless ($args{connect_info}) {
43 0         0 Carp::croak("Missing mandatory parameter: connect_info");
44             }
45              
46             # ref. http://blog.nomadscafe.jp/2012/11/dbi-connect.html
47 10   50     109 $args{connect_info}->[3]->{RaiseError} //= 1;
48 10   50     84 $args{connect_info}->[3]->{PrintError} //= 0;
49 10   50     66 $args{connect_info}->[3]->{AutoCommit} //= 1;
50 10   50     65 $args{connect_info}->[3]->{ShowErrorStatement} //= 1;
51 10   50     58 $args{connect_info}->[3]->{AutoInactiveDestroy} //= 1;
52              
53 10         99 $args{connection_manager} = DBIx::Handler->new(
54 10         20 @{$args{connect_info}}
55             );
56 10         292 my $self = bless {
57             row_class_map => {},
58             %args
59             }, $class;
60 10   33     147 $self->{query_builder} ||= Karas::QueryBuilder->new(driver => $self->_driver_name);
61 10         10756 return $self;
62             }
63              
64             sub _driver_name {
65 23     23   38 my $self = shift;
66 23   66     130 $self->{driver_name} //= $self->dbh->{Driver}->{Name};
67             }
68              
69             # -------------------------------------------------------------------------
70             # Plugin
71             #
72             # -------------------------------------------------------------------------
73             sub load_plugin {
74 0     0 1 0 my ($class, $name, $args) = @_;
75 0 0       0 Carp::croak("Do not use this plugin to instance") if ref $class;
76 0 0       0 Carp::croak("Do not load this plugin to Karas itself. Please make your own child class from Karas.") if $class eq 'Karas';
77              
78 0 0       0 $name = ($name =~ s/^\+//) ? $name : "Karas::Plugin::$name";
79 0         0 Module::Load::load($name);
80 0   0     0 my $plugin = $name->new($args || +{});
81 0         0 $plugin->init($class);
82             }
83              
84             # -------------------------------------------------------------------------
85             # Connection
86             #
87             # -------------------------------------------------------------------------
88              
89             sub dbh {
90 57     57 1 1291 my $self = shift @_;
91 57 50       148 Carp::croak("Too many arguments for Karas#dbh") if @_!=0;
92 57         198 return $self->connection_manager->dbh();
93             }
94              
95             sub disconnect {
96 1     1 0 40 my ($self) = @_;
97 1 50       6 Carp::croak("Too many arguments for Karas#disconnect") if @_!=1;
98 1         5 $self->connection_manager->disconnect();
99             }
100              
101             # -------------------------------------------------------------------------
102             # schema
103             #
104             # -------------------------------------------------------------------------
105              
106             sub load_schema_from_db {
107 0     0 0 0 my ($self, %args) = @_;
108 0         0 require Karas::Loader;
109 0   0     0 my $class = ref($self) || $self;
110 0   0     0 $args{namespace} //= do {
111 0 0       0 if ($class eq 'Karas') {
112 0         0 state $i=0;
113 0         0 $class . '::Anon' . $i++;
114             } else {
115 0         0 $class;
116             }
117             };
118 0         0 $self->{row_class_map} = Karas::Loader->load(
119             dbh => $self->dbh,
120             %args
121             );
122 0         0 return undef;
123             }
124              
125             sub get_row_class {
126 23     23 1 45 my ($self, $table_name) = @_;
127 23 50       56 Carp::croak("Missing mandatory parameter: table_name") unless $table_name;
128 23 50       81 my $row_class = $self->row_class_map->{$table_name}
129             or Carp::croak("Unknown table: $table_name. $table_name is not registered to Karas.");
130 23         262 return $row_class;
131             }
132              
133             # -------------------------------------------------------------------------
134             # SQL
135             #
136             # -------------------------------------------------------------------------
137              
138             sub search {
139 7     7 1 31 my ($self, $table, $where, $opt) = @_;
140 7   50     49 $opt->{cols} ||= [\'*'];
141 7         22 my ($sql, @binds) = $self->query_builder->select($table, $opt->{cols}, $where, $opt);
142 7         2837 my $sth = $self->dbh->prepare($sql);
143 7         951 $sth->execute(@binds);
144 7         35 my $row_class = $self->get_row_class($table);
145 7         11 my @rows;
146 7         155 while (my $row = $sth->fetchrow_hashref) {
147 7         82 push @rows, $row_class->new($row);
148             }
149 7         113 return @rows;
150             }
151              
152             sub count {
153 3     3 1 18 my ($self, $table, $where) = @_;
154 3         11 my ($sql, @binds) = $self->query_builder->select($table, [\'COUNT(*)'], $where);
155 3         917 my $sth = $self->dbh->prepare($sql);
156 3         422 $sth->execute(@binds);
157 3         27 my ($count) = $sth->fetchrow_array();
158 3         50 return $count;
159             }
160              
161             sub search_with_pager {
162 0     0 1 0 my ($self, $table, $where, $opt) = @_;
163 0   0     0 $opt->{cols} ||= [\'*'];
164 0   0     0 my $page = delete $opt->{page} // Carp::croak("Missing mandatory parameter: page");
165 0   0     0 my $rows = delete $opt->{rows} // Carp::croak("Missing mandatory parameter: rows");
166 0         0 $opt->{limit} = $rows+1;
167 0         0 $opt->{offset} = $rows*($page-1);
168 0         0 my ($sql, @binds) = $self->query_builder->select($table, $opt->{cols}, $where, $opt);
169 0         0 my $sth = $self->dbh->prepare($sql);
170 0         0 $sth->execute(@binds);
171 0         0 my $row_class = $self->get_row_class($table);
172 0         0 my @rows;
173 0         0 while (my $row = $sth->fetchrow_hashref) {
174 0         0 push @rows, $row_class->new($row);
175             }
176 0         0 my $has_next = 0;
177 0 0       0 if (@rows == $rows+1) {
178 0         0 pop @rows;
179 0         0 $has_next = 1;
180             }
181 0         0 my $pager = Data::Page::NoTotalEntries->new(
182             has_next => $has_next,
183             entries_per_page => $rows,
184             current_page => $page,
185             entries_on_this_page => 0+@rows,
186             );
187 0         0 return (\@rows, $pager);
188             }
189              
190             sub search_by_sql {
191 0     0 1 0 my ($self, $sql, $binds, $table_name) = @_;
192 0         0 my $sth = $self->dbh->prepare($sql);
193 0         0 $sth->execute(@$binds);
194 0   0     0 $table_name //= $self->guess_table_name($sql);
195 0 0       0 unless ($table_name) {
196 0         0 Carp::croak("Cannot guess table name from SQL. You need to pass the table name: " . $sql);
197             }
198 0         0 my $row_class = $self->get_row_class($table_name);
199 0         0 my @rows;
200 0         0 while (my $row = $sth->fetchrow_hashref) {
201 0         0 push @rows, $row_class->new($row);
202             }
203 0         0 return @rows;
204             }
205              
206             sub insert {
207 11     11 1 120 my ($self, $table, $values) = @_;
208 11 50       40 Carp::croak("Missing mandatory parameter: table") unless defined $table;
209 11 50       26 Carp::croak("Missing mandatory parameter: values") unless defined $values;
210 11         49 $self->_insert($table, $values);
211              
212             # and select it.
213 11         51 my $row_class = $self->get_row_class($table);
214 11         49 my $last_insert_id = $self->last_insert_id;
215 11         584 my @pk = $row_class->primary_key;
216 11 100 66     65 if (@pk == 1 && defined($last_insert_id)) {
217 4         28 return(($self->search($table, {$pk[0] => $last_insert_id}))[0]);
218             }
219              
220             # cannot select row. just create new object from arguments.
221 7         34 return $row_class->new($values);
222             }
223              
224             sub fast_insert {
225 0     0 1 0 my ($self, $table, $values) = @_;
226 0 0       0 Carp::croak("Missing mandatory parameter: table") unless defined $table;
227 0 0       0 Carp::croak("Missing mandatory parameter: values") unless defined $values;
228 0         0 $self->_insert($table, $values);
229 0         0 return $self->last_insert_id;
230             }
231              
232             sub _insert {
233 11     11   18 my ($self, $table, $values) = @_;
234 11         59 $self->call_trigger(BEFORE_INSERT => $table, $values);
235 11         717 my ($sql, @binds) = $self->query_builder->insert($table, $values);
236 11         1567 my $sth = $self->dbh->prepare($sql);
237 11         1523 $sth->execute(@binds);
238 11         138 return undef;
239             }
240              
241             sub replace {
242 2     2 1 480 my ($self, $table, $values) = @_;
243 2         20 $self->call_trigger(BEFORE_REPLACE => $table, $values);
244 2         189 my ($sql, @binds) = $self->query_builder->insert($table, $values, {prefix => 'REPLACE INTO'});
245 2         287 my $sth = $self->dbh->prepare($sql);
246 2         344 $sth->execute(@binds);
247 2         17 my $last_insert_id = $self->last_insert_id;
248 2         140 return $last_insert_id;
249             }
250              
251             sub retrieve {
252 4     4 0 28 my ($self, $table, $vals) = @_;
253 4 50       10 Carp::croak("Missing mandatory parameter: table") unless defined $table;
254 4 50       10 Carp::croak("Too many arguments") if @_ > 3;
255              
256 4         10 my $row_class = $self->get_row_class($table);
257 4         4 my %where;
258 4 100       12 if (ref $vals eq 'HASH') {
    50          
259 3         9 %where = %$vals;
260             } elsif (ref $vals) {
261 0         0 Carp::croak("Bad arguments for retrieve: $vals");
262             } else {
263 1         4 my @pk = $row_class->primary_key;
264 1 50       4 if (@pk != 1) {
265 0         0 Carp::croak(sprintf("%s has %d primary keys, but you passed %d(%s)", $table, 0+@pk, 1, join(', ', @pk)));
266             }
267 1         3 $where{$pk[0]} = $vals;
268             }
269 4         12 my ($sql, @binds) = $self->query_builder->select($table, [\'*'], \%where);
270 4         1174 my $sth = $self->dbh->prepare($sql);
271 4         505 $sth->execute(@binds);
272 4         66 my $row = $sth->fetchrow_hashref;
273 4 50       14 if ($row) {
274 4         14 return $row_class->new($row);
275             } else {
276 0         0 return undef;
277             }
278             }
279              
280             sub update {
281 1     1 1 4 my $self = shift;
282 1 50       8 if (UNIVERSAL::isa($_[0], 'Karas::Row')) {
283 1         3 my ($row, $set) = @_;
284 1   50     7 $set ||= +{};
285 1         2 $set = +{ %{$row->get_dirty_columns()}, %$set };
  1         9  
286 1         10 my $where = $row->make_where_condition();
287 1         5 $self->call_trigger(BEFORE_UPDATE_ROW => $row, $set);
288 1         90 my $rows = $self->_update($row->table_name, $set, $where);
289 1         5 $self->call_trigger(AFTER_UPDATE_ROW => $row, $set);
290 1         77 return $rows;
291             } else {
292 0         0 my ($table_name, $set, $where) = @_;
293 0 0       0 Carp::croak("Usage: \$db->update(\$table_name, \%set, \%where)") if ref $table_name;
294 0 0       0 Carp::croak("Usage: \$db->update(\$table_name, \%set, \%where)") if @_!=3;
295 0         0 $self->call_trigger(BEFORE_UPDATE_DIRECT => $table_name, $set, $where);
296 0         0 my $rows = $self->_update($table_name, $set, $where);
297 0         0 $self->call_trigger(AFTER_UPDATE_DIRECT => $table_name, $set, $where);
298 0         0 return $rows;
299             }
300             }
301              
302             sub _update {
303 1     1   4 my ($self, $table, $set, $where) = @_;
304 1 50       5 Carp::croak("Missing mandatory parameter: table") unless defined $table;
305 1 50       3 Carp::croak("Missing mandatory parameter: set") unless defined $set;
306 1         6 my ($sql, @binds) = $self->query_builder->update($table, $set, $where);
307 1         193 my $sth = $self->dbh->prepare($sql);
308 1         171 $sth->execute(@binds);
309 1         18 return $sth->rows;
310             }
311              
312             sub delete {
313 0     0 1 0 my $self = shift;
314 0 0       0 if (UNIVERSAL::isa($_[0], 'Karas::Row')) {
315 0         0 my ($row) = @_;
316 0         0 $self->call_trigger(BEFORE_DELETE_ROW => $row);
317 0         0 my $where = $row->make_where_condition();
318 0         0 my $retval = $self->_delete($row->table_name, $row->where);
319 0         0 $self->call_trigger(AFTER_DELETE_ROW => $row);
320 0         0 return $retval;
321             } else {
322 0         0 my ($table_name, $where);
323 0         0 $self->call_trigger(BEFORE_DELETE_DIRECT => $table_name, $where);
324 0         0 my $rows = $self->_delete($table_name, $where);
325 0         0 $self->call_trigger(AFTER_DELETE_DIRECT => $table_name, $where);
326 0         0 return $rows;
327             }
328             }
329              
330             sub _delete {
331 0     0   0 my ($self, $table, $where) = @_;
332 0 0       0 Carp::croak("Missing mandatory parameter: table") unless defined $table;
333 0 0       0 Carp::croak("Missing mandatory parameter: where") unless defined $where;
334 0         0 my ($sql, @binds) = $self->query_builder->delete($table, $where);
335 0         0 my $sth = $self->dbh->prepare($sql);
336 0         0 $sth->execute(@binds);
337 0         0 return $sth->rows;
338             }
339              
340             sub refetch {
341 1     1 1 2 my ($self, $row) = @_;
342 1         5 return ($self->search($row->table_name, $row->make_where_condition()))[0];
343             }
344              
345             sub bulk_insert {
346 0     0 1 0 my ($self, $table_name, $rows_data) = @_;
347 0 0       0 Carp::croak("Missing mandatory parameter: table_name") unless defined $table_name;
348 0 0       0 Carp::croak("rows_data must be ArrayRef") unless ref $rows_data eq 'ARRAY';
349              
350 0 0       0 if ($self->_driver_name eq 'mysql') {
351 0         0 $self->call_trigger(BEFORE_BULK_INSERT => $table_name, $rows_data);
352 0         0 my ($sql, @binds) = $self->query_builder->insert_multi($table_name, $rows_data);
353 0         0 my $sth = $self->dbh->prepare($sql);
354 0         0 $sth->execute(@binds);
355 0         0 return $sth->rows;
356             } else {
357             # emulate bulk insert.
358 0         0 $self->call_trigger(BEFORE_BULK_INSERT => $table_name, $rows_data);
359 0         0 my $txn = $self->txn_scope();
360             {
361             # Do not run 'BEFORE_INSERT' hook for consistency between mysql.
362 0         0 for my $row (@$rows_data) {
  0         0  
363 0         0 my ($sql, @binds) = $self->query_builder->insert($table_name, $row);
364 0         0 my $sth = $self->dbh->prepare($sql);
365 0         0 $sth->execute(@binds);
366             }
367             }
368 0         0 $txn->commit;
369             }
370             }
371              
372             sub insert_on_duplicate {
373 0     0 0 0 my ($self, $table_name, $insert_values, $update_values) = @_;
374 0 0       0 if ($self->_driver_name eq 'mysql') {
375 0         0 $self->call_trigger(BEFORE_INSERT_ON_DUPLICATE => $table_name, $insert_values, $update_values);
376 0         0 my ($sql, @binds) = $self->query_builder->insert_on_duplicate($table_name, $insert_values, $update_values);
377 0         0 my $sth = $self->dbh->prepare($sql);
378 0         0 $sth->execute(@binds);
379             } else {
380 0         0 Carp::croak("'insert_on_duplicate' method only supports mysql: " . $self->_driver_name);
381             }
382              
383 0         0 return undef;
384             }
385              
386             # taken from teng.
387             sub guess_table_name {
388 2     2 0 14 my ( $class, $sql ) = @_;
389              
390 2 50       17 if ( $sql =~ /\sfrom\s+["`]?([\w]+)["`]?\s*/si ) {
391 2         14 return $1;
392             }
393 0         0 return undef;
394             }
395              
396             # -------------------------------------------------------------------------
397             # transaction
398             #
399             # -------------------------------------------------------------------------
400              
401             sub txn_scope {
402 0     0 1 0 my ($self) = @_;
403 0         0 return $self->connection_manager->txn_scope;
404             }
405              
406             # taken from Teng
407             sub last_insert_id {
408 13     13 1 23 my ( $self, $table_name ) = @_;
409              
410 13         30 my $driver = $self->_driver_name;
411 13 50       62 if ( $driver eq 'mysql' ) {
    50          
    50          
    0          
412 0         0 return $self->dbh->{mysql_insertid};
413             }
414             elsif ( $driver eq 'Pg' ) {
415 0         0 return $self->dbh->last_insert_id( undef, undef, undef, undef, { sequence => join( '_', $table_name, 'id', 'seq' ) } );
416             }
417             elsif ( $driver eq 'SQLite' ) {
418 13         32 return $self->dbh->func('last_insert_rowid');
419             }
420             elsif ( $driver eq 'Oracle' ) {
421 0           return undef;
422             }
423             else {
424 0           Carp::croak "Don't know how to get last insert id for $driver";
425             }
426             }
427              
428             1;
429             __END__