File: Introspector.pm

package info (click to toggle)
libdbix-introspector-perl 0.001005-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye, buster, stretch
  • size: 152 kB
  • ctags: 16
  • sloc: perl: 479; makefile: 2
file content (423 lines) | stat: -rw-r--r-- 12,603 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
package DBIx::Introspector;
$DBIx::Introspector::VERSION = '0.001005';
# ABSTRACT: Detect what database you are connected to

use Moo;
use DBIx::Introspector::Driver;

has _drivers => (
   is => 'ro',
   required => 1,
   init_arg => 'drivers',
   coerce => sub {
      return $_[0] if ref $_[0] eq 'ARRAY';
      return [ map DBIx::Introspector::Driver->new($_),
         {
            name => 'DBI',
            connected_determination_strategy => sub { $_[1]->{Driver}{Name} },
            unconnected_determination_strategy => sub {
               my $dsn = $_[1] || $ENV{DBI_DSN} || '';
               my ($driver) = $dsn =~ /dbi:([^:]+):/i;
               $driver ||= $ENV{DBI_DRIVER};
               return $driver
            },
         },
         { name => 'ACCESS',      parents => ['DBI'] },
         { name => 'DB2',         parents => ['DBI'] },
         { name => 'Informix',    parents => ['DBI'] },
         { name => 'InterBase',   parents => ['DBI'] },
         { name => 'MSSQL',       parents => ['DBI'] },
         { name => 'Oracle',      parents => ['DBI'] },
         { name => 'Pg',          parents => ['DBI'] },
         { name => 'SQLAnywhere', parents => ['DBI'] },
         { name => 'SQLite',      parents => ['DBI'] },
         { name => 'Sybase',      parents => ['DBI'] },
         { name => 'mysql',       parents => ['DBI'] },
         { name => 'Firebird::Common',    parents => ['Interbase'] },
         { name => 'Firebird',    parents => ['Interbase'] },
         {
            name => 'ODBC',
            connected_determination_strategy => sub {
               my $v = $_[0]->_get_info_from_dbh($_[1], 'SQL_DBMS_NAME');
               $v =~ s/\W/_/g;
               "ODBC_$v"
            },
            parents => ['DBI'],
         },
         { name => 'ODBC_ACCESS',               parents => ['ACCESS', 'ODBC'] },
         { name => 'ODBC_DB2_400_SQL',          parents => ['DB2', 'ODBC'] },
         { name => 'ODBC_Firebird',             parents => ['Firebird::Common', 'ODBC'] },
         { name => 'ODBC_Microsoft_SQL_Server', parents => ['MSSQL', 'ODBC'] },
         { name => 'ODBC_SQL_Anywhere',         parents => ['SQLAnywhere', 'ODBC'] },
         {
            name => 'ADO',
            connected_determination_strategy => sub {
               my $v = $_[0]->_get_info_from_dbh($_[1], 'SQL_DBMS_NAME');
               $v =~ s/\W/_/g;
               "ADO_$v"
            },
            parents => ['DBI'],
         },
         { name => 'ADO_MS_Jet',               parents => ['ACCESS', 'ADO'] },
         { name => 'ADO_Microsoft_SQL_Server', parents => ['MSSQL', 'ADO'] },
      ] if $_[0] eq '2013-12.01'
   },
);

sub _root_driver { shift->_drivers->[0] }

has _drivers_by_name => (
   is => 'ro',
   builder => sub { +{ map { $_->name => $_ } @{$_[0]->_drivers} } },
   clearer => '_clear_drivers_by_name',
   lazy => 1,
);

sub add_driver {
   my ($self, $driver) = @_;

   $self->_clear_drivers_by_name;
   # check for dupes?
   push @{$self->_drivers}, DBIx::Introspector::Driver->new($driver)
}

sub replace_driver {
   my ($self, $driver) = @_;

   $self->_clear_drivers_by_name;
   @{$self->_drivers} = (
      (grep $_ ne $driver->{name}, @{$self->_drivers}),
      DBIx::Introspector::Driver->new($driver)
   );
}

sub decorate_driver_unconnected {
   my ($self, $name, $key, $value) = @_;

   if (my $d = $self->_drivers_by_name->{$name}) {
      $d->_add_unconnected_option($key => $value)
   } else {
      die "no such driver <$name>"
   }
}

sub decorate_driver_connected {
   my ($self, $name, $key, $value) = @_;

   if (my $d = $self->_drivers_by_name->{$name}) {
      $d->_add_connected_option($key => $value)
   } else {
      die "no such driver <$name>"
   }
}

