File Coverage

blib/lib/YATT/Lite/WebMVC0/DBSchema.pm
Criterion Covered Total %
statement 388 538 72.1
branch 142 256 55.4
condition 45 98 45.9
subroutine 76 100 76.0
pod 1 80 1.2
total 652 1072 60.8


line stmt bran cond sub pod time code
1             package YATT::Lite::WebMVC0::DBSchema; sub MY () {__PACKAGE__}
2 3     3   35890 use strict;
  3         8  
  3         96  
3 3     3   16 use warnings qw(FATAL all NONFATAL misc);
  3         7  
  3         102  
4 3     3   15 use Carp;
  3         6  
  3         152  
5 3     3   19 use File::Basename;
  3         5  
  3         167  
6 3     3   105 use version;
  3         5894  
  3         15  
7              
8 3         1435 use base qw/YATT::Lite::Object
9             YATT::Lite::Util::CmdLine
10 3     3   204 /;
  3         8  
11 3         30 use fields (qw/table_list table_dict dbtype cf_DBH
12             cf_user
13             cf_auth
14             cf_connection_spec
15             cf_connect_atstart
16             cf_verbose
17             cf_dbtype
18             cf_NULL
19             cf_name
20             cf_no_header
21             cf_auto_create
22             cf_coltype_map
23              
24             cf_after_dbinit
25             cf_group_writable
26              
27             cf_is_clone
28             cf_debug
29             cf_on_destroy
30              
31             role_dict
32 3     3   32 /);
  3         6  
33              
34             use YATT::Lite::Types
35 3         32 ([Item => fields => [qw/not_configured
36             cf_name/]
37             , [Table => fields => [qw/pk chk_unique
38             chk_index chk_check
39             col_list col_dict
40             relation_list relation_dict
41             reference_dict
42             initializer
43             cf_view cf_virtual
44             cf_trigger_after_delete
45             /]]
46             , [Column => fields => [qw/cf_type
47             cf_hidden
48             cf_unique
49             cf_indexed
50             cf_primary_key
51             cf_autoincrement
52              
53             cf_default
54             cf_null
55              
56             cf_usage
57             cf_label
58             cf_max_length
59             /]]]
60 3     3   1759 );
  3         6  
61              
62 3         19419 use YATT::Lite::Util qw/coalesce globref ckeval terse_dump lexpand
63             shallow_copy
64 3     3   21 /;
  3         8  
65              
66             #========================================
67             DESTROY {
68 11     11   18130 my MY $self = shift;
69 11 100       59 if (my $sub = $self->{cf_on_destroy}) {
70 1         4 $sub->($self);
71             }
72 11         49 $self->disconnect("from DBSchema->DESTROY");
73             }
74             sub disconnect {
75 11     11 0 37 (my MY $schema, my $msg) = @_;
76 11   50     39 $msg ||= "";
77 11 100       46 if (my $dbh = delete $schema->{cf_DBH}) {
78             # XXX: is_clone
79 5 100       84 $dbh->commit unless $dbh->{AutoCommit};
80 5         221 $dbh->disconnect;
81             print STDERR "DEBUG: DBSchema->disconnect $msg $schema, had dbh $dbh\n"
82 5 50       135 if $schema->{cf_debug};
83             } else {
84             print STDERR "DEBUG: DBSchema->disconnect $msg $schema, without dbh\n"
85 6 50       87 if $schema->{cf_debug};
86             }
87             }
88              
89             #========================================
90              
91             sub new {
92 14     14 1 3941 my $pack = shift;
93 14         77 $pack->parse_import(\@_, \ my %opts);
94 14         99 my MY $self = $pack->SUPER::new(%opts);
95 14         63 $self->init_schema;
96 14 50       70 $self->add_schema(@_) if @_;
97 14         75 $self->verify_schema;
98 14         92 $self;
99             }
100              
101             sub clone {
102 1     1 0 4 my MY $orig = shift;
103 1 50       4 croak "Can't clone non-object: $orig" unless ref $orig;
104 1         6 my MY $new = bless {}, ref($orig);
105 1         107 foreach my $k (keys %$orig) {
106 6         11 my $v = $orig->{$k};
107             # shallow_copy with pass-thru flag.
108 6 100       26 $new->{$k} = ref $v ? shallow_copy($v, 1) : $v;
109             }
110 1         6 $new->reset;
111 1         3 $new->{cf_is_clone} = 1;
112 1 50       14 $new->configure(@_) if @_;
113 1 50       4 print STDERR "DEBUG: dbschema clone, now=$new\n" if $new->{cf_debug};
114 1         5 $new;
115             }
116              
117             sub reset {
118 2     2 0 5 (my MY $self) = @_;
119 2 100       9 if (my $dbh = delete $self->{cf_DBH}) {
120 1 50       34 $dbh->disconnect if $self->{cf_is_clone};
121             }
122             }
123              
124             sub is_known_role {
125 0     0 0 0 (my MY $self, my $class) = @_;
126 0   0     0 $class //= caller;
127 0         0 $self->{role_dict}{$class}++;
128             }
129              
130             # Extension hook.
131       14 0   sub init_schema {}
132              
133             sub add_schema {
134 14     14 0 34 (my MY $self) = shift;
135 14         34 foreach my $item (@_) {
136 20 50       54 if (ref $item) {
137 20         74 $self->add_table(@$item);
138             } else {
139 0         0 croak "Invalid schema item: $item";
140             }
141             }
142             }
143              
144             sub parse_import {
145 14     14 0 40 my ($pack, $list, $opts) = @_;
146             # -bool_flag
147             # key => value
148 14         99 for (; @$list; shift @$list) {
149 20 100       87 last if ref $list->[0];
150 6 50       31 if ($list->[0] =~ /^-(\w+)/) {
151 0         0 $opts->{$1} = 1;
152             } else {
153 6 50       25 croak "Option value is missing for $list->[0]"
154             unless @$list >= 2;
155 6         23 $opts->{$list->[0]} = $list->[1];
156 6         22 shift @$list;
157             }
158             }
159             }
160              
161             #########################################
162             sub after_connect {
163 5     5 0 13 my MY $self = shift;
164 5 50       44 $self->ensure_created_on($self->{cf_DBH}) if $self->{cf_auto_create};
165             }
166              
167             sub dbinit_sqlite {
168 5     5 0 11 (my MY $self, my $sqlite_fn) = @_;
169 5 50 50     91 chmod 0664, $sqlite_fn if $self->{cf_group_writable} // 1;
170             }
171              
172             #========================================
173              
174             sub startup {
175 0     0 0 0 (my MY $schema, my (@apps)) = @_;
176 0         0 foreach my $app (@apps) {
177             # XXX: logging?
178 0 0       0 my $sub = $app->can("backend_startup")
179             or next;
180 0         0 $sub->($app, $schema);
181             }
182              
183 0 0       0 if ($schema->{cf_connect_atstart}) {
184 0         0 $schema->make_connection;
185             }
186             }
187              
188             #========================================
189              
190 0     0 0 0 sub has_connection { my MY $schema = shift; $schema->{cf_DBH} }
  0         0  
