SQLite.pm 3.8 KB

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