File Coverage

lib/DBIx/EAV/Schema.pm
Criterion Covered Total %
statement 18 117 15.3
branch 0 50 0.0
condition 0 8 0.0
subroutine 6 20 30.0
pod 6 12 50.0
total 30 207 14.4


line stmt bran cond sub pod time code
1             package DBIx::EAV::Schema;
2              
3 10     10   31 use Moo;
  10         12  
  10         43  
4 10     10   1829 use Carp 'croak';
  10         17  
  10         494  
5 10     10   36 use Scalar::Util 'blessed';
  10         15  
  10         331  
6 10     10   2576 use DBIx::EAV::Table;
  10         16  
  10         231  
7 10     10   4793 use SQL::Translator;
  10         1586367  
  10         344  
8             use constant {
9             SQL_DEBUG => $ENV{DBIX_EAV_TRACE}
10 10     10   62 };
  10         15  
  10         13096  
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 0     0 0   my $self = shift;
35              
36             # enable sqlite fk for cascade delete to work
37 0 0         $self->dbh_do("PRAGMA foreign_keys = ON;")
38             if $self->db_driver_name eq 'SQLite';
39             }
40              
41              
42             sub _build_translator {
43 0     0     my $self = shift;
44              
45 0           my $sqlt = SQL::Translator->new;
46 0           $self->_build_sqlt_schema($sqlt->schema);
47              
48 0           $sqlt;
49             }
50              
51             sub _build_sqlt_schema {
52 0     0     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 0           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 0           ("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 0 0         } @{ $self->data_types }
  0 0          
    0          
110             );
111              
112 0           for (my $i = 0; $i < @schema; $i += 2) {
113              
114             # add table
115 0           my $table_name = $schema[$i];
116 0           my $table_schema = $schema[$i+1];
117 0 0         my $table = $schema->add_table( name => $self->table_prefix . $table_name )
118             or die $schema->error;
119              
120             # add columns
121 0           foreach my $col ( @{ $table_schema->{columns} }) {
  0            
122              
123 0 0         my $field_params = ref $col ? $col : do {
124              
125 0           my ($name, $type, $size, $default) = split ':', $col;
126             +{
127 0           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 0 0         if $field_params->{name} =~ /(?:^id$|_id$)/;
136              
137             $field_params->{is_auto_increment} = 1
138 0 0         if $field_params->{name} eq 'id';
139              
140 0   0       $field_params->{is_nullable} //= 0;
141              
142 0 0         $table->add_field(%$field_params)
143             or die $table->error;
144             }
145              
146             # # primary key
147 0 0         my $pk = $table->get_field('id') ? 'id' : $table_schema->{pk};
148 0 0         $table->primary_key($pk) if $pk;
149              
150             # # foreign keys
151 0 0         foreach my $fk_column (keys %{ $table_schema->{fk} || {} }) {
  0            
152              
153 0           my $params = $table_schema->{fk}->{$fk_column};
154 0 0         $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 0 0         on_delete => $params->{cascade_delete} ? 'CASCADE' : 'NO ACTION'
163             );
164             }
165              
166             # # unique constraints
167 0 0         foreach my $name (keys %{ $table_schema->{unique} || {} }) {
  0            
168              
169             $table->add_index(
170             name => join('_', 'unique', $table_name, $name),
171             type => 'unique',
172 0           fields => $table_schema->{unique}{$name},
173             );
174             }
175              
176             # # index
177 0 0         foreach my $colname (@{ $table_schema->{index} || [] }) {
  0            
178              
179 0           $table->add_index(
180             name => join('_', 'idx', $table_name, $colname),
181             type => 'normal',
182             fields => $colname,
183             );
184             }
185             }
186              
187 0           return 1;
188             }
189              
190              
191 0     0 0   sub version { $SCHEMA_VERSION }
192              
193             sub get_ddl {
194 0     0 1   my ($self, $producer) = @_;
195              
196 0 0         unless ($producer) {
197              
198 0           my $driver = $self->dbh->{Driver}{Name};
199 0   0       $producer = $driver_to_producer{$driver} || $driver;
200             }
201              
202 0           $self->translator->producer($producer);
203 0           $self->translator->translate;
204             }
205              
206             sub version_table {
207 0     0 0   my $self = shift;
208              
209 0           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 0     0 0   my $self = shift;
218              
219 0           my $success = 0;
220              
221 0           eval {
222 0           $self->dbh_do(sprintf 'SELECT COUNT(*) FROM %s', $self->table_prefix . 'schema_versions');
223 0           $success = 1;
224             };
225              
226 0           $success;
227             }
228              
229             sub install_version_table {
230 0     0 0   my $self = shift;
231              
232 0           my $sqlt = SQL::Translator->new;
233 0           my $table = $sqlt->schema->add_table( name => $self->version_table->name );
234              
235 0           $table->add_field(
236             name => 'id',
237             data_type => 'INTEGER',
238             is_auto_increment => 1
239             );
240              
241 0           $table->add_field(
242             name => 'version',
243             data_type => 'INTEGER'
244             );
245              
246 0           $table->add_field(
247             name => 'ddl',
248             data_type => 'TEXT'
249             );
250              
251 0           $table->primary_key('id');
252              
253             # execute ddl
254 0           my $driver = $self->dbh->{Driver}{Name};
255 0   0       $sqlt->producer($driver_to_producer{$driver} || $driver);
256              
257             $self->dbh_do($_)
258 0           for grep { /\w/ } split ';', $sqlt->translate;
  0            
259              
260             }
261              
262             sub installed_version {
263 0     0 0   my $self = shift;
264 0           my $table = $self->version_table;
265 0           my $row;
266 0           eval {
267 0           my ($rv, $sth) = $self->dbh_do(sprintf 'SELECT * FROM %s ORDER BY id DESC', $table->name);
268 0           $row = $sth->fetchrow_hashref;
269             };
270 0 0         return unless $row;
271 0           $row->{version};
272             }
273              
274             sub deploy {
275 0     0 1   my $self = shift;
276 0           my %options = ( @_, no_comments => 1 );
277              
278             $self->translator->$_($options{$_})
279 0           for keys %options;
280              
281             # deploy version table
282 0 0         $self->install_version_table
283             unless $self->version_table_is_installed;
284              
285             # check we already installed this version
286 0           my $version_table = $self->version_table;
287 0 0         return if $version_table->select_one({ version => $self->version });
288              
289             # deploy ddl
290 0           my $ddl = $self->get_ddl;
291             $self->dbh_do($_)
292 0           for grep { /\w/ } split ';', $ddl;
  0            
293              
294             # create version record
295 0           $version_table->insert({
296             version => $self->version,
297             ddl => 'DDL'
298             });
299             }
300              
301              
302             sub dbh_do {
303 0     0 1   my ($self, $stmt, $bind) = @_;
304              
305 0           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 0           my $sth = $self->dbh->prepare($stmt);
313 0 0         my $rv = $sth->execute(ref $bind eq 'ARRAY' ? @$bind : ());
314 0 0         die $sth->errstr unless defined $rv;
315              
316 0           return ($rv, $sth);
317             }
318              
319             sub table {
320 0     0 1   my ($self, $name) = @_;
321              
322             return $self->_tables->{$name}
323 0 0         if exists $self->_tables->{$name};
324              
325 0           my $table_schema = $self->translator->schema->get_table($self->table_prefix . $name);
326              
327 0 0         croak "Table '$name' does not exist."
328             unless $table_schema;
329              
330 0           $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 0     0 1   my ($self, $name) = @_;
340 0           foreach (@{$self->data_types}) {
  0            
341 0 0         return 1 if $_ eq $name;
342             }
343 0           0;
344             }
345              
346             sub db_driver_name {
347 0     0 1   shift->dbh->{Driver}{Name};
348             }
349              
350              
351             1;
352              
353              
354             __END__