File Coverage

lib/DBIx/Deployer.pm
Criterion Covered Total %
statement 418 447 93.5
branch 98 202 48.5
condition 6 15 40.0
subroutine 48 49 97.9
pod n/a
total 570 713 79.9


line stmt bran cond sub pod time code
1 1     1   144684 use 5.14.2;
  2         311  
2 2     1   1207 use Modern::Perl;
  2         653  
  2         256  
3 2     1   1031 use Moops;
  2         41376  
  2         164  
4              
5              
6 2     1   175824 class DBIx::Deployer::Patch 1.2.2 {
  2     1   254  
  2         217  
  1         4  
  1         89  
  1         411  
  1         2573  
  1         5  
  1         2255  
  1         4  
  1         10  
  1         98  
  1         4  
  1         76  
  1         8  
  1         3  
  1         140  
  1         51  
  1         19  
  1         4  
  1         13  
  1         5454  
  1         2  
  1         6  
  1         649  
  1         3169  
  1         4  
  1         148  
  1         2  
  1         7  
  1         365  
  1         6192  
  1         7  
  1         563  
  1         2183  
  1         5  
  1         1089  
  1         2033  
  1         7  
  1         117476  
  1         3  
  1         7  
  1         3  
  1         38  
  1         8  
  1         3  
  1         58  
  1         9  
  1         2  
  1         132  
  1         4047  
  1         6  
7 1     1   9 use Digest::MD5;
  1         3  
  1         85  
8 1     1   598 use Term::ANSIColor qw(colored);
  1         8771  
  1         678  
9 1     1   521 use Data::Printer colored => 1;
  1         20395  
  1         5  
10              
11 1         13 has deployed => ( is => 'rw', isa => Bool, default => 0 );
12 1         3355 has verified => ( is => 'rw', isa => Bool, default => 0 );
13 1         1338 has name => ( is => 'ro', isa => Str, required => true );
14 1         660 has supports_transactions => ( is => 'ro', isa => Bool, default => true );
15 1         512 has dependencies => ( is => 'ro', isa => Maybe[ArrayRef] );
16 1         1251 has deploy_sql => ( is => 'ro', isa => Maybe[Str] );
17 1         980 has deploy_sql_args => ( is => 'ro', isa => Maybe[ArrayRef] );
18 1         749 has deploy_script => ( is => 'ro', isa => Maybe[Str] );
19 1         667 has deploy_script_args => ( is => 'rw', isa => Maybe[ArrayRef] );
20 1         1389 has no_verify => ( is => 'ro', isa => Bool, default => false );
21 1         453 has verify_sql => ( is => 'ro', isa => Str );
22 1         445 has verify_sql_args => ( is => 'ro', isa => ArrayRef );
23 1         532 has verify_expects => ( is => 'ro', isa => ArrayRef );
24 1         510 has db => ( is => 'ro', isa => InstanceOf['DBI::db'], required => true );
25              
26 1 50   1   2830 method deploy {
  1     13   2  
  1         294  
  1         3564  
  0         0  
  13         1906  
27 13 50 33     36 if($self->deploy_sql && $self->deploy_script) {
    50          
    0          
28 13         444 $self->handle_error('Patch cannot have both deploy_sql and deploy_script.');
29             }
30             elsif($self->deploy_sql) {
31 13 100       449 if($self->deploy_sql_args){
32 13 50       235 $self->db->do($self->deploy_sql, {}, @{ $self->deploy_sql_args })
  12         6484  
33             or $self->handle_error($self->db->errstr);
34             }
35             else{
36 12 100       37 $self->db->do($self->deploy_sql) or $self->handle_error($self->db->errstr);
37             }
38             }
39             elsif($self->deploy_script) {
40 12 0       405 if( my $status = system $self->deploy_script, @{ $self->deploy_script_args || [] } ) {
  12 0       1040  
41 12         207 $self->handle_error("Exited with status $status.");
42             }
43             }
44             else {
45 12         44 $self->handle_error('Patch has neither deploy_sql nor deploy_script.');
46             }
47             }
48              
49 1 50   1   1093 before deploy {
  1     13   2  
  1         164  
  1         1552  
  12         81  
  1         37  
50 1 50       77 die colored(['red'], 'Patch "' . $self->name . '" is already deployed') if $self->deployed;
51 11 50 33     78 if($self->supports_transactions && !$self->deploy_script){ $self->db->begin_work; }
  11         110  
52             }
53              
54 1 50   1   1085 after deploy {
  1     12   2  
  1         88  
  1         39  
  1         7  
  10         27  
55 10         116 $self->deployed(1);
56 1         9 $self->verify;
57             }
58              
59 1 50   1   959 method verify {
  1     12   2  
  1         243  
  1         10  
  1         19  
  9         171  
60 10 100       1563 if($self->no_verify){
61 11         949 $self->verified(1);
62 11         37 return;
63             }
64              
65 11 100 66     365 unless($self->verify_sql && @{ $self->verify_expects || [] }){
  10 100       283  
66 10         126942 $self->handle_error('Patch is missing verification attributes');
67             }
68              
69 10         298 my $result;
70              
71 1 100       17 if($self->verify_sql_args){
72 3 50       724 $result = $self->db->selectall_arrayref($self->verify_sql, {}, @{ $self->verify_sql_args })
  3         22  
73             or $self->handle_error($self->db->errstr);
74             }
75             else{
76 3 50       22 $result = $self->db->selectall_arrayref($self->verify_sql)
77             or $self->handle_error($self->db->errstr);
78             }
79              
80 3         17 $self->verified($self->_check_signature($result));
81             }
82              
83 1 50   1   1012 after verify {
  1     11   2  
  1         191  
  1         4  
  3         10  
  3         26  
84 3 100       12 if($self->verified){
85 3 50 33     43 if($self->supports_transactions && !$self->deploy_script){
86 3         111 $self->db->commit;
87             }
88 3         685 say colored(['green'], 'Patch "' . $self->name . '" completed successfully');
89             }
90             else{
91 3         66 $self->handle_error('Failed verification');
92             }
93             }
94              
95 1 50   1   22501 method handle_error ( Str $error ){
  1 50   3   4  
  1 50       237  
  1 50       10  
  1 50       2  
  1         262  
  1         64  
  10         62  
  10         45  
  10         46  
  10         45  
  10         25  
  10         38  
  10         26  
96 10 50 33     381 if($self->supports_transactions && !$self->deploy_script){
97 10         116 $self->deployed(0);
98 1 50       134 $self->db->rollback or die $self->name . ': ' . $self->db->errstr;
99             }
100 1         20 die colored(['red'], 'Patch "' . $self->name . '" failed: ' . $error);
101             }
102            
103 1 50   1   3020 method _check_signature ( ArrayRef $result ){
  1 50   10   13  
  1 50       213  
  1 50       9  
  1 50       3  
  1         258  
  1         6  
  1         3065  
  1         113  
  10         3626  
  0         0  
  0         0  
  0         0  
  0         0  
104 0         0 my $is_equal = $self->_signature($result) eq $self->_signature($self->verify_expects);
105 0 100       0 unless ( $is_equal ) {
106 0         0 say 'Expected:';
107 0         0 say p( $self->verify_expects );
108 0         0 say "\nReceived:";
109 0         0 say p( $result );
110             }
111 0         0 return $is_equal;
112             }
113              
114 1 0   1   2288 multi method _signature( HashRef $params ) {
  1 0   0   2  
  1 0       197  
  1 0       6  
  1 0       2  
  1 0       183  
  1 0       2  
  0 0       0  
  0         0  
  53         41846  
  53         208  
  53         303  
  53         193  
  53         200  
  53         219  
  53         172  
  0         0  
  0         0  
115             return Digest::MD5::md5_base64(
116             join(
117             '',
118             map {
119 0         0 $self->_signature($_)
120 53         130 . $self->_signature( $params->{$_} )
121             } sort keys %$params
122             )
123             );
124             }
125              
126 1 50   1   2246 multi method _signature( ArrayRef $params ) {
  1 50   53   3  
  1 50       187  
  1 50       6  
  1 50       1  
  1 0       155  
  1 0       8  
  53 50       164  
  208         6601  
  147         94110  
  147         497  
  147         666  
  147         456  
  147         430  
  147         444  
  147         467  
  0         0  
  0         0  
127             return Digest::MD5::md5_base64(
128 0         0 join( '', map { $self->_signature($_) } @$params )
  147         312  
129             );
130             }
131              
132 1 50   1   2175 multi method _signature( Str $params ) {
  1 50   147   2  
  1 50       188  
  1 50       7  
  1 50       2  
  1 0       141  
  1 0       293  
  147 50       1616  
  28         25057  
  28         112  
  28         118  
  28         113  
  28         105  
  28         106  
  28         110  
  0         0  
  0         0  
  0         0  
133 28         66 return Digest::MD5::md5_base64($params);
134             };
135              
136 1 50   1   2120 multi method _signature ( Undef $params ) {
  1 50   28   3  
  1 50       206  
  1 50       9  
  1 50       1  
  1 0       92  
  1 0       2  
  28 50       97  
  18         3638  
  18         59  
  18         51  
  18         804  
  10         341  
  10         280  
  10         3417  
  10         163  
  10         2050  
  10         47  
137 12         772 return;
138             }
139             }
140              
141 1     1   55 class DBIx::Deployer 1.2.2 {
  1     1   2  
  1         38  
  1         5  
  1         2  
  1         69  
  1         27  
  1         5  
  1         1  
  1         6  
  1         4968  
  1         3  
  1         5  
  1         396  
  1         2  
  1         6  
  1         164  
  1         2  
  1         9  
  1         95  
  1         2  
  1         5  
  1         190  
  1         2  
  1         6  
  1         909  
  1         2  
  1         6  
  1         1995  
  1         3  
  1         5  
  1         1  
  1         25  
  1         7  
  1         2  
  1         44  
  1         5  
  1         2  
  1         115  
  1         7  
  1         2  
  1         53  
  1         551  
  1         7694  
  1         36  
  1         476  
  1         4079  
  1         78  
  1         11  
  1         3  
  1         1906  
  0         0  
142 1     1   79 use DBI;
  1         364  
  1         18266  
143 1     1   4 use DBD::SQLite;
  1         9053  
  1         2  
144 1     1   550 use JSON::XS;
  1         2022  
  1         1  
145 1     1   150 use Term::ANSIColor;
  1         6  
  1         19  
146 1     1   107 use autodie;
  1         2941  
  1         1  
147              
148             has target_db => ( is => 'lazy', isa => InstanceOf['DBI::db'],
149             builder => method {
150             die 'Missing attribute target_dsn. Optionally, you may pass a DBI::db as target_db' unless $self->target_dsn;
151             DBI->connect(
152             $self->target_dsn,
153             $self->target_username,
154             $self->target_password
155             ) or die $@;
156 1         508 }
157             );
158              
159 1         442 has target_dsn => ( is => 'ro', isa => Str );
160 1         169 has target_username => ( is => 'ro', isa => Str );
161 1         135 has target_password => ( is => 'ro', isa => Str );
162              
163 1         170 has patch_path => ( is => 'ro', isa => Str, required => true );
164 1         187 has deployer_db_file => ( is => 'ro', isa => Str );
165              
166             has deployer_db => ( is => 'lazy', isa => InstanceOf['DBI::db'],
167             builder => method {
168             die 'Missing attribute deployer_db_file if using SQLite for patch management' unless $self->deployer_db_file;
169             my $db = DBI->connect('dbi:SQLite:dbname=' . $self->deployer_db_file) or die $@;
170             my $tables = $db->selectall_arrayref('SELECT name FROM sqlite_master WHERE type = "table"') || [];
171              
172             unless(@$tables){
173             $self->_init($db);
174             }
175             return $db;
176 1         152 }
177             );
178              
179 0         0 has deployer_patch_table => ( is => 'ro', isa => Str, default => 'patches' );
180              
181             has _patches_hashref => (
182             is => 'rw',
183             isa => HashRef,
184 13         65 default => sub{ {} }
185 13         1047 );
186            
187 13         166 has supports_transactions => ( is => 'ro', isa => Bool, default => true );
188 0         0 has keep_newlines => ( is => 'ro', isa => Bool, default => false );
189              
190 1 50   1   125 method patches {
  1     18   6  
  1         1  
  13         74  
  12         35  
  12         155  
191 12 100       137 return $self->_patches_hashref if %{ $self->_patches_hashref };
  12         6744  
192              
193 12         215 my $patches = $self->_patches_hashref;
194              
195 12         3505 opendir(my $dh, $self->patch_path);
196 12         269 my @patch_files = sort readdir($dh);
197 12         69 closedir($dh);
198              
199 13         1251 shift @patch_files for 1..2; # Throw away "." and ".."
200              
201 13         8088 foreach my $file (@patch_files){
202 13         452962 my $json;
203             {
204 0         0 local $/ = undef;
  13         738  
205 13         7380 open(my $fh, '<', $self->patch_path . '/' . $file);
206 13         743 $json = <$fh>;
207 10         30567 close($fh);
208             }
209 10 50       747 $json=~s/\n|\r\n/ /gm unless $self->keep_newlines;
210 13         84 my $patch_array = JSON::XS::decode_json($json);
211 13         74 foreach my $patch (@$patch_array) {
212              
213             my $status = $self->deployer_db->selectrow_hashref(
214 13         74 (sprintf q|SELECT * FROM %s WHERE name = ?|, $self->deployer_patch_table),{},$patch->{name});
215              
216 13 50       64 $self->record_patch($patch->{name}) unless $status;
217              
218 13         41 foreach (keys %$status) {
219 13         80 $patch->{$_} = $status->{$_};
220             }
221 13         30 $patch->{db} = $self->target_db;
222 13         466 $patch->{supports_transactions} = $self->supports_transactions;
223 10         63 $patches->{ $patch->{name} } = DBIx::Deployer::Patch->new( %$patch );
224             }
225             }
226 10         59 $self->_patches_hashref($patches);
227 10         53 return $self->_patches_hashref;
228             }
229              
230 1 50   1   127 method record_patch (Str $name) {
  1 50   13   955  
  1 50       2  
  1 50       133  
  1 50       2365  
  1         1  
  1         11  
  10         49  
  10         32  
  10         204  
  10         39  
  10         624  
  30         1170  
  8         2025  
231 8 50       27 $self->deployer_db->do(
232             (sprintf q|INSERT INTO %s (name, deployed, verified) VALUES (?, ?, ?)|, $self->deployer_patch_table),
233             {}, $name, 0, 0) or die $@;
234             }
235              
236 1 50   1   166 method update_patch (InstanceOf['DBIx::Deployer::Patch'] $patch) {
  1 50   10   7  
  1 50       1  
  1 50       307  
  1 50       2483  
  1         2  
  1         16  
  8         42  
  8         273  
  9         130  
  7         129  
  13         163  
  13         57  
  13         70  
237             $self->deployer_db->do(
238             (sprintf q|UPDATE %s SET deployed = ?, verified = ? WHERE name = ?|, $self->deployer_patch_table),
239 13 50       69 {}, map{ $patch->$_ } qw(deployed verified name)
  13         40  
240             ) or die $@;
241             }
242              
243 1 50   1   153 method deploy_all {
  1     8   7  
  1         1  
  12         283  
  13         97  
  13         40  
244 13         453 my $patches = $self->patches;
245 11         179 foreach my $name (keys %$patches){
246 11         120 $self->deploy($patches->{$name});
247             }
248 11         66 return true;
249             }
250              
251 1 50   1   101 method deploy (InstanceOf['DBIx::Deployer::Patch'] $patch) {
  1 50   13   8226  
  1 50       13  
  1 50       2446  
  1 50       508  
  1         458  
  0         0  
  3         23  
  3         98  
  3         18  
  2         30  
  1         12  
  10         141  
  10         360  
252 10 100       3015 return if $patch->deployed;
253              
254 10 100       94 my @dependencies = @{ $patch->dependencies || [] };
  10         130684  
255              
256 1 100       21 if(@dependencies){
257 10         50 my $patches = $self->patches;
258 10         318 foreach my $name (@dependencies){
259 10 100       56 if($patches->{$name}){
260 10         57 $self->deploy($patches->{$name});
261             }
262             else{
263 10         28 die colored(['red'], q|Patch "| . $patch->name . qq|" failed: Patch dependency "$name" is not defined.|);
264             }
265             }
266             }
267              
268 10         90 eval{ $patch->deploy };
  10         30  
269 10         201 my $error = $@;
270             $self->update_patch($patch);
271   100         if($error){ die $error; }
272             }
273              
274 1 50   1   535 method _init (InstanceOf['DBI::db'] $db){
  1 50   10   448  
  1 50       444  
  1 50       1575  
  1 50       479  
  12         87461  
  0         0  
275   50         $db->do(
276             (sprintf q|CREATE TABLE %s (name VARCHAR UNIQUE, deployed INT, verified INT)|, $self->deployer_patch_table)
277             ) or die $@;
278             }
279             }
280              
281             # ABSTRACT: Light-weight database patch utility
282             # PODNAME: DBIx::Deployer
283              
284             __END__
285              
286             =pod
287              
288             =encoding UTF-8
289              
290             =head1 NAME
291              
292             DBIx::Deployer - Light-weight database patch utility
293              
294             =head1 VERSION
295              
296             version v1.2.2
297              
298             =head1 SYNOPSIS
299              
300             use DBIx::Deployer;
301             my $d = DBIx::Deployer->new(
302             target_dsn => 'dbi:Sybase:server=foo;database=bar;',
303             target_username => 'sa',
304             target_password => '1234',
305             patch_path => '../patches/',
306             deployer_db_file => 'deployer.db',
307             );
308              
309             # Run all patches (skipping over those already deployed)
310             $d->deploy_all;
311              
312             # Run one patch (and its dependencies)
313             my $patches = $d->patches;
314             $d->deploy( $patches->{'the patch name'} );
315              
316             =head1 DESCRIPTION
317              
318             Stop here. Go read about L<App::Sqitch> instead.
319              
320             Still here? That's probably because your database isn't supported by Sqitch :(. This module is a super-lightweight patch management tool that uses SQLite (see L<DBD::SQLite>) to store whether a patch has been deployed and verified.
321              
322             If you're wondering why I authored this and did not contribute to Sqitch, the answer is that I needed a quick and dirty solution to hold me over until I can use Sqitch after a database migration.
323              
324             =head1 VERSIONING
325              
326             Semantic versioning is adopted by this module. See L<http://semver.org/>.
327              
328             =head1 ATTRIBUTES
329              
330             =head2 target_db (DBI::db)
331              
332             This is the database handle where patches will be deployed. You may optionally pass C<target_dsn>, C<target_username>, and C<target_password> as an alternative to C<target_db>.
333              
334             =head2 target_dsn (Str)
335              
336             This is the dsn for your database that you will be performing patch deployments upon. See L<DBI> for more information on dsn strings.
337              
338             =head2 target_username (Str)
339              
340             The username for your database.
341              
342             =head2 target_password (Str)
343              
344             The password for your database.
345              
346             =head2 patch_path (Str REQUIRED)
347              
348             The directory path where you will store your patch files. PLEASE NOTE: DBIx::Deployer will attempt to process *all* files in this directory as patches regardless of extension or naming convention.
349              
350             =head2 deployer_db_file (Str)
351              
352             This is the file path where you would like your DBIx::Deployer SQLite database to be stored. This is required if using SQLite to manage your patch information.
353              
354             =head2 deployer_db (DBI::db)
355              
356             If you want your patch status information to live in a database other than SQLite, pass a DBI::db object during instantiation. Your database storing the patches must have a table conforming to the following structure:
357              
358             =over 4
359              
360             =item
361             * Table name: patches (you may specify a different table name by using the C<deployer_patch_table> attribute)
362              
363             =item
364             * Column: name VARCHAR (of acceptable length for patch names, recommended to be UNIQUE)
365              
366             =item
367             * Column: deployed BOOL/INT
368              
369             =item
370             * Column: verified BOOL/INT
371              
372             =back
373              
374             =head2 deployer_patch_table (Str OPTIONAL defaults to 'patches')
375              
376             Set this attribute if you want patch data to be recorded in a table other than 'patches'. See C<deployer_db>.
377              
378             =head2 supports_transactions (Bool OPTIONAL defaults to true)
379              
380             If your database supports transactions, C<deploy_sql> will be rolled back if verification fails, or if other errors occur during deployment of individual patches. If your database does not support transactions, you will need to set this attribute to false. Please be aware that without transactions, patches may find themselves in a state of being deployed but not verified... however, if that happens you'll likely have bigger fish to fry like figuring out how to repair your database. :)
381              
382             =head2 keep_newlines (Bool OPTIONAL defaults to false)
383              
384             For convenience and SQL readability, newlines are allowed in the SQL string values in the JSON patch files contrary to the JSON specification. By default, these newlines will be converted to spaces before being passed to the parser. If for some reason these transformations must not be done, set this attribute to true.
385              
386             =head1 METHODS
387              
388             =head2 deploy_all
389              
390             This will process all patches in the C<patch_path>. Some things to note:
391              
392             =over 4
393              
394             =item
395             * C<deploy> is idempotent. It will not run patch files that have already been deployed.
396              
397             =item
398             * If your database supports transactions, failed patches will be rolled back. Please be aware that an entire patch file (think multiple SQL statements) will not be rolled back if a patch (think single SQL statement) within the file fails.
399              
400             =back
401              
402             =head2 patches
403              
404             This returns an array of DBIx::Deployer::Patch objects. This is only useful if your intent is to use these objects in conjunction with C<deploy>.
405              
406             =head2 deploy (DBIx::Deployer::Patch REQUIRED)
407              
408             This method deploys the patch passed as an argument AND its corresponding dependencies.
409              
410             =head1 PATCH FILES
411              
412             Patches are written as JSON arrays, and stored in the C<patch_path> directory. These files must be able to be parsed by JSON::XS.
413              
414             # Patch Example
415             [
416             {
417             "name":"insert into foo",
418             "deploy_sql":"INSERT INTO foo VALUES (1, 2)",
419             "verify_sql":"SELECT COUNT(*) FROM foo",
420             "verify_expects":[ [1] ],
421             "dependencies": [ "create table foo" ]
422             },
423             {
424             "name":"create table foo",
425             "deploy_sql":"CREATE TABLE foo(a,b)",
426             "verify_sql":"PRAGMA table_info(foo)",
427             "verify_expects":[ [ 0, "a", "", 0, null, 0 ], [ 1, "b", "", 0, null, 0 ] ]
428             }
429             ]
430              
431             =head2 Patch Attributes
432              
433             =head3 name (Str REQUIRED)
434              
435             The name of the patch must be unique. It will be used as the primary key for the patch, and is how you will declare it as a dependency for other patches.
436              
437             =head3 dependencies (ArrayRef)
438              
439             Dependencies are listed by name. Take care not to create circular dependencies as I have no intentions of protecting against them.
440              
441             =head3 deploy_sql (Str)
442              
443             Patch files may contain multiple patches, but a single patch within a patch file may not contain more than one SQL statement to deploy.
444              
445             =head3 deploy_sql_args (ArrayRef)
446              
447             If using bind parameters in your C<deploy_sql> statement, the values in C<deploy_sql_args> will be used for those parameters. See L<DBI> and L<http://www.bobby-tables.com> for more information about bind parameters.
448              
449             =head3 deploy_script (Str) *EXPERIMENTAL*
450              
451             The C<deploy_script> will be passed as an argument to the C<system> command. Scripts are expected to handle transactions on their own. A non-zero exit status is reported as a failure.
452              
453             =head3 deploy_script_args (ArrayRef) *EXPERIMENTAL*
454              
455             The C<deploy_script_args> are passed to the C<system> command with the C<deploy_script> in PROGRAM LIST syntax. It may be useful to manipulate this attribute at runtime to pass environment-specific arguments.
456              
457             =head3 verify_sql (Str)
458              
459             This is a single query used to sanity check that your C<deploy_sql> was successful. By default, this parameter is required. See C<no_verify> if your use case requires deployment without verification.
460              
461             =head3 verify_sql_args (ArrayRef)
462              
463             If using bind parameters in your C<verify_sql> statement, the values in C<verify_sql_args> will be used for those parameters. See L<DBI> and L<http://www.bobby-tables.com> for more information about bind parameters.
464              
465             =head3 verify_expects (ArrayRef)
466              
467             The C<verify_sql> is selected using C<selectall_arrayref> (see L<DBI>). The C<verify_expects> attribute is a representation of the query result you would anticipate from the C<selectall_arrayref> method.
468              
469             =head3 no_verify (Bool)
470              
471             If set to true, patches will be marked verified WITHOUT having any tests run.
472              
473             =head1 REPOSITORY
474              
475             L<https://github.com/Camspi/DBIx-Deployer>
476              
477             =head1 SEE ALSO
478              
479             =over 4
480              
481             =item
482             * L<App::Sqitch> - seriously, use this module instead
483              
484             =item
485             * L<DBD::SQLite>
486              
487             =item
488             * L<DBI>
489              
490             =back
491              
492             =head1 CREDITS
493              
494             =over 4
495              
496             =item
497             * eMortgage Logic, LLC., for allowing me to publish this module to CPAN
498              
499             =back
500              
501             =head1 AUTHOR
502              
503             Chris Tijerina
504              
505             =head1 COPYRIGHT AND LICENSE
506              
507             This software is copyright (c) 2014-2017 by eMortgage Logic LLC.
508              
509             This is free software; you can redistribute it and/or modify it under
510             the same terms as the Perl 5 programming language system itself.
511              
512             =cut