SQLite.pm 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. package Trog::SQLite;
  2. use v5.36;
  3. use re '/aa';
  4. use POSIX qw{floor};
  5. use DBI;
  6. use DBD::SQLite;
  7. use File::Touch;
  8. use File::Slurper();
  9. use List::Util qw{any};
  10. =head1 Name
  11. Trog::SQLite - Abstracts the boilerpain away!
  12. =head1 SYNOPSIS
  13. my $dbh = Trog::SQLite::dbh("my_schema.sql", "my_sqlite3.db");
  14. ...
  15. =head1 FUNCTIONS
  16. Everything in this module throws when something goes wrong.
  17. =head2 dbh
  18. Get you a database handle with fkeys turned on, and schema consistency enforced.
  19. Caches the handle past the first call.
  20. Be careful when first calling, the standard fork-safety concerns with sqlite apply
  21. =cut
  22. # We need to make sure this is different across forks, AND consistent within them.
  23. my $dbh = {};
  24. # Ensure the db schema is OK, and give us a handle
  25. # WARNING: do not ever call during BEGIN or outside a sub.
  26. # Otherwise, we can't preload_app.
  27. sub dbh {
  28. my ( $schema, $dbname ) = @_;
  29. $dbh //= {};
  30. return $dbh->{$dbname} if $dbh->{$dbname};
  31. File::Touch::touch($dbname) unless -f $dbname;
  32. my $db = DBI->connect( "dbi:SQLite:dbname=$dbname", "", "" );
  33. if ($schema) {
  34. die "No such schema file '$schema' !" unless -f $schema;
  35. my $qq = File::Slurper::read_text($schema);
  36. $db->{sqlite_allow_multiple_statements} = 1;
  37. $db->do($qq) or die "Could not ensure database consistency: " . $db->errstr;
  38. $db->{sqlite_allow_multiple_statements} = 0;
  39. }
  40. $dbh->{$dbname} = $db;
  41. # Turn on fkeys
  42. $db->do("PRAGMA foreign_keys = ON") or die "Could not enable foreign keys";
  43. # Turn on WALmode
  44. $db->do("PRAGMA journal_mode = WAL") or die "Could not enable WAL mode";
  45. # Turn on normal sync mode
  46. $db->do("PRAGMA synchronous = NORMAL") or die "Could not enable sync mode normal";
  47. # 5s busy timeout
  48. $db->do("PRAGMA busy_timeout = 5000") or die "Could not set busy_timeout";
  49. return $db;
  50. }
  51. =head2 bulk_insert(DBI $dbh, STRING $table, ARRAYREF $keys, STRING $action='IGNORE', MIXED @values)
  52. Insert the values into specified table corresponding to the provided keys.
  53. Values must be repeating tuples corresponding to the values. Example:
  54. my $keys = [qw{A B C}];
  55. my @values = qw{1 2 3 4 5 6 7 8 9};
  56. Essentially works around the 999 named param limit and executes by re-using prepared statements.
  57. This results in a quick insert/update of lots of data, such as when building an index or importing data.
  58. For the vast majority of in-practice usage, this will be swatting flies with an elephant gun.
  59. That said, it should always do the job correctly and quickly, even for trivial datasets.
  60. If you don't put fkeys in place (or simply turn them off),
  61. you can use REPLACE as your action to do upserts without causing destructive consequences.
  62. It's less code than writing an ON CONFLICT UPDATE clause, and faster.
  63. Batch your values to whatever is appropriate given your available heap.
  64. =cut
  65. sub bulk_insert ( $dbh, $table, $keys, $ACTION = 'IGNORE', @values ) {
  66. die "unsupported insert action $ACTION" unless any { $ACTION eq $_ } qw{ROLLBACK ABORT FAIL IGNORE REPLACE};
  67. die "keys must be nonempty ARRAYREF" unless ref $keys eq 'ARRAY' && @$keys;
  68. die "#Values must be a multiple of #keys" if @values % @$keys;
  69. my ( $smt, $query ) = ( '', '' );
  70. while (@values) {
  71. #Must have even multiple of #keys, so floor divide and chop remainder
  72. my $nkeys = scalar(@$keys);
  73. my $limit = floor( 999 / $nkeys );
  74. $limit = $limit - ( $limit % $nkeys );
  75. $smt = '' if scalar(@values) < $limit;
  76. my @params = splice( @values, 0, $limit );
  77. if ( !$smt ) {
  78. my @value_tuples;
  79. my @huh = map { '?' } @params;
  80. while (@huh) {
  81. push( @value_tuples, "(" . join( ',', ( splice( @huh, 0, $nkeys ) ) ) . ")" );
  82. }
  83. ## no critic(ValuesAndExpressions::PreventSQLInjection)
  84. $query = "INSERT OR $ACTION INTO $table (" . join( ',', @$keys ) . ") VALUES " . join( ',', @value_tuples );
  85. $smt = $dbh->prepare($query);
  86. }
  87. $smt->execute(@params);
  88. }
  89. }
  90. 1;