package Data::Model::Schema; use strict; use warnings; use Carp (); $Carp::Internal{(__PACKAGE__)}++; use Encode (); use Data::Model::Row; use Data::Model::Schema::Properties; my $SUGAR_MAP = +{}; our $COLUMN_SUGAR = +{}; sub import { my($class, %args) = @_; my $caller = caller; $SUGAR_MAP->{$caller} = $args{sugar} || 'default'; $COLUMN_SUGAR->{$SUGAR_MAP->{$caller}} ||= +{}; if ($caller eq 'Data::Model::Schema::Properties') { $args{skip_import}++; } unless ($args{skip_import}) { no strict 'refs'; for my $name (qw/ base_driver driver install_model schema column columns key index unique schema_options column_sugar utf8_column utf8_columns alias_column add_method /) { *{"$caller\::$name"} = \&$name; } } my $__properties = +{ base_driver => undef, schema => +{}, __process_tmp => +{ class => $caller, }, }; no strict 'refs'; no warnings 'redefine'; *{"$caller\::__properties"} = sub { $__properties }; } my $CALLER = undef; sub install_model ($$;%) { my($name, $schema_code, %args) = @_; my $caller = caller; my $pkg = "$caller\::$name"; my $schema = $caller->__properties->{schema}->{$name} = Data::Model::Schema::Properties->new( driver => $caller->__properties->{base_driver}, schema_class => $caller, model => $name, class => $pkg, column => {}, columns => [], index => {}, unique => {}, key => [], foreign => [], triggers => {}, options => {}, utf8_columns => {}, inflate_columns => [], deflate_columns => [], has_inflate => 0, has_deflate => 0, alias_column => {}, aluas_column_revers_map => {}, _build_tmp => {}, ); $caller->__properties->{__process_tmp}->{name} = $name; $CALLER = $caller; $schema_code->(); $schema->setup_inflate; unless ($schema->options->{bare_row}) { no strict 'refs'; @{"$pkg\::ISA"} = ( 'Data::Model::Row' ); _install_columns_to_class($schema); _install_alias_columns_to_class($schema); } $CALLER = undef; delete $caller->__properties->{__process_tmp}; if ($schema->driver) { $schema->driver->attach_model($name, $schema); } } sub schema (&) { shift } sub _install_columns_to_class { my $schema = shift; no strict 'refs'; while (my($column, $args) = each %{ $schema->column }) { my $alias_list = $schema->aluas_column_revers_map->{$column}; if ($alias_list) { *{ $schema->class . "::$column" } = sub { my $obj = shift; # getter return $obj->{column_values}->{$column} unless @_; # setter my($val, $flags) = @_; my $old_val = $obj->{column_values}->{$column}; $obj->{column_values}->{$column} = $val; unless ($flags && ref($flags) eq 'HASH' && $flags->{no_changed_flag}) { $obj->{changed_cols}->{$column} = $old_val; } for my $alias (@{ $alias_list }) { delete $obj->{alias_values}->{$alias}; } return $obj->{column_values}->{$column}; }; } else { *{ $schema->class . "::$column" } = sub { my $obj = shift; # getter return $obj->{column_values}->{$column} unless @_; # setter my($val, $flags) = @_; my $old_val = $obj->{column_values}->{$column}; $obj->{column_values}->{$column} = $val; unless ($flags && ref($flags) eq 'HASH' && $flags->{no_changed_flag}) { $obj->{changed_cols}->{$column} = $old_val; } return $obj->{column_values}->{$column}; }; } } } sub _install_alias_columns_to_class { my $schema = shift; no strict 'refs'; while (my($column, $args) = each %{ $schema->alias_column }) { my $base = $args->{base}; my $deflate_code = $args->{deflate}; my $is_utf8 = $args->{is_utf8}; my $charset = $args->{charset} || 'utf8'; my $inflate2alias = $args->{inflate2alias}; if ($is_utf8 && $deflate_code) { *{ $schema->class . "::$column" } = sub { my $obj = shift; # getter return $obj->{alias_values}->{$column} ||= $inflate2alias->($obj) unless @_; # setter $obj->{alias_values}->{$column} = $_[0]; $obj->$base( Encode::encode($charset, $deflate_code->( $_[0] ) ) ); return $_[0]; }; } elsif ($is_utf8) { *{ $schema->class . "::$column" } = sub { my $obj = shift; # getter return $obj->{alias_values}->{$column} ||= $inflate2alias->($obj) unless @_; # setter $obj->{alias_values}->{$column} = $_[0]; $obj->$base( Encode::encode($charset, $_[0]) ); return $_[0]; }; } elsif ($deflate_code) { *{ $schema->class . "::$column" } = sub { my $obj = shift; # getter return $obj->{alias_values}->{$column} ||= $inflate2alias->($obj) unless @_; # setter $obj->{alias_values}->{$column} = $_[0]; $obj->$base( $deflate_code->($_[0]) ); return $_[0]; }; } else { *{ $schema->class . "::$column" } = sub { my $obj = shift; # getter return $obj->{alias_values}->{$column} ||= $inflate2alias->($obj) unless @_; # setter $obj->{alias_values}->{$column} = $_[0]; $obj->$base( $_[0] ); return $_[0]; }; } } } sub _get_model_schema { if ($CALLER) { my $caller = caller(1); my $name = $caller->__properties->{__process_tmp}->{name}; return ($name, $caller->__properties->{schema}->{$name}); } my $method = (caller(1))[3]; $method =~ s/.+:://; Carp::croak "'$method' method is target internal only"; } sub base_driver ($) { my $caller = caller; return unless $caller->can('__properties'); $caller->__properties->{base_driver} = shift; } sub driver ($;%) { my($name, $schema) = _get_model_schema; my($driver, %args) = @_; $schema->driver($driver); } sub column ($;$;$) { my($name, $schema) = _get_model_schema; $schema->add_column(@_); } sub columns (@) { my($name, $schema) = _get_model_schema; my @columns = @_; for my $column (@columns) { $schema->add_column($column); } } sub utf8_column ($;$;$) { my($name, $schema) = _get_model_schema; $schema->add_utf8_column(@_); } sub utf8_columns (@) { my($name, $schema) = _get_model_schema; my @columns = @_; for my $column (@columns) { $schema->add_utf8_column($column); } } sub alias_column { my($name, $schema) = _get_model_schema; $schema->add_alias_column(@_); } sub key ($;%) { my($name, $schema) = _get_model_schema; $schema->add_keys(@_); } sub index ($;$;%) { my($name, $schema) = _get_model_schema; $schema->add_index(@_); } sub unique ($;$;%) { my($name, $schema) = _get_model_schema; $schema->add_unique(@_); } sub schema_options (@) { my($name, $schema) = _get_model_schema; $schema->add_options(@_); } sub add_method { my($name, $schema) = _get_model_schema; my($method, $code) = @_; no strict 'refs'; *{$schema->class."::$method"} = $code; } sub column_sugar (@) { my($column, $type, $options) = @_; Carp::croak "usage: add_column_sugar 'table_name.column_name' => type => { args };" unless $column =~ /^[^\.+]+\.[^\.+]+$/; my $caller = caller; $COLUMN_SUGAR->{$SUGAR_MAP->{$caller}} ||= +{}; $COLUMN_SUGAR->{$SUGAR_MAP->{$caller}}->{$column} = +{ type => $type || 'char', options => $options || +{}, }; } sub get_column_sugar { my($class, $schema) = @_; $COLUMN_SUGAR->{$SUGAR_MAP->{$schema->{schema_class}}}; } 1; __END__ =head1 NAME Data::Model::Schema - Schema DSL for Data::Model =head1 SYNOPSIS package Your::Model; use base 'Data::Model'; use Data::Model::Schema; use Data::Model::Driver::DBI; my $dbfile = '/foo/bar.db'; my $driver = Data::Model::Driver::DBI->new( dsn => "dbi:SQLite:dbname=$dbfile", ); base_driver( $driver ); # set the storage driver for Your::Model install_model tweet => schema { # CREATE TABLE tweet ( key 'id'; # primary key index index_name [qw/ user_id at /]; # index index_name(user_id, at); column id => int => { auto_increment => 1, required => 1, unsigned => 1, }; # id UNSIGNED INT NOT NULL AUTO_INCREMENT, column user_id => int => { required => 1, unsigned => 1, }; # user_id UNSIGNED INT NOT NULL, column at => int => { required => 1, default => sub { time() }, unsigned => 1, }; # at UNSIGNED INT NOT NULL, # If it is empty at the time of insert time() is used. utf8_column body # append to auto utf8 inflating => varchar => { required => 1, size => 140, default => '-', }; # body VARCHAR(140) NOT NULL DEFAULT'-', column field_name => char => { default => 'aaa', # default value auto_increment => 1, # auto_increment inflate => sub { unpack("H*", $_[0]) }, # inflating by original function deflate => sub { pack("H*", $_[0]) }, # deflating by original function }; column field_name_2 => char => { inflate => 'URI', # use URI inflate see L deflate => 'URI', # use URI deflate see L }; columns qw/ foo bar /; # create columns uses default config }; =head1 GLOBAL DSL =head2 install_model, schema model name and it schema is set up. install_model model_name schema { }; =head2 base_driver set driver ( Data::Model::Driver::* ) for current package's default. =head2 column_sugar column_sugar promotes reuse of a schema definition. see head1 COLUMN SUGAR =head1 SCHEMA DSL =head2 driver driver used only in install_model of current. install_model local_driver => schema { my $driver = Data::Mode::Driver::DBI->new( dsn => 'DBI:SQLite:' ); driver($driver); } =head2 column It is a column definition. column column_name => column_type => \%options; column_name puts in the column name of SQL schema. column_type puts in the column type of SQL schema. ( INT CHAR BLOB ... ) =head2 columns some columns are set up. However, options cannot be set. =head2 utf8_column column with utf8 inflated. =head2 utf8_columns columns with utf8 inflated. =head2 alias_column alias is attached to a specific column. It is helpful. I can use, when leaving original data and inflateing. { package Name; use Moose; has 'name' => ( is => 'rw' ); } # in schema columns qw( name nickname ); alias_column name => 'name_name'; alias_column nickname => 'nickname_name' => { inflate => sub { my $value = shift; Name->new( name => $value ); } # in your script is $row->nickname, $row->nickname_name->name; =head2 key set the primary key. Unless it specifies key, it does not move by lookup and lookup_multi. key 'id'; key [qw/ id sub_id /]; # multiple key =head2 index index 'name'; # index name(name); index name => [qw/ name name2 /]; # index name(name, name2) =head2 unique unique 'name'; # unique name(name); unique name => [qw/ name name2 /]; # unique name(name, name2) =head2 add_method A method is added to Row class which install_model created. add_method show_name => sub { my $row = shift; printf "Show %s\n", $row->name; }; $row->name('yappo'); $row->show_name; # print "Show yappo\n" =head2 schema_options some option to schema is added. It is used when using InnoDB in MySQL. schema_options create_sql_attributes => { mysql => 'TYPE=InnoDB', }; =head1 COLUMN OPTIONS The option which can be used in a column definition. Pasted the definition of ParamsValidate. It writes later. =head2 size size => { type => SCALAR, regex => qr/\A[0-9]+\z/, optional => 1, }, =head2 required required => { type => BOOLEAN, optional => 1, }, =head2 null null => { type => BOOLEAN, optional => 1, }, =head2 signed signed => { type => BOOLEAN, optional => 1, }, =head2 unsigned unsigned => { type => BOOLEAN, optional => 1, }, =head2 decimals decimals => { type => BOOLEAN, optional => 1, }, =head2 zerofill zerofill => { type => BOOLEAN, optional => 1, }, =head2 binary binary => { type => BOOLEAN, optional => 1, }, =head2 ascii ascii => { type => BOOLEAN, optional => 1, }, =head2 unicode unicode => { type => BOOLEAN, optional => 1, }, =head2 default default => { type => SCALAR | CODEREF, optional => 1, }, =head2 auto_increment auto_increment => { type => BOOLEAN, optional => 1, }, =head2 inflate inflate => { type => SCALAR | CODEREF, optional => 1, }, =head2 deflate deflate => { type => SCALAR | CODEREF, optional => 1, }, =head1 COLUMN SUGAR UNDOCUMENTED package Mock::ColumnSugar; use strict; use warnings; use base 'Data::Model'; use Data::Model::Schema sugar => 'column_sugar'; column_sugar 'author.id' => 'int' => +{ unsigned => 1, required => 1, # we can used to require or required }; column_sugar 'author.name' => 'varchar' => +{ size => 128, require => 1, }; column_sugar 'book.id' => 'int' => +{ unsigned => 1, require => 1, }; column_sugar 'book.title' => 'varchar' => +{ size => 255, require => 1, }; column_sugar 'book.description' => 'text' => +{ require => 1, default => 'not yet writing' }; column_sugar 'book.recommend' => 'text'; install_model author => schema { driver $main::DRIVER; key 'id'; column 'author.id' => { auto_increment => 1 }; # column name is id column 'author.name'; # column name is name }; install_model book => schema { driver $main::DRIVER; key 'id'; index 'author_id'; column 'book.id' => { auto_increment => 1 }; # column name is id column 'author.id'; # column name is author_id column 'author.id' => 'sub_author_id' => { required => 0 }; # column name is sub_author_id column 'book.title'; # column name is title column 'book.description'; # column name is description column 'book.recommend'; # column name is recommend }; =head1 AUTHOR Kazuhiro Osawa Eyappo shibuya plE =head1 LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut