activerecord-enhancedsqlite3-adapter 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f138b7e075e546691856c8e45f22413a1e381df3024c6ef30e467dbde830c896
4
- data.tar.gz: 0e6bde9f85883cacc12893f8ab2b1ca5820deea6d6a1357a262fb24401ed4980
3
+ metadata.gz: fd134c96488a861c3fdc36c32396547a31ea596c11243aff923995acff8ae4b5
4
+ data.tar.gz: d2029b2162c66df5689e65cb4e281f73d21049adafaecf9bbf78490570a10321
5
5
  SHA512:
6
- metadata.gz: dafd2a35c78565843b9aa384fb5988818c48bdc6852da55363c7e18e447a752b8194ef08ea4a40c9b482f30317cd7afc8e9501e22cb38abbab51dd16f3d6c7a7
7
- data.tar.gz: 5c0d6b07eb0b3e90c4fd6f8859f90d944e33d90e100ee96981f24b4d1cb1212266045f81abe935b4ad1913caf22d43cb3386a73bdb0670b15f8686e680619a77
6
+ metadata.gz: 6803168a5f067839e6ea7416f2f067bbc044202d14fd21a9536d068bebe35d7d89a481cbfcf5e230d4063705a7b5241aeb76d8f92846e2ecf5cdc2afa8b805f0
7
+ data.tar.gz: 2845ea0983129dd0e6e66b66e8f10fca3538192dcdaec62ec55f497ebb8f82f52bdc2352eb1fdf85b859bbd7436e86b57d15da4ce1135c00e23f8996db5cfc59
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2023-12-10
4
+
5
+ - Ensure transactions are IMMEDIATE and not DEFERRED
6
+ - Ensure that our `busy_handler` is the very first configuration to be set on a connection
7
+ - Simplify and speed up our `busy_handler` implementation
8
+
9
+ ## [0.3.0] - 2023-12-06
10
+
11
+ - Added a more performant implementation of the the `timeout` mechanism
12
+
13
+ ## [0.2.0] - 2023-09-28
14
+
15
+ - Added support for deferrable constraints
16
+
3
17
  ## [0.1.0] - 2023-09-28
4
18
 
5
19
  - Initial release