191              
192             sub dbh {
193 21     21 0 61 (my MY $schema) = @_;
194 21   66     192 $schema->{cf_DBH} // $schema->make_connection;
195             }
196              
197             #
198             # Quasi-option to configure $when and @spec at once.
199             #
200             sub configure_connect {
201 0     0 0 0 (my MY $schema, my $config) = @_;
202 0         0 my ($when, @spec) = @$config;
203 0         0 $schema->{cf_connection_spec} = \@spec;
204 0         0 $schema->{cf_connect_atstart} = $schema->parse_connect_when($when);
205             }
206              
207             sub parse_connect_when {
208 0     0 0 0 (my MY $schema, my $when) = @_;
209 0 0       0 if ($when =~ /^at_?start$/i) {
    0          
210 0         0 1;
211             } elsif ($when =~ /^on_?demand$/i) {
212 0         0 0;
213             } else {
214 0         0 croak "Unknown connection timing: '$when'";
215             }
216             }
217              
218             #
219             # This must fill cf_DBH.
220             #
221             sub make_connection {
222 5     5 0 15 (my MY $schema) = shift;
223 5 100       24 my ($spec) = @_ ? @_ : $schema->{cf_connection_spec};
224 5 50       19 unless (defined $spec) {
225 0         0 croak "connection_spec is empty";
226             }
227 5 50 33     34 if (ref $spec eq 'ARRAY' or not ref $spec) {
    0          
228 5         20 $schema->connect_to(lexpand($spec));
229             } elsif (ref $spec eq 'CODE') {
230 0         0 $spec->($schema);
231             } else {
232 0         0 croak "Unknown connection spec obj: $spec";
233             }
234             print STDERR "DEBUG: dbh for $schema=$schema->{cf_DBH}"
235             , ($schema->{cf_debug} >= 2 ? Carp::longmess() : ()), "\n\n"
236 5 0       19 if $schema->{cf_debug};
    50          
237 5         25 $schema->{cf_DBH};
238             }
239              
240             #----------------------------------------
241             sub connect_to {
242 5     5 0 15 (my MY $schema, my ($dbtype, @args)) = @_;
243 5 50       57 if ($dbtype =~ /^dbi:/i) {
    50          
244 0         0 $schema->connect_to_dbi($dbtype, @args);
245             } elsif (my $sub = $schema->can("connect_to_\L$dbtype")) {
246 5         16 $schema->{dbtype} = lc($dbtype);
247 5         20 $sub->($schema, @args);
248             } else {
249 0         0 croak sprintf("%s: Unknown dbtype: %s", MY, $dbtype);
250             }
251             }
252              
253             sub connect_to_dbi {
254 0     0 0 0 (my MY $schema, my ($dbi, @args)) = @_;
255 0 0       0 my ($driver) = $dbi =~ m{^dbi:([^:]+):}i
256             or croak "Unknown driver spec in DBI DSN! $dbi";
257 0 0       0 if (my $sub = $schema->can("connect_to_\L$driver")) {
258 0         0 $schema->{dbtype} = lc($driver);
259 0         0 $sub->($schema, $dbi, @args);
260             } else {
261 0         0 $schema->dbi_connect($dbi, @args);
262             }
263             }
264              
265             #----------------------------------------
266              
267             sub connect_to_sqlite {
268 5     5 0 17 (my MY $schema, my ($dsn_or_sqlite_fn, %opts)) = @_;
269 5         52 require DBD::SQLite; my $minver = version->parse("1.30_02");
  5         53  
270              
271 5         12 my ($sqlite_fn, $dbi_dsn) = do {
272 5 50       14 if ($dsn_or_sqlite_fn =~ /^dbi:SQLite:(?:dbname=)?(.*)$/i) {
273 0         0 ($1, $dsn_or_sqlite_fn);
274             } else {
275 5         21 ($dsn_or_sqlite_fn, "dbi:SQLite:dbname=$dsn_or_sqlite_fn");
276             }
277             };
278 5 50       16 unless (delete $opts{RO}) {
279 5 50       91 $opts{sqlite_use_immediate_transaction} = 1
280             if version->parse($DBD::SQLite::VERSION) >= $minver;
281             }
282 5   50     27 $schema->{dbtype} //= 'sqlite';
283 5         135 my $first_time = not -e $sqlite_fn;
284 5   50     39 $schema->{cf_auto_create} //= 1;
285 5         30 $schema->dbi_connect($dbi_dsn, undef, undef, %opts);
286 5 50       35 $schema->dbinit_sqlite($sqlite_fn) if $first_time;
287 5         26 $schema;
288             }
289              
290             sub dbi_connect {
291 5     5 0 21 (my MY $schema, my ($dbi_dsn, $user, $auth, %attr)) = @_;
292 5         17 my %default = $schema->default_dbi_attr;
293 5   66     52 $attr{$_} //= $default{$_} for keys %default;
294 5         28 require DBI;
295 5         40 my $dbh = $schema->{cf_DBH} = DBI->connect($dbi_dsn, $user, $auth, \%attr);
296 5         2091 $schema->after_connect;
297 5         18 $schema;
298             }
299              
300             sub default_dbi_attr {
301 5     5 0 26 (RaiseError => 1, PrintError => 0, AutoCommit => 1);
302             }
303              
304             #----------------------------------------
305              
306             #
307             # ./lib/MyModel.pm create sqlite data/myapp.db3
308             #
309             sub create {
310 4     4 0 3627 (my MY $schema, my @spec) = @_;
311             # $schema->dbh() will call ensure_created_on when auto_create is on.
312 4   66     34 my $dbh = $schema->{cf_DBH} || $schema->make_connection(\@spec);
313             #
314 4 100       20 $schema->ensure_created_on($dbh) unless $schema->{cf_auto_create};
315 4         14 $schema;
316             }
317              
318             sub ensure_created_on {
319 10     10 0 30 (my MY $schema, my $dbh) = @_;
320             # Carp::cluck("ensure_created is called");
321              
322 10         49 $schema->dbtype_try_invoke('begin_create');
323              
324 10         523 my (@sql, @created);
325 10         48 foreach my Table $table ($schema->list_tables(raw => 1)) {
326 21 50       118 next if $schema->has_table($table->{cf_name}, $dbh);
327 21         7055 push @created, $table;
328 21         89 foreach my $create ($schema->sql_create_table($table)) {
329 29 0 0     71 unless ($schema->{cf_verbose}) {
    50          
330 0         0 } elsif ($schema->{cf_verbose} >= 2) {
331 0         0 print STDERR "-- $table->{cf_name} --\n$create\n\n"
332             } elsif ($schema->{cf_verbose} and $create =~ /^create table /i) {
333             print STDERR "CREATE TABLE $table->{cf_name}\n";
334             }
335 29         70 push @sql, $create;
336             }
337             }
338 10         47 foreach my Table $view ($schema->list_views(raw => 1)) {
339 2 50       14 next if $schema->has_view($view->{cf_name}, $dbh);
340 2 50       438 next if $view->{cf_virtual};
341 2 50       8 if ($schema->{cf_verbose}) {
342 0         0 print STDERR "CREATE VIEW $view->{cf_name}\n";
343             }
344 2         12 push @sql, "CREATE VIEW $view->{cf_name}\nAS $view->{cf_view}";
345             }
346 10         79 $dbh->do($_) for @sql;
347 10 50       3939 if (@created) {
348 10         28 foreach my Table $tab (@created) {
349 21         93 $schema->ensure_table_populated($dbh, $tab);
350             }
351             }
352 10 50       35 if (@sql) {
353 10 100       110 $dbh->commit unless $dbh->{AutoCommit};
354             }
355 10         48 @created;
356             }
357              
358             sub ensure_table_populated {
359 21     21 0 58 (my MY $schema, my $dbh, my Table $tab) = @_;
360 21         85 foreach my $init (lexpand($tab->{initializer})) {
361 4         12 my ($colSpec, @values) = @$init;
362 4         22 my $sql = $schema->sql_to_insert($tab->{cf_name}, @$colSpec);
363 4         21 my $ins = $dbh->prepare($sql);
364 4         199 foreach my $record (@values) {
365 8 50       21 if (grep {ref $_ eq 'SCALAR'} @$record) {
  16         43  
366             my ($sql, $values) = $schema->sql_and_values_to_insert_expr
367 0         0 ($tab->{cf_name}, $colSpec, $record);
368 0         0 my @vals = $schema->expand_codevalue($tab, $values);
369             print STDERR $sql, "\n -- (", join(",", @vals), ")\n"
370 0 0       0 if $schema->{cf_verbose};
371 0         0 $dbh->do($sql, undef, @vals);
372             } else {
373 8         25 my @vals = $schema->expand_codevalue($tab, $record);
374             print STDERR $sql, "\n -- (", join(",", @vals), ")\n"
375 8 50       25 if $schema->{cf_verbose};
376 8         141 $ins->execute(@vals);
377             }
378             }
379             }
380             }
381              
382             sub sqlite_begin_create {
383 5     5 0 12 (my MY $schema) = @_;
384             # To speedup create statements.
385 5         19 my $dbh = $schema->dbh;
386 5 50       51 if ($dbh->{AutoCommit}) {
387 5         39 $dbh->do("PRAGMA synchronous = OFF");
388             }
389             }
390              
391             sub expand_codevalue {
392 8     8 0 19 (my MY $schema, my $tab, my $record) = @_;
393 8 50       14 map {ref $_ ? $_->($schema, $tab) : $_} @$record;
  16         46  
394             }
395              
396 21     21 0 73 sub has_table { shift->has_type(table => @_); }
397 2     2 0 9 sub has_view { shift->has_type(view => @_); }
398              
399             sub has_type {
400 23     23 0 64 (my MY $schema, my ($type, $table, $dbh)) = @_;
401 23 100 66     128 if ($$schema{dbtype}
402             and my $sub = $schema->can("$$schema{dbtype}_has_type")) {
403 10         29 $sub->($schema, $type, $table, $dbh);
404             } else {
405 13   33     37 $dbh ||= $schema->dbh;
406 13         104 $dbh->tables("", "", $table, uc($type));
407             }
408             }
409              
410             sub dbtype_try_invoke {
411 10     10 0 34 (my MY $schema, my ($method, @args)) = @_;
412 10 100       41 return unless $schema->{dbtype};
413 5 50       41 my $sub = $schema->can("$schema->{dbtype}_$method")
414             or return;
415 5         21 $sub->($schema, @args);
416             }
417              
418             sub sqlite_has_type {
419 10     10 0 24 (my MY $schema, my ($type, $name, $dbh)) = @_;
420 10 50       79 my ($found) = $dbh->selectrow_array(<<'END', undef, $type, $name)
421             select name from sqlite_master where type = ? and name = ?
422             END
423              
424             or return undef;
425 0         0 $found;
426             }
427              
428             sub tables {
429 0     0 0 0 my MY $schema = shift;
430 0         0 keys %{$schema->{table_dict}};
  0         0  
431             }
432              
433             sub has_column {
434 0     0 0 0 (my MY $schema, my ($table, $column, $dbh)) = @_;
435 0   0     0 my $hash = $schema->columns_hash($table, $dbh || $schema->dbh);
436 0         0 exists $hash->{$column};
437             }
438              
439             sub columns_hash {
440 0     0 0 0 (my MY $schema, my ($table, $dbh)) = @_;
441 0   0     0 $dbh ||= $schema->dbh;
442 0         0 my $sth = $dbh->prepare("select * from $table limit 0");
443 0         0 $sth->execute;
444 0         0 my %hash = %{$sth->{NAME_hash}};
  0         0  
445 0         0 \%hash;
446             }
447              
448             sub drop {
449 0     0 0 0 (my MY $schema) = @_;
450 0         0 foreach my $sql ($schema->sql_drop) {
451 0         0 $schema->dbh->do($sql);
452             }
453             }
454              
455             #========================================
456              
457             sub _list_items {
458 48     48   141 (my MY $self, my $opts) = splice @_, 0, 2;
459             $opts->{raw} ? @_ : map {
460 48 100       180 my Item $item = $_;
  46         84  
461             $item->{cf_name}
462 46         496 } @_;
463             }
464              
465             sub list_tables {
466 20     20 0 114 (my MY $self, my %opts) = @_;
467             $self->_list_items(\%opts, grep {
468 44         86 my Table $tab = $_;
469             not $tab->{cf_view}
470 20         55 } @{$self->{table_list}});
  44         172  
  20         66  
471             }
472              
473             sub list_views {
474 11     11 0 43 (my MY $self, my %opts) = @_;
475             $self->_list_items(\%opts, grep {
476 26         43 my Table $tab = $_;
477             $tab->{cf_view}
478 11         28 } @{$self->{table_list}});
  26         67  
  11         34  
479             }
480              
481             sub list_relations {
482 17     17 0 64 (my MY $self, my ($tabName, %opts)) = @_;
483 17 50       72 my Table $tab = $self->{table_dict}{$tabName}
484             or return;
485 17 50       48 if ($opts{raw}) {
486 0         0 @{$tab->{relation_list}}
  0         0  
487             } else {
488             map {
489 18         56 (my ($relType, $relName, $fkName), my Table $subTab) = @$_;
490 18   66     56 $fkName //= do {
491 3 50 33     9 if (my Column $pk = $self->get_table_pk($subTab)
492             || $self->get_table_pk($tab)) {
493 3         15 $pk->{cf_name};
494             }
495             };
496 18         124 [$relType, $relName, $fkName, $subTab->{cf_name}];
497 17         35 } @{$tab->{relation_list}};
  17         54  
498             }
499             }
500              
501             sub list_table_columns {
502 7     7 0 42 (my MY $self, my ($tabName, %opts)) = @_;
503 7 50       31 my Table $tab = $self->{table_dict}{$tabName}
504             or return;
505 7         16 $self->_list_items(\%opts, @{$tab->{col_list}});
  7         27  
506             }
507              
508             sub get_table {
509 91     91 0 2814 (my MY $self, my $name) = @_;
510 91   66     387 $self->{table_dict}{$name} //= do {
511 33         65 push @{$self->{table_list}}
  33         216  
512             , my Table $tab = $self->Table->new(name => $name);
513 33         88 $tab->{not_configured} = 1;
514 33         131 $tab;
515             };
516             }
517              
518             sub get_table_pk {
519 23     23 0 69 (my MY $self, my ($tabName, %opts)) = @_;
520 23 50       61 my Table $tab = ref $tabName ? $tabName : $self->{table_dict}{$tabName};
521 23         53 my $pkinfo = $tab->{pk};
522 23 50       64 return unless $pkinfo;
523 23 100       49 if (wantarray) {
524 10 100       51 $self->_list_items(\%opts, ref $pkinfo eq 'ARRAY' ? @$pkinfo : $pkinfo);
525             } else {
526 13 100       53 ref $pkinfo eq 'ARRAY' ? $pkinfo->[0] : $pkinfo
527             }
528             }
529              
530             sub add_table {
531 33     33 0 68 my MY $self = shift;
532 33         109 my ($name, $opts, @colpairs) = @_;
533 33         100 my Table $tab = $self->get_table($name);
534 33 50       96 return $tab if @_ == 1;
535 33 50 33     162 if ($tab and not $tab->{not_configured}) {
536 0         0 croak "Duplicate definition of table $name";
537             }
538 33         72 delete $tab->{not_configured};
539 33         120 $self->extend_table(@_);
540             }
541              
542             sub extend_table {
543 35     35 0 1335 my MY $self = shift;
544 35         113 my ($name, $opts, @colpairs) = @_;
545 35         97 my Table $tab = $self->get_table($name);
546 35 100       122 $tab->configure(lhexpand($opts)) if $opts;
547 35         94 while (@colpairs) {
548             # colName => [colSpec]
549             # [check => args]
550 134 100       336 unless (ref $colpairs[0]) {
551 114         268 my ($col, $desc) = splice @colpairs, 0, 2;
552 114 100       418 $self->add_table_column($tab, $col, ref $desc ? @$desc : $desc);
553             } else {
554 20         36 my ($method, @args) = @{shift @colpairs};
  20         62  
555 20         55 $method =~ s/^-//;
556             # XXX: [has_many => @tables]
557 20 100       60 if (my ($relType, @relSpec) = $self->known_rels($method, undef, @args)) {
558 13         48 $self->add_table_relation($tab, undef, $relType => \@relSpec, @args);
559             } else {
560 7 50       52 my $sub = $self->can("add_table_\L$method")
561             or croak "Unknown table option '$method' for table $name";
562 7         22 $sub->($self, $tab, @args);
563             }
564             }
565             }
566              
567 35         135 $tab;
568             }
569              
570             sub add_table_primary_key {
571 3     3 0 11 (my MY $self, my Table $tab, my @args) = @_;
572 3 50 33     15 if ($tab->{pk} and @args) {
573 0         0 croak "Duplicate PK definition. old $tab->{pk}";
574             }
575 3         8 $tab->{pk} = [map {$tab->{col_dict}{$_}} @args];
  6         22  
576             }
577              
578             sub add_table_unique {
579 0     0 0 0 (my MY $self, my Table $tab, my @cols) = @_;
580             # XXX: 重複検査, 有無検査
581 0         0 push @{$tab->{chk_unique}}, [@cols];
  0         0  
582             }
583              
584             sub add_table_index {
585 0     0 0 0 (my MY $self, my Table $tab, my @cols) = @_;
586             # XXX: 重複検査, 有無検査
587 0         0 push @{$tab->{chk_index}}, [@cols];
  0         0  
588             }
589              
590             # -opt は引数無フラグ、又は [-opt, ...] として可変長オプションに使う
591             sub add_table_relation {
592 33     33 0 95 (my MY $self, my Table $tab, my Column $fkCol
593             , my ($relType, $relSpec, $item, $fkName, $atts)) = @_;
594 33 50       88 unless (defined $item) {
595 0         0 croak "Undefined relation spec for table $tab->{cf_name}";
596             }
597              
598             #
599             # [-has_many => 'table.key']
600             #
601 33 50 66     161 $fkName = $1 if not ref $item and $item =~ s/\.(\w+)$//;
602              
603 33 100       126 my Table $subTab = ref $item ? $self->add_table(@$item)
604             : $self->get_table($item);
605 33   66     148 my $relName = $relSpec->[0] // lc($subTab->{cf_name});
606             $fkName //= $relSpec->[1] // $fkCol->{cf_name}
607 33   66     210 // $subTab->{reference_dict}{$tab->{cf_name}};
      33        
      100        
608 33 50       108 if ($tab->{relation_dict}{$relName}) {
609 0         0 croak "Conflicting relation! $tab->{cf_name}.$relName";
610             }
611 33         223 push @{$tab->{relation_list}}
612 33         56 , $tab->{relation_dict}{$relName}
613             = [$relType => $relName, $fkName, $subTab];
614             }
615              
616             sub add_table_column {
617 114     114 0 291 (my MY $self, my Table $tab, my ($colName, $type, @colSpec)) = @_;
618 114 50       321 if ($tab->{col_dict}{$colName}) {
619 0         0 croak "Conflicting column name $colName for table $tab->{cf_name}";
620             }
621             # $tab.$colName is encoded by $refTab.pk
622 114 50       312 if (ref $type) {
    50          
623 0         0 croak "Deprecated column spec in $tab->{cf_name}.$colName";
624             } elsif (not defined $type) {
625 0         0 Carp::cluck "Column type $tab->{cf_name}.$colName is undef";
626             }
627              
628 114         211 my (@opt, @rels);
629 114         271 while (@colSpec) {
630 89 50       431 unless (defined (my $key = shift @colSpec)) {
    100          
    50          
631 0         0 croak "Undefined colum spec for $tab->{cf_name}.$colName";
632 0         0 } elsif (ref $key) {
633 20         59 my ($method, @args) = @$key;
634 20         51 $method =~ s/^-//;
635             # XXX: [has_many => @tables]
636             # XXX: [unique => k1, k2..]
637 20 50       69 if (my ($relType, @relSpec)
638             = $self->known_rels($method, $colName, @args)) {
639 20         101 push @rels, [$relType => \@relSpec, @args];
640             } else {
641 0         0 croak "Unknown method $method";
642             }
643 0         0 } elsif ($key =~ /^-/) {
644 69         228 push @opt, $key => 1;
645             } else {
646 0         0 push @opt, $key, shift @colSpec;
647             }
648             }
649 114         199 push @{$tab->{col_list}}, ($tab->{col_dict}{$colName})
  114         603  
650             = (my Column $col) = $self->Column->new
651             (@opt, name => $colName, type => $type);
652 114 100       377 $tab->{pk} = $col if $col->{cf_primary_key};
653              
654 114         298 $self->add_table_relation($tab, $col, @$_) for @rels;
655              
656             # XXX: Validation: name/option conflicts and others.
657 114         409 $col;
658             }
659              
660             sub add_table_values {
661 4     4 0 13 (my MY $self, my Table $tab, my ($colspec, @values)) = @_;
662 4         6 push @{$tab->{initializer}}, [$colspec, @values];
  4         22  
663             }
664              
665             sub verify_schema {
666 14     14 0 98 (my MY $self) = @_;
667 14         39 my @not_configured;
668 14         63 foreach my Table $tab (lexpand($self->{table_list})) {
669 33 50       93 if ($tab->{not_configured}) {
670 0         0 push @not_configured, $tab->{cf_name};
671 0         0 next;
672             }
673             # foreach my Column $col (lexpand($tab->{col_list})) { }
674             }
675 14 50       50 if (@not_configured) {
676 0         0 croak "Some tables are not configure, possibly spellmiss!: @not_configured";
677             }
678             }
679              
680             {
681             my %known_rels = qw(has_many 1 has_one 1 belongs_to 1
682             many_to_many 1 might_have 1
683             );
684             sub known_rels {
685 40     40 0 102 (my MY $self, my ($desc, $myColName, @args)) = @_;
686             # ['-has_many:rel:fk' => 'table']
687             # has_many ..fk is their_fk
688             # belongs_to ..fk is our_fk
689 40         142 my ($relType, $relName, $fkName) = split /:/, $desc, 3;
690 40 100       149 return unless $known_rels{$relType};
691 33   100     216 ($relType, $relName, $fkName || $myColName)
692             }
693             }
694              
695             #========================================
696              
697             sub sql_create {
698 3     3 0 24 (my MY $schema, my %opts) = @_;
699 3         16 $schema->foreach_tables_do('sql_create_table', \%opts)
700             }
701              
702 26     26 0 98 sub default_dbtype {'sqlite'}
703             sub sql_create_table {
704 26     26 0 71 (my MY $schema, my Table $tab, my $opts) = @_;
705 26         49 my (@cols, @indices);
706 26   33     132 my $dbtype = $opts->{dbtype} || $schema->default_dbtype;
707 26   33     164 my $sub = $schema->can($dbtype.'_sql_create_column')
708             || $schema->can('sql_create_column');
709              
710 26         51 my $pk_ok;
711 26         49 foreach my Column $col (@{$tab->{col_list}}) {
  26         83  
712 87 100       271 $pk_ok = 1 if $col->{cf_primary_key};
713 87         212 push @cols, $sub->($schema, $tab, $col, $opts);
714 87 100       279 push @indices, $col if $col->{cf_indexed};
715             }
716              
717             # Multi column primary key(...)
718             # XXX: conflict clause
719 26 50 66     86 if (not $pk_ok and $tab->{pk}) {
720             push @cols, "PRIMARY KEY(".join(", ", map {
721 6         14 my Column $col = $_;
722             $col->{cf_name}
723 3         10 } @{$tab->{pk}}).")";
  6         24  
  3         10  
724             }
725              
726             # Other unique(...)
727 26         135 foreach my $constraint (lexpand($tab->{chk_unique})) {
728 0         0 push @cols, sprintf q{unique(%s)}, join(", ", @$constraint);
729             }
730              
731             # XXX: SQLite specific.
732             # XXX: MySQL ENGINE(TYPE) = ...
733             push my @create
734             , sprintf qq{CREATE TABLE %s\n(%s)}, $tab->{cf_name}
735 26         174 , join "\n, ", @cols;
736              
737 26         66 foreach my Column $ix (@indices) {
738             push @create
739             , sprintf q{CREATE INDEX %1$s_%2$s on %1$s(%2$s)}
740 9         43 , $tab->{cf_name}, $ix->{cf_name};
741             }
742              
743 26         100 foreach my $colnames (lexpand($tab->{chk_index})) {
744 0         0 my $ixname = join "_", $tab->{cf_name}, @$colnames;
745             push @create, sprintf(q{CREATE INDEX %s on %s(%s)}
746             , $tab->{cf_name}
747 0         0 , join("_", $tab->{cf_name}, @$colnames)
748             , join(",", @$colnames));
749             }
750              
751             # after delete on user for each row begin
752 26 100       95 if (my $trigger = $tab->{cf_trigger_after_delete}) {
753             push @create, map {
754 1         4 qq{CREATE TRIGGER $_ AFTER DELETE ON $tab->{cf_name}}
755 1         7 . qq{ FOR EACH ROW } . $schema->sql_compound_trigger($trigger->{$_});
756             } keys %$trigger;
757             }
758              
759 26 50       131 wantarray ? @create : join(";\n", @create);
760             }
761              
762             # XXX: text => varchar(80)
763             sub map_coltype {
764 64     64 0 124 (my MY $schema, my $typeName) = @_;
765 64   33     469 $schema->{cf_coltype_map}{$typeName} // $typeName;
766             }
767              
768             sub sql_create_column {
769 64     64 0 131 (my MY $schema, my Table $tab, my Column $col, my $opts) = @_;
770             # XXX: primary key ASC/DESC
771             join(" ", $col->{cf_name}
772             , $schema->map_coltype($col->{cf_type})
773             , ($col->{cf_primary_key} ? "primary key" : ())
774             , ($col->{cf_unique} ? "unique" : ())
775 64 50       168 , ($col->{cf_autoincrement} ? "autoincrement" : ()));
    100          
    50          
776             }
777              
778             sub sqlite_sql_create_column {
779 87     87 0 176 (my MY $schema, my Table $tab, my Column $col, my $opts) = @_;
780 87 100       474 unless (defined $col->{cf_type}) {
    50          
781 0         0 croak "Column type is not yet defined! $tab->{cf_name}.$col->{cf_name}"
782 0 100       0 } elsif ($col->{cf_type} =~ /^int/i && $col->{cf_primary_key}) {
783 23         86 "$col->{cf_name} integer primary key"
784             } else {
785 64         172 $schema->sql_create_column($tab, $col, $opts);
786             }
787             }
788              
789             sub sql_compound_trigger {
790 1     1 0 3 (my MY $schema, my $item) = @_;
791 1 50       9 my $sub = $schema->can($$schema{dbtype}.'_sql_compound_trigger')
792             or croak "Compound trigger for $$schema{dbtype} is not yet implemented";
793 1         4 $sub->($schema, $item);
794             }
795              
796             sub mysql_sql_compound_trigger {
797 0     0 0 0 (my MY $schema, my $item) = @_;
798 0 0       0 unless (ref $item) {
    0          
799 0         0 $item
800 0         0 } elsif (@$item == 1) {
801 0         0 $item->[0]
802             } else {
803 0         0 "BEGIN ".join("; ", @$item). "; END";
804             }
805             }
806              
807             sub sqlite_sql_compound_trigger {
808 1     1 0 3 (my MY $schema, my $item) = @_;
809 1 50       8 "BEGIN ".join("; ", ref $item ? @$item : $item). "; END";
810             }
811              
812             sub sql_drop {
813             shift->foreach_tables_do
814             (sub {
815 0     0   0 (my Table $tab) = @_;
816 0         0 qq{drop table $tab->{cf_name}};
817             })
818 0     0 0 0 }
819              
820             sub foreach_tables_do {
821 3     3 0 12 (my MY $self, my $method, my $opts) = @_;
822             my $code = ref $method ? $method : sub {
823 5     5   29 $self->$method(@_);
824 3 50       25 };
825 3         11 my @result;
826 3         9 my $wantarray = wantarray;
827 3         7 foreach my Table $tab (@{$self->{table_list}}) {
  3         11  
828             push @result, map {
829 5 50       18 $wantarray ? $_ . "\n" : $_
  7         59  
830             } $code->($tab, $opts);
831             }
832 3 50       35 wantarray ? @result : join(";\n", @result);
833             }
834              
835             ########################################
836             # Below is poorman's CRUD closure generator(instead of ORM).
837              
838              
839             sub to_encode {
840 1     1 0 5 (my MY $self, my $tabName, my $keyCol, my @otherCols) = @_;
841              
842 1         3 my $to_find = $self->to_find($tabName, $keyCol);
843 1         4 my $to_ins = $self->to_insert($tabName, $keyCol, @otherCols);
844              
845             sub {
846 2     2   8 my ($value, @rest) = @_;
847 2 100       5 $to_find->($value) || $to_ins->($value, @rest);
848 1         5 };
849             }
850              
851             # to_fetchall は別途用意する
852             sub to_find {
853 2     2 0 13 (my MY $self, my ($tabName, $keyCol, $rowidCol)) = @_;
854 2         10 my $sql = $self->sql_to_find($tabName, $keyCol, $rowidCol);
855 2 50       9 print STDERR "-- $sql\n" if $self->{cf_verbose};
856 2         4 my $sth;
857             sub {
858 3     3   10 my ($value) = @_;
859 3   66     16 $sth ||= $self->dbh->prepare($sql);
860 3         213 $sth->execute($value);
861 3 100       44 my ($rowid) = $sth->fetchrow_array
862             or return;
863 2         16 $rowid;
864 2         13 };
865             }
866              
867             sub to_fetch {
868 2     2 0 12 (my MY $self, my ($tabName, $keyColList, $resColList, @rest)) = @_;
869 2         10 my $sql = $self->sql_to_fetch($tabName, $keyColList, $resColList, @rest);
870 2 50       10 print STDERR "-- $sql\n" if $self->{cf_verbose};
871 2         4 my $sth;
872             sub {
873 2     2   7 my (@value) = @_;
874 2   33     22 $sth ||= $self->dbh->prepare($sql);
875 2         212 $sth->execute(@value);
876 2         42 $sth;
877 2         17 };
878             }
879              
880             sub to_insert {
881 3     3 0 1942 (my MY $self, my ($tabName, @fields)) = @_;
882 3         14 my $sql = $self->sql_to_insert($tabName, @fields);
883 3 50       15 print STDERR "-- $sql\n" if $self->{cf_verbose};
884 3         7 my $sth;
885             sub {
886 4     4   24 my (@value) = @_;
887 4   66     25 $sth ||= $self->dbh->prepare($sql);
888             # print STDERR "-- inserting @value to $sql\n";
889 4         325 $sth->execute(@value);
890 4         23 $self->dbh->last_insert_id('', '', '', '');
891 3         19 };
892             }
893              
894             sub sql_to_find {
895 2     2 0 7 (my MY $self, my ($tabName, $keyCol, $rowidCol)) = @_;
896 2 50       11 my Table $tab = $self->{table_dict}{$tabName}
897             or croak "No such table: $tabName";
898             # XXX: col name check.
899 2   33     16 $rowidCol ||= $self->rowid_col($tab);
900 2         11 <
901             select $rowidCol from $tabName where $keyCol = ?
902             END
903             }
904              
905             sub sql_to_fetch {
906 4     4 0 16 (my MY $self, my ($tabName, $keyColList, $resColList, %opts)) = @_;
907 4         11 my $group_by = delete $opts{group_by};
908 4         10 my $order_by = delete $opts{order_by};
909 4 50       20 my Table $tab = $self->{table_dict}{$tabName}
910             or croak "No such table: $tabName";
911             # XXX: col name check... いや、式かもしれないし。
912 4 50       27 my $cols = $resColList ? join(", ", lexpand $resColList) : '*';
913 4         9 my $where = do {
914 4 0       17 unless (defined $keyColList) {
    0          
    50          
    50          
915 0         0 undef;
916 0         0 } elsif (not ref $keyColList) {
917 4         13 "$keyColList = ?"
918 0         0 } elsif (ref $keyColList eq 'ARRAY') {
919 0         0 join " AND ", map {"$_ = ?"} @$keyColList
  0         0  
920 0         0 } elsif (ref $keyColList eq 'SCALAR') {
921             # RAW SQL
922 0         0 $$keyColList;
923             } else {
924 0         0 die "Not yet implemented!";
925             }
926             };
927 4 50       14 if ($group_by) {
928 0         0 $where .= " GROUP BY $group_by";
929             }
930 4 100       14 if ($order_by) {
931 2         8 $where .= " ORDER BY $order_by";
932             }
933 4 50       26 qq|select $cols from $tabName| . (defined $where ? " where $where" : "");
934             }
935              
936              
937             sub sql_to_insert {
938 7     7 0 22 (my MY $self, my ($tabName, @fields)) = @_;
939             sprintf qq{INSERT INTO $tabName(%s) VALUES(%s)}
940             , join(", ", @fields)
941 7         31 , join(", ", map {'?'} @fields);
  12         56  
942             }
943              
944             sub sql_and_values_to_insert_expr {
945 0     0 0 0 (my MY $self, my ($tabName, $colNames, $valsOrExprs)) = @_;
946 0         0 my (@values);
947             my @exprs = map {
948 0 0       0 if (ref $_) {
  0         0  
949 0         0 $$_;
950             } else {
951 0         0 push @values, $_;
952 0         0 '?'
953             }
954             } @$valsOrExprs;
955 0         0 my $sql = sprintf qq{INSERT INTO $tabName(%s) VALUES(%s)}
956             , join(", ", @$colNames), join(", ", @exprs);
957              
958 0         0 ($sql, \@values);
959             }
960              
961              
962 0     0 0 0 sub default_rowid_col { 'rowid' }
963             sub rowid_col {
964 2     2 0 7 (my MY $schema, my Table $tab) = @_;
965 2 50       9 if (my Column $pk = $tab->{pk}) {
966             $pk->{cf_name}
967 2         12 } else {
968             # XXX: dbtype dispatch
969 0         0 $schema->default_rowid_col;
970             }
971             }
972              
973             ########################################
974              
975             sub add_inc {
976 15     15 0 34 my ($pack, $callpack) = @_;
977 15         64 $callpack =~ s{::}{/}g;
978 15         62 $INC{$callpack . '.pm'} = 1;
979             }
980              
981             ########################################
982              
983 3     3   2106 use YATT::Lite::XHF::Dumper;
  3         11  
  3         1745  
