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 +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +72 -11
- data/lib/enhanced_sqlite3/adapter.rb +77 -4
- data/lib/enhanced_sqlite3/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd134c96488a861c3fdc36c32396547a31ea596c11243aff923995acff8ae4b5
|
4
|
+
data.tar.gz: d2029b2162c66df5689e65cb4e281f73d21049adafaecf9bbf78490570a10321
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
#
|
1
|
+
# ActiveRecord Enhanced SQLite3 Adapter
|
2
2
|
|
3
|
-
|
3
|
+
Enhance ActiveRecord's 7.1 SQLite3 adapter. Adds support for:
|
4
4
|
|
5
|
-
|
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
|
-
|
14
|
+
```shell
|
15
|
+
$ bundle add activerecord-enhancedsqlite3-adapter
|
16
|
+
```
|
14
17
|
|
15
|
-
|
18
|
+
## Usage
|
16
19
|
|
17
|
-
|
20
|
+
This gem hooks into your Rails application to enhance the `SQLite3Adapter` automatically. No setup required!
|
18
21
|
|
19
|
-
|
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
|
-
|
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/
|
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
|
-
#
|
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
|
-
|
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
|
-
|
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
|
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.
|
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
|
11
|
+
date: 2023-12-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|