SQLite.pm 4.2 KB

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