20
+ - Added support for virtual columns
21
+ - Added support setting PRAGMA statements via the `config/database.yml` file
22
+ - Added support for loading extensions via the `config/database.yml` file
data/README.md CHANGED
@@ -1,24 +1,85 @@
1
- # Activerecord::Enhanced::Sqlite3::Adapter
1
+ # ActiveRecord Enhanced SQLite3 Adapter
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ Enhance ActiveRecord's 7.1 SQLite3 adapter. Adds support for:
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/activerecord/enhanced/sqlite3/adapter`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ * generated columns,
6
+ * deferred foreign keys,
7
+ * `PRAGMA` tuning,
8
+ * and extension loading
6
9
 
7
10
  ## Installation
8
11
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
-
11
12
  Install the gem and add to the application's Gemfile by executing:
12
13
 
13
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
14
+ ```shell
15
+ $ bundle add activerecord-enhancedsqlite3-adapter
16
+ ```
14
17
 
15
- If bundler is not being used to manage dependencies, install the gem by executing:
18
+ ## Usage
16
19
 
17
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
20
+ This gem hooks into your Rails application to enhance the `SQLite3Adapter` automatically. No setup required!
18
21
 
19
- ## Usage
22
+ Once installed, you can take advantage of the added features.
23
+
24
+ ### Generated columns
25
+
26
+ You can now create `virtual` columns, both stored and dynamic. The [SQLite docs](https://www.sqlite.org/gencol.html) explain the difference:
27
+
28
+ > Generated columns can be either VIRTUAL or STORED. The value of a VIRTUAL column is computed when read, whereas the value of a STORED column is computed when the row is written. STORED columns take up space in the database file, whereas VIRTUAL columns use more CPU cycles when being read.
29
+
30
+ The default is to create dynamic/virtual columns.
31
+
32
+ ```ruby
33
+ create_table :virtual_columns, force: true do |t|
34
+ t.string :name
35
+ t.virtual :upper_name, type: :string, as: "UPPER(name)", stored: true
36
+ t.virtual :lower_name, type: :string, as: "LOWER(name)", stored: false
37
+ t.virtual :octet_name, type: :integer, as: "LENGTH(name)"
38
+ end
39
+ ```
40
+
41
+ ### Deferred foreign keys
42
+
43
+ You can now specify whether or not a foreign key should be deferrable, whether `:deferred` or `:immediate`.
44
+
45
+ `:deferred` foreign keys mean that the constraint check will be done once the transaction is committed and allows the constraint behavior to change within transaction. `:immediate` means that constraint check is immediate and allows the constraint behavior to change within transaction. The default is `:immediate`.
46
+
47
+ ```ruby
48
+ add_reference :person, :alias, foreign_key: { deferrable: :deferred }
49
+ add_reference :alias, :person, foreign_key: { deferrable: :deferred }
50
+ ```
51
+
52
+ ### `PRAGMA` tuning
53
+
54
+ Pass any [`PRAGMA` key-value pair](https://www.sqlite.org/pragma.html) under a `pragmas` list in your `config/database.yml` file to ensure that these configuration settings are applied to all database connections.
55
+
56
+ ```yaml
57
+ default: &default
58
+ adapter: sqlite3
59
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
60
+ pragmas:
61
+ # level of database durability, 2 = "FULL" (sync on every write), other values include 1 = "NORMAL" (sync every 1000 written pages) and 0 = "NONE"
62
+ # https://www.sqlite.org/pragma.html#pragma_synchronous
63
+ synchronous: "FULL"
64
+ ```
65
+
66
+ ### Extension loading
67
+
68
+ There are a number of [SQLite extensions available as Ruby gems](https://github.com/asg017/sqlite-ecosystem). In order to load the extensions, you need to install the gem (`bundle add {extension-name}`) and then load it into the database connections. In order to support the latter, this gem enhances the `config/database.yml` file to support an `extensions` array. For example, to install and load [an extension](https://github.com/asg017/sqlite-ulid) for supporting [<abbr title="Universally Unique Lexicographically Sortable Identifiers">ULIDs</abbr>](https://github.com/ulid/spec), we would do:
69
+
70
+ ```shell
71
+ $ bundle add sqlite_ulid
72
+ ```
73
+
74
+ then
20
75
 
21
- TODO: Write usage instructions here
76
+ ```yaml
77
+ default: &default
78
+ adapter: sqlite3
79
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
80
+ extensions:
81
+ - sqlite_ulid
82
+ ```
22
83
 
23
84
  ## Development
24
85
 
@@ -28,7 +89,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
28
89
 
29
90
  ## Contributing
30
91
 
31
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/activerecord-enhancedsqlite3-adapter.
92
+ Bug reports and pull requests are welcome on GitHub at https://github.com/fractaledmind/activerecord-enhancedsqlite3-adapter.
32
93
 
33
94
  ## License
34
95
 
@@ -10,6 +10,27 @@ require "enhanced_sqlite3/supports_deferrable_constraints"
10
10
 
11
11
  module EnhancedSQLite3
12
12
  module Adapter
13
+ # Setup the Rails SQLite3 adapter instance.
14
+ #
15
+ # extends https://github.com/rails/rails/blob/main/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#L90
16
+ def initialize(...)
17
+ super
18
+ # Ensure that all connections default to immediate transaction mode.
19
+ # This is necessary to prevent SQLite from deadlocking when concurrent processes open write transactions.
20
+ # By default, SQLite opens transactions in deferred mode, which means that a transactions acquire
21
+ # a shared lock on the database, but will attempt to upgrade that lock to an exclusive lock if/when
22
+ # a write is attempted. Because SQLite is in the middle of a transaction, it cannot retry the transaction
23
+ # if a BUSY exception is raised, and so it will immediately raise a SQLITE_BUSY exception without calling
24
+ # the `busy_handler`. Because Rails only wraps writes in transactions, this means that all transactions
25
+ # will attempt to acquire an exclusive lock on the database. Thus, under any concurrent load, you are very
26
+ # likely to encounter a SQLITE_BUSY exception.
27
+ # By setting the default transaction mode to immediate, SQLite will instead attempt to acquire
28
+ # an exclusive lock as soon as the transaction is opened. If the lock cannot be acquired, it will
29
+ # immediately call the `busy_handler` to retry the transaction. This allows concurrent processes to
30
+ # coordinate and linearize their transactions, avoiding deadlocks.
31
+ @connection_parameters.merge!(default_transaction_mode: :immediate)
32
+ end
33
+
13
34
  # Perform any necessary initialization upon the newly-established
14
35
  # @raw_connection -- this is the place to modify the adapter's
15
36
  # connection settings, run queries to configure any application-global
@@ -18,10 +39,10 @@ module EnhancedSQLite3
18
39
  # Implementations may assume this method will only be called while
19
40
  # holding @lock (or from #initialize).
20
41
  #
21
- # extends https://github.com/rails/rails/blob/main/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#L691
42
+ # overrides https://github.com/rails/rails/blob/main/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#L691
22
43
  def configure_connection
23
- super
24
-
44
+ configure_busy_handler_timeout
45
+ check_version
25
46
  configure_pragmas
26
47
  configure_extensions
27
48
 
@@ -31,8 +52,60 @@ module EnhancedSQLite3
31
52
 
32
53
  private
33
54
 
55
+ def configure_busy_handler_timeout
56
+ return unless @config.key?(:timeout)
57
+
58
+ timeout = self.class.type_cast_config_to_integer(@config[:timeout])
59
+ timeout_seconds = timeout.fdiv(1000)
60
+ retry_interval = 6e-5 # 60 microseconds
61
+
62
+ @raw_connection.busy_handler do |count|
63
+ timed_out = false
64
+ # keep track of elapsed time every 100 iterations (to lower load)
65
+ if (count % 100).zero?
66
+ # fail if we exceed the timeout value (captured from the timeout config option, converted to seconds)
67
+ timed_out = (count * retry_interval) > timeout_seconds
68
+ end
69
+ if timed_out
70
+ false # this will cause the BusyException to be raised
71
+ else
72
+ sleep(retry_interval)
73
+ true
74
+ end
75
+ end
76
+ end
77
+
34
78
  def configure_pragmas
35
- @config.fetch(:pragmas, []).each do |key, value|
79
+ defaults = {
80
+ # Enforce foreign key constraints
81
+ # https://www.sqlite.org/pragma.html#pragma_foreign_keys
82
+ # https://www.sqlite.org/foreignkeys.html
83
+ "foreign_keys" => "ON",
84
+ # Impose a limit on the WAL file to prevent unlimited growth
85
+ # https://www.sqlite.org/pragma.html#pragma_journal_size_limit
86
+ "journal_size_limit" => 64.megabytes,
87
+ # Set the local connection cache to 2000 pages
88
+ # https://www.sqlite.org/pragma.html#pragma_cache_size
89
+ "cache_size" => 2000
90
+ }
91
+ unless @memory_database
92
+ defaults.merge!(
93
+ # Journal mode WAL allows for greater concurrency (many readers + one writer)
94
+ # https://www.sqlite.org/pragma.html#pragma_journal_mode
95
+ "journal_mode" => "WAL",
96
+ # Set more relaxed level of database durability
97
+ # 2 = "FULL" (sync on every write), 1 = "NORMAL" (sync every 1000 written pages) and 0 = "NONE"
98
+ # https://www.sqlite.org/pragma.html#pragma_synchronous
99
+ "synchronous" => "NORMAL",
100
+ # Set the global memory map so all processes can share some data
101
+ # https://www.sqlite.org/pragma.html#pragma_mmap_size
102
+ # https://www.sqlite.org/mmap.html
103
+ "mmap_size" => 128.megabytes
104
+ )
105
+ end
106
+ pragmas = defaults.merge(@config.fetch(:pragmas, {}))
107
+
108
+ pragmas.each do |key, value|
36
109
  execute("PRAGMA #{key} = #{value}", "SCHEMA")
37
110
  end
38
111
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EnhancedSQLite3
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-enhancedsqlite3-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Margheim
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-10-09 00:00:00.000000000 Z
11
+ date: 2023-12-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord