<commit_before>#!/usr/bin/env perl

=head1 NAME

Data::Object - Store Perl objects in a MySql (or Oracle) database

=head1 VERSION

This document refers to version 1.2 of Data::Object, released Jul 07, 2015.
Download Data::Object.pm from http://www.legendum.com/perl/Data::Object.pm

=head1 SYNOPSIS

First create a table Foo in a MySql database Bar:

    create table Foo
    (
        id        INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        name        VARCHAR(255) NOT NULL,
        email        VARCHAR(255),
        telephone    VARCHAR(255),
        timestamp    TIMESTAMP
    );

Then create a Perl program to access table Foo in database Bar as objects:

    package Foo;
    @ISA = qw(Data::Object);
    use Data::Object;

    package main;
    Foo->connect(database=>'Bar', user=>'fred', password=>'<PASSWORD>');
    Foo->fields( qw(id name email telephone) );
    my $foo = Foo->new(name=>'kev', email=>'<EMAIL>');
    $foo->insert();
    $foo->{telephone} = '+44 207 359 0985';
    $foo->update();
    $foo = Foo->select('name=?', 'kev');
    print $foo->{name} . ' has email address ' . $foo->{email} . '\n';
    $foo->delete();
    Foo->disconnect();

=head1 DESCRIPTION

Data::Object stores objects in a database. It should be inherited by a class
wishing to persist its objects in the database. The inheriting class should
correspond to a table of the same name (case-sensitive) in the database. This
table must have an auto_increment column as its primary key.

Data::Object provides database connectivity via connect() and disconnect(),
object creation via methods new(), select() and next(), and object/database
syncronisation via methods insert(), update() and delete().

Data::Object does not check whether objects were modified in the database while
they were being modified in your Perl program. Nor does it support any locking
mechanisms. Basically it's designed to be fast, simple, easy and predictable.

To use Data::Object with an Oracle database, set the ORACLE_HOME database
environment variable. When using Oracle on Sun Solaris, you should also set the
LD_PRELOAD to "/lib/libthread.so.1" to enable thread support. When you connect,
you should add "driver=>'Oracle'" to the argument list. When setting and getting
object field values, use the Data::Object AUTOLOAD method because Oracle fields
have uppercase names. For example, to set blah to be 2, use "$obj->blah(2)".

=cut
package Data::Object;
$VERSION = "1.2";