984              
985             sub cmd_deploy {
986 0     0 0 0 (my MY $schema) = @_;
987 0         0 local $schema->{cf_verbose} = 1;
988 0         0 my $dbh = $schema->dbh;
989 0         0 local $dbh->{AutoCommit};
990 0         0 $schema->ensure_created_on($dbh);
991 0         0 $dbh->commit;
992             }
993              
994             sub cmd_schema {
995 0     0 0 0 (my MY $schema) = @_;
996             print $schema->dump_xhf(map {
997 0         0 $schema->info_tableobj($_);
998 0         0 } @{$schema->{table_list}}), "\n";
  0         0  
999             }
1000              
1001             sub info_tableobj {
1002 0     0 0 0 (my MY $schema, my Table $tab) = @_;
1003             [$tab->{cf_name}, undef, map {
1004 0         0 $schema->info_columnobj($_);
1005 0         0 } @{$tab->{col_list}}];
  0         0  
1006             }
1007              
1008             sub info_columnobj {
1009 0     0 0 0 (my MY $schema, my Column $col) = @_;
1010 0         0 ($col->{cf_name}, $col->{cf_type});
1011             }
1012              
1013             sub cmd_help {
1014 0     0 0 0 my ($self) = @_;
1015 0   0     0 my $pack = ref($self) || $self;
1016 0         0 my @opts = do {
1017 0 0       0 if (my $sub = $pack->can('cf_list')) {
1018 0         0 $sub->($pack, qr{^cf_([a-z]\w*)});
1019             } else {
1020 0         0 ();
1021             }
1022             };
1023 0         0 require YATT::Lite::Util::FindMethods;
1024             my @methods = YATT::Lite::Util::FindMethods::FindMethods
1025 0     0   0 ($pack, , sub {s/^cmd_//});
  0         0  
1026 0         0 die <
1027 0         0 Usage: @{[basename($0)]} [--opt=value] [--opt=value] []
1028              
1029             Available commands are:
1030 0         0 @{[join("\n ", @methods)]}
1031              
1032             All options(might not usefull) are:
1033 0         0 @{[join "\n ", map {"--$_"} @opts]}
  0         0  
1034             END
1035              
1036             }
1037              
1038             #========================================
1039              
1040             sub ymd_hms {
1041 0     0 0 0 my ($pack, $time, $as_utc) = @_;
1042             my ($S, $M, $H, $d, $m, $y) = map {
1043 0 0       0 $as_utc ? gmtime($_) : localtime($_)
  0         0  
1044             } $time;
1045 0         0 sprintf q{%04d-%02d-%02d %02d:%02d:%02d}, 1900+$y, $m+1, $d, $H, $M, $S;
1046             }
1047              
1048             sub lhexpand {
1049 3 50   3 0 15 return unless defined $_[0];
1050 3         20 ref $_[0] eq 'HASH' ? %{$_[0]}
1051 3 0       28 : ref $_[0] eq 'ARRAY' ? @{$_[0]}
  0 50          
1052             : croak "Invalid option: $_[0]";
1053             }
1054              
1055 3     3   28 use YATT::Lite::Breakpoint ();
  3         6  
  3         105  
1056             YATT::Lite::Breakpoint::break_load_dbschema();
1057              
1058             1;