File Coverage

lib/DBIx/EAV/Schema.pm
Criterion Covered Total %
statement 116 117 99.1
branch 42 50 84.0
condition 3 8 37.5
subroutine 20 20 100.0
pod 6 12 50.0
total 187 207 90.3


line stmt bran cond sub pod time code
1             package DBIx::EAV::Schema;
2              
3 10     10   34 use Moo;
  10         11  
  10         48  
4 10     10   1974 use Carp 'croak';
  10         12  
  10         394  
5 10     10   39 use Scalar::Util 'blessed';
  10         16  
  10         369  
6 10     10   2452 use DBIx::EAV::Table;
  10         18  
  10         228  
7 10     10   4743 use SQL::Translator;
  10         1606968  
  10         363  
8             use constant {
9             SQL_DEBUG => $ENV{DBIX_EAV_TRACE}
10 10     10   58 };
  10         11  
  10         17647  
11              
12             our $SCHEMA_VERSION = 1;
13              
14              
15             my %driver_to_producer = (
16             mysql => 'MySQL'
17             );
18              
19              
20             has 'dbh', is => 'ro', required => 1;
21              
22             has 'database_cascade_delete', is => 'ro', default => 1;
23             has 'table_prefix', is => 'ro', default => 'eav_';
24             has 'tenant_id', is => 'ro';
25             has 'data_types', is => 'ro', default => sub { [qw/ int decimal varchar text datetime bool /] };
26             has 'static_attributes', is => 'ro', default => sub { [] };
27             has 'id_type', is => 'ro', default => 'int';
28              
29             has 'translator', is => 'ro', init_arg => undef, lazy => 1, builder => 1;
30             has '_tables', is => 'ro', init_arg => undef, default => sub { {} };
31              
32              
33             sub BUILD {
34 12     12 0 122 my $self = shift;
35              
36             # enable sqlite fk for cascade delete to work
37 12 50       40 $self->dbh_do("PRAGMA foreign_keys = ON;")
38             if $self->db_driver_name eq 'SQLite';
39             }
40              
41              
42             sub _build_translator {
43 12     12   3199 my $self = shift;
44              
45 12         252 my $sqlt = SQL::Translator->new;
46 12         9279 $self->_build_sqlt_schema($sqlt->schema);
47              
48 12         192 $sqlt;
49             }
50              
51             sub _build_sqlt_schema {
52 12     12   16738 my ($self, $schema) = @_;
53              
54             my @schema = (
55              
56             entity_types => {
57             columns => ['id', $self->tenant_id ? 'tenant_id' : (), 'name:varchar:255'],
58             index => [$self->tenant_id ? 'tenant_id' : ()],
59             unique => {
60             name => [$self->tenant_id ? 'tenant_id' : (),'name']
61             }
62             },
63              
64             entities => {
65 12         413 columns => [qw/ id entity_type_id /, @{ $self->static_attributes } ],
66             fk => { entity_type_id => 'entity_types' }
67             },
68              
69             attributes => {
70             columns => [qw/ id entity_type_id name:varchar:255 data_type:varchar:64 /],
71             fk => { entity_type_id => 'entity_types' }
72             },
73              
74             relationships => {
75             columns => [qw/ id left_entity_type_id right_entity_type_id name:varchar:255 incoming_name:varchar:255 is_has_one:bool::0 is_has_many:bool::0 is_many_to_many:bool::0 /],
76             fk => { left_entity_type_id => 'entity_types', right_entity_type_id => 'entity_types' },
77             unique => {
78             name => ['left_entity_type_id','name']
79             }
80             },
81              
82             entity_relationships => {
83             columns => [qw/ relationship_id left_entity_id right_entity_id /],
84             pk => [qw/ relationship_id left_entity_id right_entity_id /],
85             fk => {
86             relationship_id => 'relationships',
87             left_entity_id => { table => 'entities', cascade_delete => $self->database_cascade_delete },
88             right_entity_id => { table => 'entities', cascade_delete => $self->database_cascade_delete },
89             }
90             },
91              
92             type_hierarchy => {
93             columns => [qw/ parent_type_id child_type_id /],
94             pk => [qw/ parent_type_id child_type_id /],
95             fk => {
96             parent_type_id => { table => 'entity_types', cascade_delete => $self->database_cascade_delete },
97             child_type_id => { table => 'entity_types', cascade_delete => $self->database_cascade_delete },
98             }
99             },
100              
101             map {
102 72         376 ("value_$_" => {
103             columns => [qw/ entity_id attribute_id /, 'value:'.$_],
104             fk => {
105             entity_id => { table => 'entities', cascade_delete => $self->database_cascade_delete },
106             attribute_id => 'attributes'
107             }
108             })
109 12 100       166 } @{ $self->data_types }
  12 100       33  
    100          
110             );
111              
112 12         65 for (my $i = 0; $i < @schema; $i += 2) {
113              
114             # add table
115 144         8563 my $table_name = $schema[$i];
116 144         186 my $table_schema = $schema[$i+1];
117 144 50       755 my $table = $schema->add_table( name => $self->table_prefix . $table_name )
118             or die $schema->error;
119              
120             # add columns
121 144         84518 foreach my $col ( @{ $table_schema->{columns} }) {
  144         374  
122              
123 488 50       307468 my $field_params = ref $col ? $col : do {
124              
125 488         1182 my ($name, $type, $size, $default) = split ':', $col;
126             +{
127 488         1500 name => $name,
128             data_type => $type,
129             size => $size,
130             default_value => $default
131             }
132             };
133              
134             $field_params->{data_type} = $self->id_type
135 488 100       2172 if $field_params->{name} =~ /(?:^id$|_id$)/;
136              
137             $field_params->{is_auto_increment} = 1
138 488 100       809 if $field_params->{name} eq 'id';
139              
140 488   50     1547 $field_params->{is_nullable} //= 0;
141              
142 488 50       1648 $table->add_field(%$field_params)
143             or die $table->error;
144             }
145              
146             # # primary key
147 144 100       147879 my $pk = $table->get_field('id') ? 'id' : $table_schema->{pk};
148 144 100       14074 $table->primary_key($pk) if $pk;
149              
150             # # foreign keys
151 144 100       120063 foreach my $fk_column (keys %{ $table_schema->{fk} || {} }) {
  144         626  
152              
153 252         70715 my $params = $table_schema->{fk}->{$fk_column};
154 252 100       625 $params = { table => $params } unless ref $params;
155              
156             $table->add_constraint(
157             name => join('_', 'fk', $table_name, $fk_column, $params->{table}),
158             type => 'foreign_key',
159             fields => $fk_column,
160             reference_fields => 'id',
161             reference_table => $self->table_prefix . $params->{table},
162 252 100       1592 on_delete => $params->{cascade_delete} ? 'CASCADE' : 'NO ACTION'
163             );
164             }
165              
166             # # unique constraints
167 144 100       79849 foreach my $name (keys %{ $table_schema->{unique} || {} }) {
  144         723  
168              
169             $table->add_index(
170             name => join('_', 'unique', $table_name, $name),
171             type => 'unique',
172 24         139 fields => $table_schema->{unique}{$name},
173             );
174             }
175              
176             # # index
177 144 100       12802 foreach my $colname (@{ $table_schema->{index} || [] }) {
  144         841  
178              
179 11         58 $table->add_index(
180             name => join('_', 'idx', $table_name, $colname),
181             type => 'normal',
182             fields => $colname,
183             );
184             }
185             }
186              
187 12         337 return 1;
188             }
189              
190              
191 26     26 0 177 sub version { $SCHEMA_VERSION }
192              
193             sub get_ddl {
194 13     13 1 78509 my ($self, $producer) = @_;
195              
196 13 100       48 unless ($producer) {
197              
198 12         173 my $driver = $self->dbh->{Driver}{Name};
199 12   33     113 $producer = $driver_to_producer{$driver} || $driver;
200             }
201              
202 13         323 $self->translator->producer($producer);
203 13         14128 $self->translator->translate;
204             }
205              
206             sub version_table {
207 25     25 0 806295 my $self = shift;
208              
209 25         619 DBIx::EAV::Table->new(
210             dbh => $self->dbh,
211             name => $self->table_prefix . 'schema_versions',
212             columns => [qw/ id version ddl /]
213             );
214             }
215              
216             sub version_table_is_installed {
217 14     14 0 73018 my $self = shift;
218              
219 14         26 my $success = 0;
220              
221 14         22 eval {
222 14         123 $self->dbh_do(sprintf 'SELECT COUNT(*) FROM %s', $self->table_prefix . 'schema_versions');
223 3         7 $success = 1;
224             };
225              
226 14         103 $success;
227             }
228              
229             sub install_version_table {
230 10     10 0 17 my $self = shift;
231              
232 10         260 my $sqlt = SQL::Translator->new;
233 10         7086 my $table = $sqlt->schema->add_table( name => $self->version_table->name );
234              
235 10         4991 $table->add_field(
236             name => 'id',
237             data_type => 'INTEGER',
238             is_auto_increment => 1
239             );
240              
241 10         6264 $table->add_field(
242             name => 'version',
243             data_type => 'INTEGER'
244             );
245              
246 10         7006 $table->add_field(
247             name => 'ddl',
248             data_type => 'TEXT'
249             );
250              
251 10         8289 $table->primary_key('id');
252              
253             # execute ddl
254 10         13469 my $driver = $self->dbh->{Driver}{Name};
255 10   33     246 $sqlt->producer($driver_to_producer{$driver} || $driver);
256              
257             $self->dbh_do($_)
258 10         72118 for grep { /\w/ } split ';', $sqlt->translate;
  40         78098  
259              
260             }
261              
262             sub installed_version {
263 2     2 0 5 my $self = shift;
264 2         7 my $table = $self->version_table;
265 2         10 my $row;
266 2         4 eval {
267 2         15 my ($rv, $sth) = $self->dbh_do(sprintf 'SELECT * FROM %s ORDER BY id DESC', $table->name);
268 2         58 $row = $sth->fetchrow_hashref;
269             };
270 2 50       27 return unless $row;
271 2         14 $row->{version};
272             }
273              
274             sub deploy {
275 12     12 1 33 my $self = shift;
276 12         45 my %options = ( @_, no_comments => 1 );
277              
278             $self->translator->$_($options{$_})
279 12         138 for keys %options;
280              
281             # deploy version table
282 12 100       8971 $self->install_version_table
283             unless $self->version_table_is_installed;
284              
285             # check we already installed this version
286 12         55 my $version_table = $self->version_table;
287 12 100       83 return if $version_table->select_one({ version => $self->version });
288              
289             # deploy ddl
290 11         132 my $ddl = $self->get_ddl;
291             $self->dbh_do($_)
292 11         744451 for grep { /\w/ } split ';', $ddl;
  209         335  
293              
294             # create version record
295 11         83 $version_table->insert({
296             version => $self->version,
297             ddl => 'DDL'
298             });
299             }
300              
301              
302             sub dbh_do {
303 368     368 1 4095 my ($self, $stmt, $bind) = @_;
304              
305 368         273 if (SQL_DEBUG) {
306             my $i = 0;
307             print STDERR "$stmt";
308             print STDERR $bind ? sprintf(": %s\n", join(' ', map { $i++.'='.$_ } @{ $bind || [] }))
309             : ";\n";
310             }
311              
312 368         2010 my $sth = $self->dbh->prepare($stmt);
313 368 50       42305 my $rv = $sth->execute(ref $bind eq 'ARRAY' ? @$bind : ());
314 357 50       823 die $sth->errstr unless defined $rv;
315              
316 357         3448 return ($rv, $sth);
317             }
318              
319             sub table {
320 1208     1208 1 34558 my ($self, $name) = @_;
321              
322             return $self->_tables->{$name}
323 1208 100       4348 if exists $self->_tables->{$name};
324              
325 82         1181 my $table_schema = $self->translator->schema->get_table($self->table_prefix . $name);
326              
327 82 50       5465 croak "Table '$name' does not exist."
328             unless $table_schema;
329              
330 82         15484 $self->_tables->{$name} = DBIx::EAV::Table->new(
331             dbh => $self->dbh,
332             tenant_id => $self->tenant_id,
333             name => $table_schema->name,
334             columns => [ $table_schema->field_names ]
335             );
336             }
337              
338             sub has_data_type {
339 113     113 1 20923 my ($self, $name) = @_;
340 113         93 foreach (@{$self->data_types}) {
  113         302  
341 337 100       762 return 1 if $_ eq $name;
342             }
343 0         0 0;
344             }
345              
346             sub db_driver_name {
347 26     26 1 1155 shift->dbh->{Driver}{Name};
348             }
349              
350              
351             1;
352              
353              
354             __END__