use strict;
use Carp;
use DBI;
{
    # Class static references to hashes keyed by class/table name

    my $_Connections = {};  # Database connections, keyed by class
    my $_Drivers = {};      # Database drivers, keyed by class
    my $_Cache = {};        # Database connection cache, keyed by database

    my $_IdField = {};      # Primary key for uniquely identifying objects
    my $_Fields  = {};      # Fields to be syncronised with the database
    my $_Queries = {};      # User-defined queries for select() and next()

    my $_Selects = {};      # Standard select queries for row()
    my $_Inserts = {};      # Standard insert queries for insert()/update()
    my $_Updates = {};      # Standard update queries for update()
    my $_Deletes = {};      # Standard delete queries for delete()

    # Error messages

    my $_ERROR_FIELDS = "call class static method fields() first";

=head2 Class Methods

=over 4

=item connect(driver=>'mysql', host=>'localhost', database=>'dbname', user=>'username', password=>'<PASSWORD>', print_error=>0, raise_error=>1, fields=>[qw(id_field fields1 field2 field3)])

Connect to the database with optional connection details, and return the DBI
connection. The database driver always defaults to "mysql" unless environment
variable $DRIVER has been set. The database, user and password will default
to environment variables $DATABASE, $USER and $PASSWORD. If a field list is
not supplied, then a separate call to fields() must be made after connecting.

=cut
sub connect
{
    my ($self, %args) = @_;
    my $class = ref($self) || $self;

    # Get the connection details

    my $driver      = $args{driver}      || $ENV{DRIVER} || 'mysql';
    my $server      = $args{server}      || $ENV{SERVER} || 'unknown';
    my $host        = $args{host}        || $ENV{HOST}   || 'localhost';
    my $database    = $args{database}    || $ENV{DB_DATABASE};
    my $user        = $args{user}        || $ENV{DB_USER};
    my $password    = $args{password}    || $ENV{DB_PASSWORD};
    my $print_error = $args{print_error} || $ENV{PRINT_ERROR} || 0;
    my $raise_error = $args{raise_error} || $ENV{RAISE_ERROR} || 1;

    # Connect to the database using cached connections

    my $dbi = "DBI:$driver:";
    $dbi .= "$database:$host" if $driver =~ /mysql/i;
    $dbi .= "server=$server" if $driver =~ /sybase/i;
    $dbi .= "host=$host;sid=$database" if $driver =~ /oracle/i;

    my $uid = "$dbi:$user";
    $_Cache->{$uid} ||= DBI->connect( $dbi,
                                      $user,
                                      $password,
                                      { PrintError => $print_error, # warn()
                                        RaiseError => $raise_error, # die()
                                      } );

    $_Connections->{$class} = $_Cache->{$uid};
    $_Drivers->{$class} = $driver;

    # Create standard queries for the database fields

    $class->fields(@{ $args{fields} }) if $args{fields};

    # Return the database connection

    return $_Connections->{$class}
            or croak("cannot connect to database: $DBI::errstr");
}

=item disconnect()

Disconnect from the database cleanly. You must call disconnect() before calling
connect() for a second time, otherwise connect() will merely return a handle to
the current database connection.

=cut
sub disconnect
{
    my ($self) = @_;
    my $class = ref($self) || $self;
    return unless $_Connections->{$class};

    # Finish user-defined queries

    foreach my $query (keys %{ $_Queries->{$class} })
    {
        $_Queries->{$class}->{$query}->finish();
    }
    delete $_Queries->{$class};

    # Finish standard queries

    $_Selects->{$class}->finish();
    $_Deletes->{$class}->finish();
    $_Inserts->{$class}->finish();
    $_Updates->{$class}->finish();

    # Close the database connection and disconnect if it's the last one

    delete $_Connections->{$class};

    my $connected = 0;
    foreach my $class (keys %{$_Connections})
    {
        $connected++ if $_Connections->{$class};
    }

    if (!$connected)
    {
        foreach my $uid (keys %{$_Cache})
        {
            $_Cache->{$uid}->disconnect();
            delete $_Cache->{$uid};
        }
    }
}

=item fields('id_field', 'field1', 'field2', ...)

Setup a list of fields to keep in sync with the database. The first field
should be the "id" field followed by the fields that you want to keep
syncronised with the database. Call fields() immediately after calling
connect() if you didn't provide a field list when you called connect().
Use an id field name of "null" if there is no auto increment id field.

=cut
sub fields
{<commit_msg>fix Data::Object cleanup<commit_after>#!/usr/bin/env perl

=head1 NAME

Data::Object - Store Perl objects in a MySql (or Oracle) database

=head1 VERSION

This document refers to version 1.2 of Data::Object, released Jul 07, 2015.
Download Data::Object.pm from http://www.legendum.com/perl/Data::Object.pm

=head1 SYNOPSIS

First create a table Foo in a MySql database Bar:

    create table Foo
    (
        id        INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        name        VARCHAR(255) NOT NULL,
        email        VARCHAR(255),
        telephone    VARCHAR(255),
        timestamp    TIMESTAMP
    );

Then create a Perl program to access table Foo in database Bar as objects:

    package Foo;
    @ISA = qw(Data::Object);
    use Data::Object;

    package main;
    Foo->connect(database=>'Bar', user=>'fred', password=>'<PASSWORD>');
    Foo->fields( qw(id name email telephone) );
    my $foo = Foo->new(name=>'kev', email=>'<EMAIL>');
    $foo->insert();
    $foo->{telephone} = '+44 207 359 0985';
    $foo->update();
    $foo = Foo->select('name=?', 'kev');
    print $foo->{name} . ' has email address ' . $foo->{email} . '\n';
    $foo->delete();
    Foo->disconnect();

=head1 DESCRIPTION

Data::Object stores objects in a database. It should be inherited by a class
wishing to persist its objects in the database. The inheriting class should
correspond to a table of the same name (case-sensitive) in the database. This
table must have an auto_increment column as its primary key.

Data::Object provides database connectivity via connect() and disconnect(),
object creation via methods new(), select() and next(), and object/database
syncronisation via methods insert(), update() and delete().

Data::Object does not check whether objects were modified in the database while
they were being modified in your Perl program. Nor does it support any locking
mechanisms. Basically it's designed to be fast, simple, easy and predictable.

To use Data::Object with an Oracle database, set the ORACLE_HOME database
environment variable. When using Oracle on Sun Solaris, you should also set the
LD_PRELOAD to "/lib/libthread.so.1" to enable thread support. When you connect,
you should add "driver=>'Oracle'" to the argument list. When setting and getting
object field values, use the Data::Object AUTOLOAD method because Oracle fields
have uppercase names. For example, to set blah to be 2, use "$obj->blah(2)".

=cut
package Data::Object;
$VERSION = "1.2";

use strict;
use Carp;
use DBI;
{
    # Class static references to hashes keyed by class/table name

    my $_Connections = {};  # Database connections, keyed by class
    my $_Drivers = {};      # Database drivers, keyed by class
    my $_Cache = {};        # Database connection cache, keyed by database

    my $_IdField = {};      # Primary key for uniquely identifying objects
    my $_Fields  = {};      # Fields to be syncronised with the database
    my $_Queries = {};      # User-defined queries for select() and next()

    my $_Selects = {};      # Standard select queries for row()
    my $_Inserts = {};      # Standard insert queries for insert()/update()
    my $_Updates = {};      # Standard update queries for update()
    my $_Deletes = {};      # Standard delete queries for delete()

    # Error messages

    my $_ERROR_FIELDS = "call class static method fields() first";

=head2 Class Methods

=over 4

=item connect(driver=>'mysql', host=>'localhost', database=>'dbname', user=>'username', password=>'<PASSWORD>', print_error=>0, raise_error=>1, fields=>[qw(id_field fields1 field2 field3)])

Connect to the database with optional connection details, and return the DBI
connection. The database driver always defaults to "mysql" unless environment
variable $DRIVER has been set. The database, user and password will default
to environment variables $DATABASE, $USER and $PASSWORD. If a field list is
not supplied, then a separate call to fields() must be made after connecting.

=cut
sub connect
{
    my ($self, %args) = @_;
    my $class = ref($self) || $self;

    # Get the connection details

    my $driver      = $args{driver}      || $ENV{DRIVER} || 'mysql';
    my $server      = $args{server}      || $ENV{SERVER} || 'unknown';
    my $host        = $args{host}        || $ENV{HOST}   || 'localhost';
    my $database    = $args{database}    || $ENV{DB_DATABASE};
    my $user        = $args{user}        || $ENV{DB_USER};
    my $password    = $args{password}    || $ENV{DB_PASSWORD};
    my $print_error = $args{print_error} || $ENV{PRINT_ERROR} || 0;
    my $raise_error = $args{raise_error} || $ENV{RAISE_ERROR} || 1;

    # Connect to the database using cached connections

    my $dbi = "DBI:$driver:";
    $dbi .= "$database:$host" if $driver =~ /mysql/i;
    $dbi .= "server=$server" if $driver =~ /sybase/i;
    $dbi .= "host=$host;sid=$database" if $driver =~ /oracle/i;

    my $uid = "$dbi:$user";
    $_Cache->{$uid} ||= DBI->connect( $dbi,
                                      $user,
                                      $password,
                                      { PrintError => $print_error, # warn()
                                        RaiseError => $raise_error, # die()
                                      } );

    $_Connections->{$class} = $_Cache->{$uid};
    $_Drivers->{$class} = $driver;

    # Create standard queries for the database fields

    $class->fields(@{ $args{fields} }) if $args{fields};

    # Return the database connection

    return $_Connections->{$class}
            or croak("cannot connect to database: $DBI::errstr");
}

=item disconnect()

Disconnect from the database cleanly. You must call disconnect() before calling
connect() for a second time, otherwise connect() will merely return a handle to
the current database connection.

=cut
sub disconnect
{
    my ($self) = @_;
    my $class = ref($self) || $self;
    return unless $_Connections->{$class};

    # Finish user-defined queries

    foreach my $query (keys %{ $_Queries->{$class} })
    {
        $_Queries->{$class}->{$query}->finish();
    }
    delete $_Queries->{$class};

    # Finish standard queries

    $_Selects->{$class}->finish();
    $_Deletes->{$class}->finish();
    $_Inserts->{$class}->finish();
    $_Updates->{$class}->finish();

    # Close the database connection and disconnect if it's the last one

    delete $_Connections->{$class};

    my $connected = 0;
    foreach my $class (keys %{$_Connections})
    {
        $connected++ if $_Connections->{$class};
    }

    if (!$connected)
    {
        foreach my $uid (keys %{$_Cache})
        {
            $_Cache->{$uid}->disconnect() if $_Cache->{$uid};
            delete $_Cache->{$uid};
        }
    }
}

=item fields('id_field', 'field1', 'field2', ...)

Setup a list of fields to keep in sync with the database. The first field
should be the "id" field followed by the fields that you want to keep
syncronised with the database. Call fields() immediately after calling
connect() if you didn't provide a field list when you called connect().
Use an id field name of "null" if there is no auto increment id field.

=cut
sub fields
{
{'fraction_non_alphanumeric': 0.12748669094984588, 'fraction_numerical': 0.0070047632390025216, 'mean_word_length': 2.994683827644096, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 118, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 12, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7284667', 'n_tokens_mistral': 4262, 'n_tokens_neox': 4043, 'n_words': 1528}