diff --git a/MANIFEST b/MANIFEST index 9b30654..1fd6783 100644 --- a/MANIFEST +++ b/MANIFEST @@ -90,6 +90,7 @@ t/61_strlike.t t/62_regexp_multibyte_char_class.t t/63_param_values.t t/64_limit.t +t/65_db_config.t t/cookbook_variance.t t/lib/SQLiteTest.pm t/rt_106151_outermost_savepoint.t diff --git a/SQLite.xs b/SQLite.xs index e18e1cc..2e718eb 100644 --- a/SQLite.xs +++ b/SQLite.xs @@ -354,6 +354,20 @@ limit(dbh, id, new_value = -1) OUTPUT: RETVAL +static int +db_config(dbh, id, new_value = -1) + SV *dbh + int id + int new_value + ALIAS: + DBD::SQLite::db::sqlite_db_config = 1 + CODE: + { + RETVAL = sqlite_db_config(aTHX_ dbh, id, new_value); + } + OUTPUT: + RETVAL + MODULE = DBD::SQLite PACKAGE = DBD::SQLite::st PROTOTYPES: DISABLE diff --git a/dbdimp.c b/dbdimp.c index b6669ac..4d5ec6a 100644 --- a/dbdimp.c +++ b/dbdimp.c @@ -2756,6 +2756,41 @@ sqlite_db_limit(pTHX_ SV *dbh, int id, int new_value) return sqlite3_limit(imp_dbh->db, id, new_value); } +int +sqlite_db_config(pTHX_ SV *dbh, int id, int new_value) +{ + D_imp_dbh(dbh); + int ret; + int rc = -1; + switch (id) { + case SQLITE_DBCONFIG_LOOKASIDE: + sqlite_error(dbh, rc, "SQLITE_DBCONFIG_LOOKASIDE is not supported"); + return FALSE; + case SQLITE_DBCONFIG_MAINDBNAME: + sqlite_error(dbh, rc, "SQLITE_DBCONFIG_MAINDBNAME is not supported"); + return FALSE; + case SQLITE_DBCONFIG_ENABLE_FKEY: + case SQLITE_DBCONFIG_ENABLE_TRIGGER: + case SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER: + case SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION: + case SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE: + case SQLITE_DBCONFIG_ENABLE_QPSG: + case SQLITE_DBCONFIG_TRIGGER_EQP: + case SQLITE_DBCONFIG_RESET_DATABASE: + case SQLITE_DBCONFIG_DEFENSIVE: + rc = sqlite3_db_config(imp_dbh->db, id, new_value, &ret); + break; + default: + sqlite_error(dbh, rc, form("Unknown config id: %d", id)); + return FALSE; + } + if ( rc != SQLITE_OK ) { + sqlite_error(dbh, rc, form("sqlite_config failed with error %s", sqlite3_errmsg(imp_dbh->db))); + return FALSE; + } + return ret; +} + #include "dbdimp_tokenizer.inc" #include "dbdimp_virtual_table.inc" diff --git a/dbdimp.h b/dbdimp.h index 09b8883..1b46d53 100644 --- a/dbdimp.h +++ b/dbdimp.h @@ -135,6 +135,7 @@ HV* _sqlite_status(int reset); HV* _sqlite_st_status(pTHX_ SV *sth, int reset); int sqlite_db_create_module(pTHX_ SV *dbh, const char *name, const char *perl_class); int sqlite_db_limit(pTHX_ SV *dbh, int id, int new_value); +int sqlite_db_config(pTHX_ SV *dbh, int id, int new_value); int sqlite_db_do_sv(SV *dbh, imp_dbh_t *imp_dbh, SV *sv_statement); void init_cxt(); diff --git a/lib/DBD/SQLite.pm b/lib/DBD/SQLite.pm index ada9768..6bbcada 100644 --- a/lib/DBD/SQLite.pm +++ b/lib/DBD/SQLite.pm @@ -60,6 +60,7 @@ sub driver { DBD::SQLite::st->install_method('sqlite_st_status', { O => 0x0004 }); DBD::SQLite::db->install_method('sqlite_create_module'); DBD::SQLite::db->install_method('sqlite_limit'); + DBD::SQLite::db->install_method('sqlite_db_config'); $methods_are_installed++; } @@ -2304,6 +2305,20 @@ Returns a hash reference that holds a set of status information of SQLite statem You may also pass 0 as an argument to reset the status. +=head2 $dbh->sqlite_db_config( $id, $new_integer_value ) + +You can change how the connected database should behave like this: + + use DBD::SQLite::Constants qw/:database_connection_configuration_options/; + + my $dbh = DBI->connect('dbi:SQLite::memory:'); + + # This disables language features that allow ordinary SQL + # to deliberately corrupt the database file + $dbh->sqlite_db_config( SQLITE_DBCONFIG_DEFENSIVE, 1 ); + +As of this writing, C only supports options that set an integer value. C and C are not supported. See also C for details. + =head2 $dbh->sqlite_create_module() Registers a name for a I. Module names must be diff --git a/t/65_db_config.t b/t/65_db_config.t new file mode 100644 index 0000000..4bdf123 --- /dev/null +++ b/t/65_db_config.t @@ -0,0 +1,185 @@ +#!/usr/bin/perl + +use strict; +BEGIN { + $| = 1; + $^W = 1; +} + +use lib "t/lib"; +use SQLiteTest qw/connect_ok @CALL_FUNCS/; +use Test::More; +use DBD::SQLite::Constants qw/:database_connection_configuration_options/; + +#plan tests => 7 * @CALL_FUNCS; + +# LOOKASIDE +for my $func (@CALL_FUNCS) { + SKIP: { + skip 'LOOKASIDE is not supported', 2 if !SQLITE_DBCONFIG_LOOKASIDE; + my $dbh = connect_ok(RaiseError => 1, PrintError => 0); + eval { $dbh->$func(SQLITE_DBCONFIG_LOOKASIDE, 1, 'db_config') }; + ok $@, 'LOOKASIDE is not supported'; + like $@ => qr/LOOKASIDE is not supported/; + } +} + +# MAINDBNAME +for my $func (@CALL_FUNCS) { + SKIP: { + skip 'MAINDBNAME is not supported', 2 if !SQLITE_DBCONFIG_MAINDBNAME; + my $dbh = connect_ok(RaiseError => 1, PrintError => 0); + eval { $dbh->$func(SQLITE_DBCONFIG_MAINDBNAME, 1, 'db_config') }; + ok $@, 'MAINDBNAME is not supported'; + like $@ => qr/MAINDBNAME is not supported/; + } +} + +# ENABLE_FKEY +for my $func (@CALL_FUNCS) { + SKIP: { + skip 'ENABLE_FKEY is not supported', 3 if !SQLITE_DBCONFIG_ENABLE_FKEY; + my $dbh = connect_ok(RaiseError => 1, PrintError => 0); + my $ret = $dbh->$func(SQLITE_DBCONFIG_ENABLE_FKEY, 1, 'db_config'); + is $ret => 1, 'enable foreign key'; + + # TODO: add foreign key check + + $ret = $dbh->$func(SQLITE_DBCONFIG_ENABLE_FKEY, 0, 'db_config'); + is $ret => 0, 'disable foreign key'; + } +} + +# ENABLE_TRIGGER +for my $func (@CALL_FUNCS) { + SKIP: { + skip 'ENABLE_TRIGGER is not supported', 3 if !SQLITE_DBCONFIG_ENABLE_TRIGGER; + my $dbh = connect_ok(RaiseError => 1, PrintError => 0); + my $ret = $dbh->$func(SQLITE_DBCONFIG_ENABLE_TRIGGER, 1, 'db_config'); + is $ret => 1, 'enable trigger'; + + # TODO: add trigger check + + $ret = $dbh->$func(SQLITE_DBCONFIG_ENABLE_TRIGGER, 0, 'db_config'); + is $ret => 0, 'disable trigger'; + } +} + +# ENABLE_FTS3_TOKENIZER +for my $func (@CALL_FUNCS) { + SKIP: { + skip 'ENABLE_FTS3_TOKENIZER is not supported', 3 if !SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER; + my $dbh = connect_ok(RaiseError => 1, PrintError => 0); + my $ret = $dbh->$func(SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER, 1, 'db_config'); + is $ret => 1, 'enable fts3_tokenizer'; + + # TODO: add fts3_tokenizer check + + $ret = $dbh->$func(SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER, 0, 'db_config'); + is $ret => 0, 'disable fts3_tokenizer'; + } +} + +# ENABLE_LOAD_EXTENSION +for my $func (@CALL_FUNCS) { + SKIP: { + skip 'ENABLE_LOAD_EXTENSION is not supported', 3 if !SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION; + my $dbh = connect_ok(RaiseError => 1, PrintError => 0); + my $ret = $dbh->$func(SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, 'db_config'); + is $ret => 1, 'enable load_extension'; + + # TODO: add load_extension check + + $ret = $dbh->$func(SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 0, 'db_config'); + is $ret => 0, 'disable load_extension'; + } +} + +# ENABLE_NO_CKPT_ON_CLOSE +for my $func (@CALL_FUNCS) { + SKIP: { + skip 'NO_CKPT_ON_CLOSE is not supported', 3 if !SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE; + my $dbh = connect_ok(RaiseError => 1, PrintError => 0); + my $ret = $dbh->$func(SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE, 1, 'db_config'); + is $ret => 1, 'no checkpoint on close'; + + # TODO: add checkpoint check + + $ret = $dbh->$func(SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE, 0, 'db_config'); + is $ret => 0, 'checkpoint on close'; + } +} + +# ENABLE_QPSG +for my $func (@CALL_FUNCS) { + SKIP: { + skip 'ENABLE_QPSG is not supported', 3 if !SQLITE_DBCONFIG_ENABLE_QPSG; + my $dbh = connect_ok(RaiseError => 1, PrintError => 0); + my $ret = $dbh->$func(SQLITE_DBCONFIG_ENABLE_QPSG, 1, 'db_config'); + is $ret => 1, 'enable query planner stability guarantee'; + + # TODO: add qpsg check + + $ret = $dbh->$func(SQLITE_DBCONFIG_ENABLE_QPSG, 0, 'db_config'); + is $ret => 0, 'disable query planner stability guarantee'; + } +} + +# TRIGGER_EQP +for my $func (@CALL_FUNCS) { + SKIP: { + skip 'TRIGGER_EQP is not supported', 3 if !SQLITE_DBCONFIG_TRIGGER_EQP; + my $dbh = connect_ok(RaiseError => 1, PrintError => 0); + my $ret = $dbh->$func(SQLITE_DBCONFIG_TRIGGER_EQP, 1, 'db_config'); + is $ret => 1, 'trigger explain query plan'; + + # TODO: add trigger check + + $ret = $dbh->$func(SQLITE_DBCONFIG_TRIGGER_EQP, 0, 'db_config'); + is $ret => 0, 'no trigger explain query plan'; + } +} + +# RESET_DATABASE +for my $func (@CALL_FUNCS) { + SKIP: { + skip 'RESET_DATABASE is not supported', 3 if !SQLITE_DBCONFIG_RESET_DATABASE; + my $dbh = connect_ok(RaiseError => 1, PrintError => 0); + my $ret = $dbh->$func(SQLITE_DBCONFIG_RESET_DATABASE, 1, 'db_config'); + is $ret => 1, 'enable reset database'; + + # TODO: add reset check + + $ret = $dbh->$func(SQLITE_DBCONFIG_RESET_DATABASE, 0, 'db_config'); + is $ret => 0, 'disable reset database'; + } +} + +# DEFENSIVE +for my $func (@CALL_FUNCS) { + SKIP: { + skip 'DEFENSIVE is not supported', 8 if !SQLITE_DBCONFIG_DEFENSIVE; + my $dbh = connect_ok(RaiseError => 1, PrintError => 0); + + my $sql = 'CREATE TABLE foo (id, text)'; + $dbh->do($sql); + $dbh->do('PRAGMA writable_schema=ON'); + my $row = $dbh->selectrow_hashref('SELECT * FROM sqlite_master WHERE name = ?', {Slice => +{}}, 'foo'); + is $row->{sql} => $sql, 'found sql'; + + my $ret = $dbh->$func(SQLITE_DBCONFIG_DEFENSIVE, 1, 'db_config'); + is $ret => 1; + eval { $dbh->do('UPDATE sqlite_master SET name = ? WHERE name = ?', undef, 'bar', 'foo'); }; + ok $@, "updating sqlite_master is prohibited"; + like $@ => qr/table sqlite_master may not be modified/; + + $ret = $dbh->$func(SQLITE_DBCONFIG_DEFENSIVE, 0, 'db_config'); + is $ret => 0; + $ret = $dbh->do('UPDATE sqlite_master SET name = ? WHERE name = ?', undef, 'bar', 'foo'); + ok $ret, 'updating sqlite_master is succeeded'; + $row = $dbh->selectrow_hashref('SELECT * FROM sqlite_master WHERE name = ?', {Slice => +{}}, 'foo'); + ok !$row, 'sql not found'; + } +} + +done_testing; \ No newline at end of file