File Coverage

blib/lib/EntityModel/Storage/PostgreSQL.pm
Criterion Covered Total %
statement 3 3 100.0
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 4 4 100.0


line stmt bran cond sub pod time code
1             package EntityModel::Storage::PostgreSQL;
2             use EntityModel::Class {
3 1         19 _isa => [qw{EntityModel::Storage}],
4             db => { type => 'EntityModel::DB' },
5             schema => { type => 'string' },
6             field_cache => { type => 'hash' },
7             primary_cache => { type => 'array' },
8 1     1   1689 };
  1         81959  
9              
10             our $VERSION = '0.003';
11              
12             =head1 NAME
13              
14             EntityModel::Storage::PostgreSQL - backend storage interface for L
15              
16             =head1 SYNOPSIS
17              
18             See L.
19              
20             =head1 DESCRIPTION
21              
22             See L.
23              
24             =cut
25              
26             use Scalar::Util ();
27              
28             =head1 METHODS
29              
30             =cut
31              
32             =head2 setup
33              
34             Open the initial database connection and set schema if provided.
35              
36             =cut
37              
38             sub setup {
39             my $self = shift;
40             my %args = %{+shift};
41              
42             # If we have a DB object already, just use that.
43             $self->db(delete $args{db}) if exists $args{db};
44             $self->schema(delete $args{schema}) if exists $args{schema};
45              
46             # Otherwise we'll need to pick one up from the parameters
47             $self->db(EntityModel::DB->new(
48             user => delete $args{user},
49             password => delete $args{password},
50             host => delete $args{host},
51             port => delete $args{port},
52             dbname => delete $args{dbname}
53             )) unless $self->db;
54              
55             # Without a database, we can't do much, bail out here
56             die "no db" unless $self->db;
57             return $self;
58             }
59              
60             =head2 apply_model
61              
62             Applies the requested model to the database.
63              
64             =cut
65              
66             sub apply_model {
67             my $self = shift;
68             my $model = shift;
69             logDebug("Apply model");
70             Scalar::Util::weaken $self;
71             Scalar::Util::weaken $model;
72             $model->transaction(sub {
73             $self->apply_model_and_schema($model);
74             });
75             }
76              
77             =head2 apply_model_and_schema
78              
79             Given a model, apply it to the database, optionally creating the requested schema.
80              
81             =cut
82              
83             sub apply_model_and_schema {
84             my $self = shift;
85             my $model = shift;
86             $self->create_schema if $self->schema && !$self->schema_exists;
87              
88             my @pending = $model->entity->list;
89             my @existing;
90              
91             ITEM:
92             while(@pending) {
93             my $entity = shift(@pending);
94              
95             my @deps = $entity->dependencies;
96             my @pendingNames = map { $_->name } @pending;
97              
98             # Include current entity in list of available entries, so that we can allow self-reference
99             foreach my $dep (@deps) {
100             unless(grep { $dep->name ~~ $_->name } @pending, @existing, $entity) {
101             logError("%s unresolved (pending %s, deps %s for %s)", $dep->name, join(',', @pendingNames), join(',', @deps), $entity->name);
102             die "Dependency error";
103             }
104             }
105              
106             my @unsatisfied = grep { $_ ~~ [ map { $_->name } @deps ] } @pendingNames;
107             if(@unsatisfied) {
108             logInfo("%s has %d unsatisfied deps, postponing: %s", $entity->name, scalar @unsatisfied, join(',',@unsatisfied));
109             push @pending, $entity;
110             next ITEM;
111             }
112              
113             $self->apply_entity($entity);
114             push @existing, $entity;
115             }
116             return $self;
117             }
118              
119             =head2 apply_entity
120              
121             Applies this entity to the database - currently, supports creation only.
122              
123             =cut
124              
125             sub apply_entity {
126             my $self = shift;
127             my $entity = shift;
128             my ($sql, @bind) = $self->create_table_query($entity);
129              
130             my $sth = $self->dbh->prepare($sql);
131             $sth->execute(@bind);
132             $self->field_cache->clear;
133             $self->primary_cache->clear;
134             return $self;
135             }
136              
137             =head2 read_primary
138              
139             Get the primary keys for a table.
140              
141             =cut
142              
143             sub read_primary {
144             my $self = shift;
145             my $tbl = shift;
146             logDebug("Get primary key info for [%s]", $tbl->name);
147             $self->_cache_primary if $self->primary_cache->is_empty;
148              
149             # Reorder the result according to the reported sequence
150             my @keyList = map {
151             $_->{name}
152             } sort {
153             $a->{order} <=> $b->{order}
154             } grep {
155             $_->{table} eq $tbl->name
156             } $self->primary_cache->list;
157             logDebug("Keys were: [%s]", join(',', @keyList));
158             return @keyList;
159             }
160              
161             =head2 read_fields
162              
163             Read all fields for a given table.
164              
165             Since this is typically a slow query, we cache the entire set of fields for all tables on
166             the first call.
167              
168             =cut
169              
170             sub read_fields {
171             my $self = shift;
172             my $tbl = shift;
173              
174             $self->_cache_fields unless $self->{field_cache};
175             my $field_list = $self->field_cache->get($tbl->name);
176             unless($field_list) {
177             logDebug("No items for [%s]", $tbl->name);
178             return;
179             }
180             logDebug("Check [%s] has %d items: %s", $tbl->name, $field_list->count, $field_list->join(','));
181             return map +{ %$_ }, $field_list->list;
182             }
183              
184             =head1 _cache_fields
185              
186             Cache field information across all tables in the currently-selected database.
187              
188             =cut
189              
190             sub _cache_fields {
191             my $self = shift;
192             logInfo("Reloading cache");
193             # Get all field for all tables
194             my $sth = $self->dbh->column_info(undef, $self->schema, '%', '%');
195             my $rslt = $sth->fetchall_arrayref
196             or return $self;
197             my %field_cache;
198             foreach (@$rslt) {
199             # We get a load of data back from DBI, most of which isn't useful yet
200             my (undef, undef, $tableName, $name, $type, $size, $length, $digits, $radix, $nullable, $default, $dataType, $datetimeSub, $octetLength, $order, $isNullable) = @$_;
201             $name =~ s/"//g;
202             $tableName =~ s/"//g;
203             logDebug("Have [%s] field [%s]", $tableName, $name);
204             $field_cache{$tableName} = EntityModel::Array->new([ ]) unless $field_cache{$tableName};
205             $field_cache{$tableName}->push({
206             'name' => $name,
207             'default' => $default,
208             'null' => $isNullable,
209             'type' => $type,
210             'length' => $size,
211             'precision' => $digits,
212             });
213             }
214             $self->{field_cache} = \%field_cache;
215             $self->_cache_primary;
216             return $self;
217             }
218              
219             =head1 _cache_primary
220              
221             Cache primary key information across all tables in the database.
222              
223             =cut
224              
225             sub _cache_primary {
226             my $self = shift;
227             logInfo("Reloading primary cache");
228              
229             # XXX PostgreSQL only code here, because DBI default was teh slow.
230             my $sql = q{
231             select '' as "something",
232             n.nspname as "schema",
233             c.relname as "table",
234             a.attname as "column",
235             a.attnum as "order",
236             c2.relname as "keyname"
237             from pg_catalog.pg_class c
238             inner join pg_catalog.pg_index i on (i.indrelid = c.oid)
239             inner join pg_catalog.pg_class c2 on (c2.oid = i.indexrelid)
240             inner join pg_catalog.pg_attribute a on a.attrelid = c.oid and a.attnum = any(i.indkey)
241             inner join pg_catalog.pg_type t2 on a.atttypid = t2.oid
242             left join pg_catalog.pg_namespace n on (n.oid = c.relnamespace)
243             left join pg_catalog.pg_tablespace t on (t.oid = c.reltablespace)
244             where i.indisprimary is true
245             and n.nspname = ?
246             order by 1,2,4
247             };
248             my $sth = $self->dbh->prepare($sql);
249             logDebug("Run $sql");
250             $sth->execute($self->schema);
251             # Get all tables
252             # my $sth = $self->dbh->primary_key_info(undef, $self->schema, $tbl->name);
253             my $rslt = $sth->fetchall_arrayref
254             or return $self;
255              
256             my @keyList;
257             foreach (@$rslt) {
258             my (undef, undef, $tableName, $name, $order, $constraint) = @$_;
259             push @keyList, {
260             table => $tableName,
261             name => $name,
262             order => $order,
263             constraint => $constraint
264             };
265             }
266             logDebug("Had %d entries", scalar @keyList);
267             $self->{primary_cache} = \@keyList;
268             return $self;
269             }
270              
271             =head2 table_list
272              
273             Get a list of all the existing tables in the schema.
274              
275             =cut
276              
277             sub table_list {
278             my $self = shift;
279             my $q = EntityModel::Query->new(
280             select => 'table_name',
281             from => 'information_schema.tables',
282             where => [
283             table_type => 'BASE TABLE',
284             table_schema => $self->schema,
285             ],
286             );
287             return $q->results;
288             }
289              
290             =head2 field_list
291              
292             Returns a list of all fields for the given table.
293              
294             =cut
295              
296             sub field_list {
297             my $self = shift;
298             my $tbl = shift;
299             my $schema = $self->schema;
300              
301             my $q = EntityModel::Query->new(
302             select => [
303             { name => 'column_name' },
304             { default => 'column_default' },
305             { null => 'is_nullable' },
306             { type => 'data_type' },
307             { length => 'character_maximum_length' },
308             { description => \q{''} },
309             { precision => 'numeric_precision' },
310             { scale => 'numeric_scale' },
311             ],
312             from => { schema => 'information_schema', table => 'columns' },
313             where => [
314             table_schema => $schema,
315             -and => table_name => $tbl,
316             ],
317             order => 'ordinal_position'
318             );
319             return $q->results;
320             }
321              
322             =head2 quoted_schema_name
323              
324             Returns the quoted version of the current schema.
325              
326             =cut
327              
328             sub quoted_schema_name {
329             my $self = shift;
330             return undef unless $self->schema;
331              
332             return $self->dbh->quote_identifier($self->schema);
333             }
334              
335             =head1 quoted_table_name
336              
337             Generate the quoted table identifier including any schema name if available.
338              
339             =cut
340              
341             sub quoted_table_name {
342             my $self = shift;
343             my $tbl = shift;
344             return $self->dbh->quote_identifier(undef, $self->schema, $tbl->name);
345             }
346              
347             =head2 quoted_field_name
348              
349             Generate the quoted field identifier.
350              
351             =cut
352              
353             sub quoted_field_name {
354             my $self = shift;
355             my $field = shift;
356             return $self->dbh->quote_identifier(undef, undef, $field->name);
357             }
358              
359             =head2 create_table_query
360              
361             Create a new table.
362              
363             =cut
364              
365             sub create_table_query {
366             my $self = shift;
367             my $tbl = shift;
368              
369             my @bind;
370             # Put together the constituent fields
371             my $content = join(', ', map {
372             $self->quoted_field_name($_) . ' ' . $_->type . ($tbl->primary eq $_->name ? ' primary key' : '')
373             } $tbl->field->list);
374              
375             # TODO Any extras such as index or constraints
376              
377             # And build the create statement itself
378             my $sql = 'create table ' . $self->quoted_table_name($tbl) . ' (' . $content . ')';
379             return ($sql, @bind);
380             }
381              
382             =head1 remove_table_query
383              
384             Query for removing the given table.
385              
386             =cut
387              
388             sub remove_table_query {
389             my $self = shift;
390             my $tbl = shift;
391              
392             my @bind;
393             my $sql = 'drop table ' . $self->quoted_table_name($tbl);
394             return ($sql, @bind);
395             }
396              
397             =head1 create_table
398              
399             Create the given table.
400              
401             =cut
402              
403             sub create_table {
404             my ($self, $tbl) = @_;
405             my ($sql, @bind) = $self->create_table_query($tbl);
406             my $sth = $self->dbh->prepare($sql);
407             $sth->execute(@bind);
408             $self->field_cache(undef);
409             $self->primary_cache(undef);
410             return $self->SUPER::create_table($tbl);
411             }
412              
413             =head2 add_field_to_table
414              
415             Add the requested field to the given table, and clear related caches.
416              
417             =cut
418              
419             sub add_field_to_table {
420             my $self = shift;
421             my $entity = shift;
422             my $field = shift;
423              
424             my ($sql, @bind) = $self->alter_table_query(
425             table => $entity,
426             add => [ $field ]
427             );
428             my $sth = $self->dbh->prepare($sql);
429             logDebug($sql);
430             $sth->execute(@bind);
431             $self->field_cache(undef);
432             $self->primary_cache(undef);
433             return $self->SUPER::add_field_to_table($entity, $field);
434             }
435              
436             =head2 remove_table
437              
438             Remove a table entirely.
439              
440             =cut
441              
442             sub remove_table {
443             my $self = shift;
444             my $tbl = shift;
445              
446             my ($sql, @bind) = $self->remove_table_query($tbl);
447             my $sth = $self->dbh->prepare($sql);
448             logInfo($sql);
449             $sth->execute(@bind);
450             $self->field_cache(undef);
451             $self->primary_cache(undef);
452             return $self;
453             }
454              
455             =head2 read_tables
456              
457             Read all table definitions from the database.
458              
459             =cut
460              
461             sub read_tables {
462             my $self = shift;
463             die 'no schema' unless $self->schema;
464             logWarning("Get tables for " . $self->schema);
465              
466             delete $self->{field_cache};
467             $self->primary_cache(undef);
468             my $sth = $self->dbh->table_info(undef, $self->schema, '%');
469             my $rslt = $sth->fetchall_arrayref
470             or return $self;
471             my @table_list;
472             foreach (@$rslt) {
473             my (undef, undef, $name, $type) = @$_;
474             $name =~ s/^"//;
475             $name =~ s/"$//;
476             push @table_list, { name => $name } if lc($type) eq 'table';
477             }
478             return @table_list;
479             }
480              
481             =head2 post_commit
482              
483             =cut
484              
485             sub post_commit {
486             my $self = shift;
487             $self->dbh->commit;
488             return $self;
489             }
490              
491             =head2 create_schema
492              
493             =cut
494              
495             sub create_schema {
496             my $self = shift;
497             try {
498             $self->db->transaction(sub {
499             my $dbh = shift->dbh;
500             $dbh->do('create schema ' . $self->quoted_schema_name);
501             });
502             } catch {
503             logWarning($_);
504             };
505             return $self;
506             }
507              
508             =head1 remove_schema
509              
510             Remove the schema entry.
511              
512             =cut
513              
514             sub remove_schema {
515             my $self = shift;
516             die "No schema" unless $self->schema;
517             try {
518             $self->db->transaction(sub {
519             my $dbh = shift->dbh;
520             $dbh->do('drop schema ' . $self->quoted_schema_name . ' cascade');
521             });
522             } catch {
523             logWarning($_);
524             };
525             return $self;
526             }
527              
528             =head2 schema_exists
529              
530             Returns true if the current schema exists in the database, false if not.
531              
532             =cut
533              
534             sub schema_exists {
535             my $self = shift;
536             my $sth = $self->dbh->prepare(q{select schema_name, catalog_name from information_schema.schemata where schema_name = ?});
537             $sth->execute($self->schema);
538             my $rslt = $sth->fetchall_arrayref
539             or return undef;
540             return scalar @$rslt;
541             }
542              
543             =head2 row_count
544              
545             Reports how many rows are in the given table.
546              
547             =cut
548              
549             sub row_count {
550             my $self = shift;
551             my $tbl = shift;
552             die 'not yet implemented';
553             }
554              
555             =head2 find
556              
557             Find entries.
558              
559             =cut
560              
561             sub find {
562             my $self = shift;
563             my $tbl = shift;
564             my $spec = shift;
565             die 'not yet implemented';
566             }
567              
568             =head2 create
569              
570             Creates a new instance for the given entity.
571              
572             =cut
573              
574             sub create {
575             my $self = shift;
576             my %args = @_;
577             logError("Creating entity [%s] with [%s]", $args{entity}, $args{data});
578             my $q = EntityModel::Query->new(
579             db => $self->db,
580             'insert into' => $self->quoted_table_name($args{entity}),
581             values => $args{data},
582             returning => [ $args{entity}->primary ]
583             );
584             my ($rslt) = $q->results;
585             return $rslt->{$args{entity}->primary};
586             }
587              
588             =head2 store
589              
590             Update the database with current in-memory values for the given entity instance.
591              
592             =cut
593              
594             sub store {
595             my $self = shift;
596             my %args = @_;
597             logError("Creating entity [%s] with [%s]", $args{entity}, $args{data});
598             my $q = EntityModel::Query->new(
599             db => $self->db,
600             'update' => $self->quoted_table_name($args{entity}),
601             fields => $args{data},
602             where => [ $args{entity}->primary => $args{id} ]
603             );
604             my $rslt = $q->results;
605             return $rslt;
606             }
607              
608             =head2 read
609              
610             Read information for the requested entity instance.
611              
612             =cut
613              
614             sub read {
615             my $self = shift;
616             my %args = @_;
617             logDebug("Reading entity [%s] id [%s]", $args{entity}, $args{id});
618             my $q = EntityModel::Query->new(
619             db => $self->db,
620             'select' => [ map { $self->quoted_field_name($_) } $args{entity}->field->list ],
621             'from' => $self->quoted_table_name($args{entity}),
622             where => [ $args{entity}->primary => $args{id} ],
623             limit => 1
624             );
625             my ($rslt) = $q->results;
626             logError($rslt);
627             return $rslt;
628             }
629              
630             =head2 dbh
631              
632             Returns a database handle for this storage backend.
633              
634             =cut
635              
636             sub dbh {
637             my $self = shift;
638             return $self->db->dbh(@_);
639             }
640              
641             1;
642              
643             __END__