File Coverage

blib/lib/YATT/Lite/WebMVC0/DBSchema.pm
Criterion Covered Total %
statement 33 529 6.2
branch 0 254 0.0
condition 0 101 0.0
subroutine 11 100 11.0
pod 1 80 1.2
total 45 1064 4.2


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