package Pcore::Handle::sqlite; use Pcore -class, -const, -res; use DBI qw[]; use Pcore::Handle::DBI::Const qw[:CONST]; use DBD::SQLite qw[]; use DBD::SQLite::Constants qw[:file_open]; use Pcore::Util::Scalar qw[weaken is_bool is_blessed_ref looks_like_number is_plain_arrayref is_blessed_arrayref]; use Pcore::Util::UUID qw[uuid_v1mc_str uuid_v4_str]; use Pcore::Util::Digest qw[md5_hex]; use Pcore::Util::Data qw[to_json]; use Pcore::Util::Text qw[encode_utf8]; use Time::HiRes qw[]; # NOTE http://habrahabr.ru/post/149635/ # для вставки данных в цикле надо использовать h->begin_work ... h->commit with qw[Pcore::Handle::DBI]; const our $SQLITE_OPEN_RO => SQLITE_OPEN_READONLY | SQLITE_OPEN_SHAREDCACHE; const our $SQLITE_OPEN_RW => SQLITE_OPEN_READWRITE | SQLITE_OPEN_SHAREDCACHE; const our $SQLITE_OPEN_RWC => SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_SHAREDCACHE; const our $SQLITE_OPEN => { ro => $SQLITE_OPEN_RO, rw => $SQLITE_OPEN_RW, rwc => $SQLITE_OPEN_RWC, }; has mode => 'rwc'; # Enum [ keys $SQLITE_OPEN->%* ] has busy_timeout => 1_000 * 3; # PositiveOrZeroInt ), milliseconds, set to 0 to disable timeout, default - 3 seconds # SQLITE PRAGMAS has temp_store => 'MEMORY'; # Enum [qw[FILE MEMORY]] has journal_mode => 'WAL'; # Enum [qw[DELETE TRUNCATE PERSIST MEMORY WAL OFF]], WAL is the best has synchronous => 'OFF'; # Enum [qw[FULL NORMAL OFF]], OFF - data integrity on app failure, NORMAL - data integrity on app and OS failures, FULL - full data integrity on app or OS failures, slower has cache_size => -1_048_576; # Int, 0+ - pages, -kilobytes, default 1G has foreign_keys => 1; # Bool # TODO # has to_string => 999; # automaticaly stringify query if number of bind params greater than this threshold has is_sqlite => ( 1, init_arg => undef ); # Bool has h => ( init_arg => undef ); # Object has prepared_sth => ( init_arg => undef ); # HashRef has query => ( init_arg => undef ); # ScalarRef, ref to the last query # SQLite types const our $SQLITE_UNKNOWN => 0; const our $SQLITE_INTEGER => 4; const our $SQLITE_REAL => 6; const our $SQLITE_TEXT => 12; const our $SQLITE_BLOB => 30; # postgreSQL types to SQLite const our $TYPE_TO_SQLITE => { $SQL_BOOL => $SQLITE_INTEGER, $SQL_BYTEA => $SQLITE_BLOB, $SQL_CHAR => $SQLITE_TEXT, $SQL_FLOAT4 => $SQLITE_REAL, $SQL_FLOAT8 => $SQLITE_REAL, $SQL_JSON => $SQLITE_BLOB, $SQL_INT2 => $SQLITE_INTEGER, $SQL_INT4 => $SQLITE_INTEGER, $SQL_INT8 => $SQLITE_INTEGER, $SQL_MONEY => $SQLITE_REAL, $SQL_NUMERIC => $SQLITE_REAL, $SQL_TEXT => $SQLITE_TEXT, $SQL_UNKNOWN => $SQLITE_UNKNOWN, $SQL_UUID => $SQLITE_BLOB, $SQL_VARCHAR => $SQLITE_TEXT, }; sub BUILD ( $self, $args ) { my $attr = { AutoCommit => 1, sqlite_open_flags => $SQLITE_OPEN->{ $self->{mode} }, sqlite_unicode => 1, sqlite_allow_multiple_statements => 1, sqlite_use_immediate_transaction => 1, sqlite_see_if_its_a_number => 1, Warn => 1, PrintWarn => 0, PrintError => 0, RaiseError => 0, ShowErrorStatement => 1, # HandleError => sub { # my $msg = shift; # # # escape_perl $msg; # # P->sendlog( 'Pcore-DBH.ERROR', $msg ); # # return; # }, # Callbacks => { # connected => sub { # P->sendlog( 'Pcore-DBH.DEBUG', 'Connected to: ' . $_[1] ) if $ENV{PCORE_DBH_DEBUG}; # # return; # }, # prepare => sub { # return; # }, # do => sub { # P->sendlog( 'Pcore-DBH.DEBUG', 'Do: ' . $_[1] ) if $ENV{PCORE_DBH_DEBUG}; # # return; # }, # ChildCallbacks => { # execute => sub { # P->sendlog( 'Pcore-DBH.DEBUG', 'Execute: ' . $_[0]->{Statement} ) if $ENV{PCORE_DBH_DEBUG}; # # return; # } # } # }, }; my $dbname = $self->{uri}->{path} ? $self->{uri}->{path}->to_string : ':memory:'; my $dbh = DBI->connect( "dbi:SQLite:dbname=$dbname", $EMPTY, $EMPTY, $attr ); $dbh->do('PRAGMA encoding = "UTF-8"'); $dbh->do( 'PRAGMA temp_store = ' . $self->{temp_store} ); $dbh->do( 'PRAGMA journal_mode = ' . $self->{journal_mode} ); $dbh->do( 'PRAGMA synchronous = ' . $self->{synchronous} ); $dbh->do( 'PRAGMA cache_size = ' . $self->{cache_size} ); $dbh->do( 'PRAGMA foreign_keys = ' . $self->{foreign_keys} ); $dbh->sqlite_busy_timeout( $self->{busy_timeout} ); # create custom functions $dbh->sqlite_create_function( 'uuid_generate_v1mc', 0, sub { return [ uuid_v1mc_str, $SQLITE_BLOB ] } ); $dbh->sqlite_create_function( 'uuid_generate_v4', 0, sub { return [ uuid_v4_str, $SQLITE_BLOB ] } ); $dbh->sqlite_create_function( 'gen_random_uuid', 0, sub { return [ uuid_v4_str, $SQLITE_BLOB ] } ); $dbh->sqlite_create_function( 'time_hires', 0, sub { return Time::HiRes::time() } ); $dbh->sqlite_create_function( 'md5', 1, sub { return defined $_[0] ? [ md5_hex( encode_utf8 $_[0] ), $SQLITE_BLOB ] : undef } ); $self->{on_connect}->($self) if $self->{on_connect}; $self->{h} = $dbh; return; } # QUOTE sub _get_sqlite_type : prototype($) ($type) { # use TEXT as default type if ( !defined $type || !exists $TYPE_TO_SQLITE->{$type} ) { $type = $SQLITE_TEXT; } else { $type = $TYPE_TO_SQLITE->{$type}; } return $type; } sub quote ( $self, $var ) { return 'NULL' if !defined $var; my $type; # expand type if ( is_blessed_arrayref $var) { return 'NULL' if !defined $var->[1]; $type = _get_sqlite_type( $var->[0] ); if ( $var->[0] == $SQL_BOOL ) { return $var->[1] ? 'TRUE' : 'FALSE'; } elsif ( $var->[0] == $SQL_JSON ) { $var = to_json $var->[1]; } else { $var = $var->[1]; } } else { # transparently encode arrays to JSON if ( is_plain_arrayref $var) { $type = $SQLITE_BLOB; $var = to_json $var; } # known boolean values elsif ( is_bool $var ) { return $var ? 'TRUE' : 'FALSE'; } # default type is TEXT else { $type = $SQLITE_TEXT; } } # NUMBER if ( ( $type == $SQLITE_INTEGER || $type == $SQLITE_REAL ) && looks_like_number $var) { return $var; } # BLOB elsif ( $type == $SQLITE_BLOB ) { utf8::encode $var if utf8::is_utf8 $var; $var = q[x'] . unpack( 'H*', $var ) . q[']; return $var; } # TEXT, default # a string constant is formed by enclosing the string in single quotes (') # a single quote within the string can be encoded by putting two single quotes in a row else { # quote \x00 in literal if ( index( $var, "\x00" ) != -1 ) { utf8::encode $var if utf8::is_utf8 $var; return q[CAST(x'] . unpack( 'H*', $var ) . q[' AS TEXT)]; } else { $var =~ s/'/''/smg; return qq['$var']; } } } # https://sqlite.org/lang_keywords.html sub quote_id ( $self, $id ) { utf8::encode $id if utf8::is_utf8 $id; if ( index( $id, q[.] ) != -1 ) { my @id = split /[.]/sm, $id; for my $s (@id) { $s =~ s/"/""/smg; $s = qq["$s"]; } return join q[.], @id; } else { $id =~ s/"/""/smg; return qq["$id"]; } } sub _exec_sth ( $self, $query, @args ) { # parse args my $bind = is_plain_arrayref $args[0] ? shift @args : undef; my %args = @args; my ( $dbh, $sth, $rows ) = $self->{h}; # query is prepared DBI sth if ( ref $query eq 'DBI::st' ) { $sth = $query; } # query is pcore sth elsif ( ref $query eq 'Pcore::Handle::DBI::STH' ) { $sth = $self->{prepared_sth}->{ $query->{id} }; if ( !defined $sth ) { $self->{query} = \$query->{query}; $sth = $dbh->prepare( $query->{query} ); return $rows, $sth, \%args if defined $DBI::err; $self->{prepared_sth}->{ $query->{id} } = $sth; push $query->{dbh}->@*, $self; weaken $query->{dbh}->[-1]; } } # query is ArrayRef elsif ( is_plain_arrayref $query) { ( $query, $bind ) = $self->prepare_query($query); } # prepare sth if ( !defined $sth ) { $self->{query} = \$query; $sth = $dbh->prepare($query); return $rows, $sth, \%args if defined $DBI::err; } if ( defined $bind ) { # bind and exec $rows = $self->_execute( $sth, $bind, 0 ); } else { $rows = $sth->execute; } if ( defined $DBI::err ) { return $rows, $sth, \%args; } else { $rows = 0 if $rows == 0; # convert "0E0" to "0" return $rows, $sth, \%args; } } sub _warn ($self) { warn qq[DBI: "$DBI::errstr"] . ( defined $self->{query} ? qq[, current query: "$self->{query}->$*"] : $EMPTY ); return; } sub _execute ( $self, $sth, $bind, $bind_pos ) { # make a copy my @bind = $bind->@[ $bind_pos .. $bind_pos + $sth->{NUM_OF_PARAMS} - 1 ]; $bind_pos += $sth->{NUM_OF_PARAMS}; # bind types for ( my $i = 0; $i <= $#bind; $i++ ) { if ( is_blessed_arrayref $bind[$i] ) { $sth->bind_param( $i + 1, undef, _get_sqlite_type $bind[$i]->[0] ); # values is not defined if ( !defined $bind[$i]->[1] ) { $bind[$i] = undef; } elsif ( $bind[$i]->[0] == $SQL_BOOL ) { $bind[$i] = $bind[$i]->[1] ? 1 : 0; } elsif ( $bind[$i]->[0] == $SQL_JSON ) { $bind[$i] = to_json $bind[$i]->[1]; } elsif ( $bind[$i]->[0] == $SQL_BYTEA ) { $bind[$i] = encode_utf8 $bind[$i]->[1]; } else { $bind[$i] = $bind[$i]->[1]; } } # encode ArrayRef as JSON elsif ( is_plain_arrayref $bind[$i] ) { $sth->bind_param( $i + 1, undef, $SQLITE_BLOB ); $bind[$i] = to_json $bind[$i]; } # known boolean values elsif ( is_bool $bind[$i] ) { $bind[$i] = $bind[$i] ? 1 : 0; } } return $sth->execute(@bind); } sub get_dbh ( $self ) { return res(200), $self; } # PUBLIC DBI METHODS sub do ( $self, $query, @args ) { ## no critic qw[Subroutines::ProhibitBuiltinHomonyms] # parse args my $bind = is_plain_arrayref $args[0] ? shift @args : undef; my %args = @args; my ( $dbh, $rows, $res ) = $self->{h}; # query is sth if ( is_blessed_ref $query) { my $sth; # query is Pcore sth if ( ref $query eq 'Pcore::Handle::DBI::STH' ) { $sth = $self->{prepared_sth}->{ $query->{id} }; if ( !defined $sth ) { $self->{query} = \$query->{query}; # prepare sth $sth = $dbh->prepare( $query->{query} ); # check error if ( defined $DBI::err ) { $self->_warn; $res = res [ 500, $DBI::errstr ]; } $self->{prepared_sth}->{ $query->{id} } = $sth; push $query->{dbh}->@*, $self; weaken $query->{dbh}->[-1]; } } # query is prepared DBI sth elsif ( ref $query eq 'DBI::st' ) { $sth = $query; } else { warn 'Invalid STH class'; $res = res [ 400, 'Invalid STH class' ]; } # bind and exec if ( !defined $res ) { if ( defined $bind ) { $rows = $self->_execute( $sth, $bind, 0 ); } else { $rows = $sth->execute; } # check error if ( defined $DBI::err ) { $self->_warn; $res = res [ 500, $DBI::errstr ]; } } # success if ( !defined $res ) { $rows = 0 if $rows == 0; # convert "0E0" to "0" $res = res 200, rows => $rows; } return $res; } # query is ArrayRef elsif ( is_plain_arrayref $query) { ( $query, $bind ) = $self->prepare_query($query); } # simple query mode # execute query directly without prepare and bind params # multiple queries in single statement are allowed if ( !defined $bind ) { $self->{query} = \$query; $rows = DBD::SQLite::db::_do( $dbh, $query ); # check error if ( defined $DBI::err ) { $self->_warn; $res = res [ 500, $DBI::errstr ]; } else { $rows = 0 if $rows == 0; # convert "0E0" to "0" $res = res 200, rows => $rows; } return $res; } # extended query mode # multiple queries in single statement are allowed else { my $bind_pos = 0; while ($query) { $self->{query} = \$query; # prepare sth my $sth = $dbh->prepare($query); # prepare sth error if ( defined $DBI::err ) { $self->_warn; $res = res [ 500, $DBI::errstr ]; last; } # bind and exec $self->_execute( $sth, $bind, $bind_pos ); # execute error if ( defined $DBI::err ) { $self->_warn; $res = res [ 500, $DBI::errstr ]; last; } $rows += $sth->rows; $query = $sth->{sqlite_unprepared_statements}; } $res = res 200, rows => $rows if !defined $res; return $res; } } # key_col => [0, 1, 'id'], key_col => 'id' sub selectall ( $self, @ ) { my ( $rows, $sth, $args ) = &_exec_sth; ## no critic qw[Subroutines::ProhibitAmpersandSigils] my $res; # check error if ( defined $DBI::err ) { $self->_warn; $res = res [ 500, $DBI::errstr ]; } else { my $data; if ( defined $args->{key_col} ) { # make fields indexes 0-based my @key_cols = map { looks_like_number $_ ? $_ + 1 : $_ } is_plain_arrayref $args->{key_col} ? $args->{key_col}->@* : $args->{key_col}; $data = $sth->fetchall_hashref( \@key_cols ); # check error if ( defined $DBI::err ) { $self->_warn; $res = res [ 500, $DBI::errstr ]; } else { $res = res 200, $data->%* ? $data : undef, rows => $rows; } } else { $data = $sth->fetchall_arrayref( {}, undef ); $res = res 200, $data->@* ? $data : undef, rows => $rows; } } return $res; } sub selectall_arrayref ( $self, @ ) { my ( $rows, $sth, $args ) = &_exec_sth; ## no critic qw[Subroutines::ProhibitAmpersandSigils] my $res; # check error if ( defined $DBI::err ) { $self->_warn; $res = res [ 500, $DBI::errstr ]; } else { my $data = $sth->fetchall_arrayref( undef, undef ); $res = res 200, $data->@* ? $data : undef, rows => $rows; } return $res; } sub selectrow ( $self, @ ) { my ( $rows, $sth, $args ) = &_exec_sth; ## no critic qw[Subroutines::ProhibitAmpersandSigils] my $res; # check error if ( defined $DBI::err ) { $self->_warn; $res = res [ 500, $DBI::errstr ]; } else { $res = res 200, $sth->fetchrow_hashref, rows => $rows; $sth->finish; } return $res; } sub selectrow_arrayref ( $self, @ ) { my ( $rows, $sth, $args ) = &_exec_sth; ## no critic qw[Subroutines::ProhibitAmpersandSigils] my $res; # check error if ( defined $DBI::err ) { $self->_warn; $res = res [ 500, $DBI::errstr ]; } else { $res = res 200, $sth->fetchrow_arrayref, rows => $rows; $sth->finish; } return $res; } # col => [0, 'id'], col => 'id', default col => 0 sub selectcol ( $self, @ ) { my ( $rows, $sth, $args ) = &_exec_sth; ## no critic qw[Subroutines::ProhibitAmpersandSigils] my $res; # check error if ( defined $DBI::err ) { $self->_warn; $res = res [ 500, $DBI::errstr ]; } else { my ( $name2idx, $idx, @vals ); my $num_of_fields = $sth->{NUM_OF_FIELDS} - 1; $args->{col} //= 0; for my $col ( is_plain_arrayref $args->{col} ? $args->{col}->@* : $args->{col} ) { if ( looks_like_number $col) { if ( $col > $num_of_fields ) { warn qq[Invalid column index: "$col"]; $res = res [ 400, qq[Invalid column index: "$col"] ]; last; } $sth->bind_col( $col + 1, \$vals[ $idx++ ] ); } else { $name2idx //= $sth->{NAME_hash}; if ( !exists $name2idx->{$col} ) { warn qq[Invalid column name: "$col"]; $res = res [ 400, qq[Invalid column name: "$col"] ]; last; } $sth->bind_col( $name2idx->{$col} + 1, \$vals[ $idx++ ] ); } } if ( !defined $res ) { my $data; push $data->@*, @vals while $sth->fetch; $res = res 200, $data, rows => $rows; } } return $res; } # TRANSACTIONS sub in_transaction ($self) { return !$self->{h}->{AutoCommit} || !$self->{h}->sqlite_get_autocommit; } sub begin_work ( $self ) { $self->{h}->begin_work; my $res; # check error if ( defined $DBI::err ) { $self->_warn; $res = res [ 500, $DBI::errstr ]; } else { $res = res 200; } return $res; } sub commit ( $self ) { $self->{h}->commit; my $res; # check error if ( defined $DBI::err ) { $self->_warn; $res = res [ 500, $DBI::errstr ]; } else { $res = res 200; } return $res; } sub rollback ( $self ) { $self->{h}->rollback; my $res; # check error if ( defined $DBI::err ) { $self->_warn; $res = res [ 500, $DBI::errstr ]; } else { $res = res 200; } return $res; } # LAST INSERT ID sub last_insert_id ( $self ) { return $self->{h}->sqlite_last_insert_rowid; } # ATTACH sub attach ( $self, $name, $path = undef ) { $path //= ':memory:'; $self->{h}->do(qq[ATTACH DATABASE '$path' AS "$name"]); $self->{h}->do(qq[PRAGMA $name.encoding = "UTF-8"]); $self->{h}->do( qq[PRAGMA $name.temp_store = ] . $self->{temp_store} ); $self->{h}->do( qq[PRAGMA $name.journal_mode = ] . $self->{journal_mode} ); $self->{h}->do( qq[PRAGMA $name.synchronous = ] . $self->{synchronous} ); $self->{h}->do( qq[PRAGMA $name.cache_size = ] . $self->{cache_size} ); $self->{h}->do( qq[PRAGMA $name.foreign_keys = ] . $self->{foreign_keys} ); return; } 1; ## -----SOURCE FILTER LOG BEGIN----- ## ## PerlCritic profile "pcore-script" policy violations: ## +------+----------------------+----------------------------------------------------------------------------------------------------------------+ ## | Sev. | Lines | Policy | ## |======+======================+================================================================================================================| ## | 3 | | Subroutines::ProhibitExcessComplexity | ## | | 163 | * Subroutine "quote" with high complexity score (22) | ## | | 381 | * Subroutine "do" with high complexity score (24) | ## |------+----------------------+----------------------------------------------------------------------------------------------------------------| ## | 3 | 466 | Subroutines::ProtectPrivateSubs - Private subroutine/method used | ## |------+----------------------+----------------------------------------------------------------------------------------------------------------| ## | 2 | 338 | ControlStructures::ProhibitCStyleForLoops - C-style "for" loop used | ## |------+----------------------+----------------------------------------------------------------------------------------------------------------| ## | 2 | 677 | ControlStructures::ProhibitPostfixControls - Postfix control "while" used | ## +------+----------------------+----------------------------------------------------------------------------------------------------------------+ ## ## -----SOURCE FILTER LOG END----- __END__ =pod =encoding utf8 =head1 NAME Pcore::Handle::sqlite =head1 SYNOPSIS =head1 DESCRIPTION =head1 ATTRIBUTES =head1 METHODS =head1 SEE ALSO =cut