sub get {
   my ($self, $dbh, $dsn, $key, $opt) = @_;
   $opt ||= {};

   my @args = (
      drivers_by_name => $self->_drivers_by_name,
      key => $key
   );

   if ($dbh and my $driver = $self->_driver_for((ref $dbh eq 'CODE' ? $dbh->() : $dbh), $dsn)) {
      my $ret = $driver
         ->_get_when_connected({
            dbh => $dbh,
            dsn => $dsn,
            @args,
         });
      return $ret if defined $ret;
      $ret = $driver
         ->_get_when_unconnected({
            dsn => $dsn,
            @args,
         });
      return $ret if defined $ret;
   }

   my $dsn_ret = $self->_driver_for($dbh, $dsn)
      ->_get_when_unconnected({
         dsn => $dsn,
         @args,
      }) if $dsn;
   return $dsn_ret if defined $dsn_ret;

   if (ref $dbh eq 'CODE' && ref $opt->{dbh_fallback_connect} eq 'CODE') {
      $opt->{dbh_fallback_connect}->();
      my $dbh = $dbh->();
      return $self->_driver_for($dbh, $dsn)
         ->_get_when_connected({
            dbh => $dbh,
            dsn => $dsn,
            @args,
         })
   }

   die "missing key: $key"
}

sub _driver_for {
   my ($self, $dbh, $dsn) = @_;

   if ($dbh and my $d = $dbh->{private_dbii_driver}) {
      if (my $found = $self->_drivers_by_name->{$d}) {
         return $found
      } else {
         warn "user requested non-existant driver $d"
      }
   }

   my $driver = $self->_root_driver;
   my $done;

   DETECT:
   do {
      $done = $driver->_determine($dbh, $dsn);
      if (!defined $done) {
         die "cannot figure out wtf this is"
      } elsif ($done ne 1) {
         $driver = $self->_drivers_by_name->{$done}
            or die "no such driver <$done>"
      }
   } while $done ne 1;

   return $driver
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

DBIx::Introspector - Detect what database you are connected to

=head1 VERSION

version 0.001005

=head1 SYNOPSIS

 my $d = DBIx::Introspector->new(drivers => '2013-12.01');

 # standard dialects
 $d->decorate_driver_unconnected(Pg     => concat_sql => '? || ?');
 $d->decorate_driver_unconnected(SQLite => concat_sql => '? || ?');

 # non-standard
 $d->decorate_driver_unconnected(MSSQL  => concat_sql => '? + ?');
 $d->decorate_driver_unconnected(mysql  => concat_sql => 'CONCAT( ?, ? )');

 my $concat_sql = $d->get($dbh, $dsn, 'concat_sql');

=head1 DESCRIPTION

C<DBIx::Introspector> is a module factored out of the L<DBIx::Class> database
detection code.  Most code that needs to detect which database it is connected
to assumes that there is a one-to-one mapping from database drivers to database
engines.  Unfortunately reality is rarely that simple.  For instance,
L<DBD::ODBC> is typically used to connect to SQL Server, but ODBC can be used to
connect to PostgreSQL, MySQL, and Oracle.  Additionally, while ODBC is the most
common way to connect to SQL Server, it is not the only option, as L<DBD::ADO>
can also be used.

C<DBIx::Introspector> can correctly detect which database you are connected to,
because it was factored out of a complex, working codebase.  On top of
that it has been written to be very extensible.  So if you needed to
detect which version of your given database you are connected to that
would not be difficult.

Furthermore, C<DBIx::Introspector> does its best to try to detect information
based on the dsn you give it if you have not yet connected, so you can possibly
avoid connection or at least defer connection.

=head1 METHODS

=head2 C<add_driver>

 $dbii->add_driver({
   name => 'Pg',
   parents => ['DBI'],
   unconnected_options => {
      concat_sql => '? || ?',
      random_func => 'RANDOM()',
   })

Takes a hashref L<< defining a new driver | DRIVER DEFINITION >>.

=head2 C<replace_driver>

 $dbii->replace_driver({
   name => 'Pg',
   parents => ['DBI'],
   unconnected_options => {
      concat_sql => '? || ?',
      random_func => 'RANDOM()',
   })

Takes a hashref L<< replacing an existing driver | DRIVER DEFINITION >>.
Replaces the driver already defined with the same name.

=head2 C<decorate_driver_connected>

 $dbii->decorate_driver_connected('MSSQL', 'concat_sql', '? + ?')

Takes a C<driver name>, C<key> and a C<value>.  The C<key value> pair will
be inserted into the driver's C<connected_options>.

=head2 C<decorate_driver_unconnected>

 $dbii->decorate_driver_unconnected('SQLite', 'concat_sql', '? || ?')

Takes a C<driver name>, C<key> and a C<value>.  The C<key value> pair will
be inserted into the driver's C<unconnected_options>.

=head2 C<get>

 $dbii->get($dbh, $dsn, 'concat_sql')

Takes a C<dbh>, C<dsn>, C<key>, and optionally a hashref of C<options>.

The C<dbh> can be a coderef returning a C<dbh>.  If you provide the
C<dbh_fallback_connect> option it will be used to connect the C<dbh> if it is
not already connected and then queried, if the C<dsn> was insufficient.

So for example, one might do:

 my $dbh;
 $dbii->get(sub { $dbh }, $dsn, 'concat_sql', {
    dbh_fallback_connect => sub { $dbh = DBI->connect($dsn, $user, $pass) },
 });

Which will only connect if it has to, like if the user is using the C<DBD::ODBC>
driver to connect.

=head1 ATTRIBUTES

=head2 C<drivers>

This has no default and is required, though a sane defaultish value does exist.

Currently there is one predefined set of drivers, named C<2013-12.01>.
If drivers or facts or just the general structure of drivers changes they
will always be as a new named set of drivers.  C<2013-12.01> matches
the 0.08250 release of L<DBIx::Class> and probably many previous and
following releases.

If you need to define it from scratch, you can just pass an arrayref of drivers;
see the L<DRIVER DEFINITION> section on what is required for that.  But
generally it will look something like this (from the tests):

 my $d = DBIx::Introspector->new(
   drivers => [ map DBIx::Introspector::Driver->new($_),
      {
         name => 'DBI',
         connected_determination_strategy => sub { $_[1]->{Driver}{Name} },
         unconnected_determination_strategy => sub {
            my $dsn = $_[1] || $ENV{DBI_DSN} || '';
            my ($driver) = $dsn =~ /dbi:([^:]+):/i;
            $driver ||= $ENV{DBI_DRIVER};
            return $driver
         },
      },
      {
         name => 'SQLite',
         parents => ['DBI'],
         connected_determination_strategy => sub {
            my ($v) = $_[1]->selectrow_array('SELECT "value" FROM "a"');
            return "SQLite$v"
         },
         connected_options => {
            bar => sub { 2 },
         },
         unconnected_options => {
            borg => sub { 'magic ham' },
         },
      },
      { name => 'SQLite1', parents => ['SQLite'] },
      { name => 'SQLite2', parents => ['SQLite'] },
   ]
 );

=head1 DRIVER DEFINITION

Drivers (C<DBIx::Introspector::Driver> objects) have the following six
attributes:

=head2 C<name>

Required.  Must be unique among the drivers contained in the introspector.

=head2 C<parents>

Arrayref of parent drivers.  This allows parent drivers to implement common
options among children.  So for example on might define a driver for each
version of PostgreSQL, and have a parent driver that they all use for common
base info.

=head2 C<connected_determination_strategy>

This is a code reference that is called as a method on the driver with the
C<dbh> as the first argument and an optional C<dsn> as the second argument.
It should return a driver name.

=head2 C<unconnected_determination_strategy>

This is a code reference that is called as a method on the driver with the
C<dsn> as the first argument.  It should return a driver name.

=head2 C<connected_options>

Hashref of C<< key value >> pairs for detecting information based on the
C<dbh>.  A value that is not a code reference is returned directly, though
I suggest non-coderefs all go in the L</unconnected_options> so that they may be
used without connecting if possilbe.

If a code reference is passed it will get called as a method on the driver
with the following list of values:

=over 2

=item C<dbh>

This is the connected C<dbh> that you can use to introspect the database.

=item C<dsn>

This is the C<dsn> passed to L</get>, possibly undefined.

=back

=head2 C<unconnected_options>

Hashref of C<< key value >> pairs for detecting information based on the
C<dsn>.  A value that is not a code reference is returned directly.

If a code reference is passed it will get called as a method on the driver
with the following list value:

=over 2

=item C<dsn>

This is the connected C<dsn> that you can use to introspect the database.

=back

=head1 AUTHOR

Arthur Axel "fREW" Schmidt <frioux+cpan@gmail.com>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2015 by Arthur Axel "fREW" Schmidt.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut