activerecord-enhancedsqlite3-adapter 0.5.0 → 0.7.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: 6bd667df1779690780802a3f91133b9a6b8155f9f3c040393b5a722a299ce370
4
- data.tar.gz: ed4008074efd597d2bc79ab2a4b3aea8e7ccccb37fd8158892843d20918b0015
3
+ metadata.gz: 97af786ab4b105a44e48a8501611f13b6a090545b1fa5516138aaebd4a2c203f
4
+ data.tar.gz: 0f8eec83e65a55ee39a043d5a0d23861dbebd8d2fee1ee554ecc9ca6cf836fb9
5
5
  SHA512:
6
- metadata.gz: 3252cf16cc3619a857488b450542ef6b9402b666bdb1a5458d3615d880c073226cbf8d7472032b74bcfa205196f4febf0aaa8bc1fa2e6e00f00dc0d66e975d97
7
- data.tar.gz: ddbafb131266c8edcf5e66303083ab964e8ac9248eaa12eeea4c377501f00e820b54edf406280eb832f0996120e666434e256e308a108d6df945bb21168f8416
6
+ metadata.gz: 04f39dc81145d1f2a2e39cb41c562675f1bdcbf036291f885d5cfe75203c4a1e36b2575c2cf8762a07c184f7d3ada02fec6403eb0284c4ec82c900b80fd362c5
7
+ data.tar.gz: 5d7a13c12e202fa5bd5e4da274271194cd7fb770510bb235dc85ede7ead91893c3615c4e8b424073c1416931003bba42c06afc93d0ce3e96353bfa6a6bb7bf89
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.7.0] - 2024-04-29
4
+
5
+ - Loosen the `sqlite3` dependency to allow for `>= 1.4.0` (including 2.x) ([@fractaledmind](https://github.com/fractaledmind/activerecord-enhancedsqlite3-adapter/pull/15))
6
+
7
+ ## [0.6.0] - 2024-04-10
8
+
9
+ - Use the same busy_handler function as will be in the sqlite3-ruby gem ([@fractaledmind](https://github.com/fractaledmind/activerecord-enhancedsqlite3-adapter/pull/11))
10
+ - Allow for isolated reading and writing connection pools ([@fractaledmind](https://github.com/fractaledmind/activerecord-enhancedsqlite3-adapter/pull/12))
11
+ - Ensure that even Rails 7.0 apps can use insert returning ([@fractaledmind](https://github.com/fractaledmind/activerecord-enhancedsqlite3-adapter/pull/8))
12
+ - Load virtual columns extension only if the app is running 7.1 or less ([@npezza93](https://github.com/fractaledmind/activerecord-enhancedsqlite3-adapter/pull/7))
13
+
3
14
  ## [0.5.0] - 2023-12-24
4
15
 
5
16
  - Load extensions installed via project-scoped `sqlpkg`
data/README.md CHANGED
@@ -21,6 +21,14 @@ This gem hooks into your Rails application to enhance the `SQLite3Adapter` autom
21
21
 
22
22
  Once installed, you can take advantage of the added features.
23
23
 
24
+ ### Configuration
25
+
26
+ One optional advanced feature is to have this gem isolate reading and writing connection pools. This is useful if you have a large amount of write operations and want to avoid blocking reads.
27
+
28
+ You can configure this gem via the Rails configuration object, under the `enhanced_sqlite3` key. Currently, only 1 configuration option is available:
29
+
30
+ * `isolate_connection_pools` - Whether or not to isolate reading from writing connection pools. See [below](#isolated-connection-pools) for more information.
31
+
24
32
  ### Generated columns
25
33
 
26
34
  You can now create `virtual` columns, both stored and dynamic. The [SQLite docs](https://www.sqlite.org/gencol.html) explain the difference:
@@ -81,6 +89,25 @@ default: &default
81
89
  - sqlite_ulid
82
90
  ```
83
91
 
92
+ ### Isolated connection pools
93
+
94
+ By default, Rails uses a single connection pool for both reading and writing. This can lead to contention if you have a large number of write operations. This gem allows you to isolate the connection pools for reading and writing.
95
+
96
+ To enable this feature, set the `isolate_connection_pools` configuration option to `true` in your `config/environments/*.rb` file or `config/application.rb` file:
97
+
98
+ ```ruby
99
+ config.enhanced_sqlite3.isolate_connection_pools = true
100
+ ```
101
+
102
+ If enabled, the gem will patch your application in 3 ways:
103
+
104
+ 1. define separate `reader` and `writer` database configurations
105
+ 2. activate Rails' automatic role database switching middleware, defaulting all requests to the `reader` connection pool
106
+ 3. patch the ActiveRecord `#transaction` method to switch to the `writer` connection pool for write operations
107
+ 4. patch the ActiveRecord `#log` method to log the database name for each database operation
108
+
109
+ This feature is experimental and may not work with all Rails configurations. Please report any issues you encounter.
110
+
84
111
  ## Development
85
112
 
86
113
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -1,3 +1,15 @@
1
1
  require "active_record"
2
2
  require "enhanced_sqlite3/version"
3
3
  require "enhanced_sqlite3/railtie"
4
+
5
+ module EnhancedSQLite3
6
+ Error = Class.new(StandardError)
7
+
8
+ mattr_writer :isolate_connection_pools
9
+
10
+ class << self
11
+ def isolate_connection_pools?
12
+ @isolate_connection_pools ||= @@isolate_connection_pools || false
13
+ end
14
+ end
15
+ end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # supports concatenation in default functions: https://github.com/rails/rails/pull/49287
4
- # supports insert returning values: https://github.com/rails/rails/pull/49290
5
4
  # supports configuring busy_handler: https://github.com/rails/rails/pull/49352
6
5
 
7
6
  require "active_record/connection_adapters/sqlite3_adapter"
8
7
  require "enhanced_sqlite3/supports_virtual_columns"
9
8
  require "enhanced_sqlite3/supports_deferrable_constraints"
9
+ require "enhanced_sqlite3/supports_insert_returning"
10
10
 
11
11
  module EnhancedSQLite3
12
12
  module Adapter
@@ -44,10 +44,28 @@ module EnhancedSQLite3
44
44
  configure_busy_handler_timeout
45
45
  check_version
46
46
  configure_pragmas
47
- load_extensions
47
+ configure_extensions
48
48
 
49
- EnhancedSQLite3::SupportsVirtualColumns.apply!
50
- EnhancedSQLite3::SupportsDeferrableConstraints.apply!
49
+ EnhancedSQLite3::SupportsVirtualColumns.apply! unless try(:supports_virtual_columns?)
50
+ EnhancedSQLite3::SupportsDeferrableConstraints.apply! unless try(:supports_deferrable_constraints?)
51
+ EnhancedSQLite3::SupportsInsertReturning.apply! unless try(:supports_insert_returning?)
52
+ end
53
+
54
+ # Patch the #transaction method to ensure that all transactions are sent to the writing role database connection pool.
55
+ def transaction(...)
56
+ ActiveRecord::Base.connected_to(role: ActiveRecord.writing_role, prevent_writes: false) do
57
+ super(...)
58
+ end
59
+ end
60
+
61
+ # Patch the #log method to ensure that all log messages are tagged with the database connection name.
62
+ def log(...)
63
+ db_connection_name = ActiveRecord::Base.connection_db_config.name
64
+ if Rails.logger.formatter.current_tags.include? db_connection_name
65
+ super
66
+ else
67
+ Rails.logger.tagged(db_connection_name) { super }
68
+ end
51
69
  end
52
70
 
53
71
  private
@@ -55,22 +73,17 @@ module EnhancedSQLite3
55
73
  def configure_busy_handler_timeout
56
74
  return unless @config.key?(:timeout)
57
75
 
58
- timeout = self.class.type_cast_config_to_integer(@config[:timeout])
59
- timeout_seconds = timeout.fdiv(1000)
60
- retry_interval = 6e-5 # 60 microseconds
76
+ timeout_milliseconds = self.class.type_cast_config_to_integer(@config[:timeout])
77
+ timeout_seconds = timeout_milliseconds.fdiv(1000)
61
78
 
62
79
  @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
80
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
81
+ if count.zero?
82
+ @timeout_deadline = now + timeout_seconds
83
+ elsif now > @timeout_deadline
84
+ next false
71
85
  else
72
- sleep(retry_interval)
73
- true
86
+ sleep(0.001)
74
87
  end
75
88
  end
76
89
  end
@@ -110,13 +123,8 @@ module EnhancedSQLite3
110
123
  end
111
124
  end
112
125
 
113
- def load_extensions
126
+ def configure_extensions
114
127
  @raw_connection.enable_load_extension(true)
115
- # first, load any extensions installed via `sqlpkg`
116
- Dir.glob(".sqlpkg/**/*.{dll,so,dylib}") do |extension_path|
117
- @raw_connection.load_extension(extension_path)
118
- end
119
- # then, load any extensions specified in the `database.yml`
120
128
  @config.fetch(:extensions, []).each do |extension_name|
121
129
  require extension_name
122
130
  extension_classname = extension_name.camelize
@@ -2,16 +2,63 @@
2
2
 
3
3
  require "rails/railtie"
4
4
  require "enhanced_sqlite3/adapter"
5
+ require "enhanced_sqlite3/resolver"
5
6
 
6
7
  module EnhancedSQLite3
7
8
  class Railtie < ::Rails::Railtie
9
+ config.enhanced_sqlite3 = ActiveSupport::OrderedOptions.new
10
+
11
+ initializer "enhanced_sqlite3.config" do
12
+ config.enhanced_sqlite3.each do |name, value|
13
+ EnhancedSQLite3.public_send(:"#{name}=", value)
14
+ end
15
+ end
16
+
8
17
  # Enhance the SQLite3 ActiveRecord adapter with optimized defaults
9
18
  initializer "enhanced_sqlite3.enhance_active_record_sqlite3adapter" do |app|
10
19
  ActiveSupport.on_load(:active_record_sqlite3adapter) do
11
- # self refers to `SQLite3Adapter` here,
12
- # so we can call .prepend
20
+ # self refers to `SQLite3Adapter` here
13
21
  prepend EnhancedSQLite3::Adapter
14
22
  end
15
23
  end
24
+
25
+ # Enhance the application with isolated reading and writing connection pools
26
+ initializer "enhanced_sqlite3.setup_isolated_connection_pools" do |app|
27
+ next unless EnhancedSQLite3.isolate_connection_pools?
28
+
29
+ ActiveSupport.on_load(:active_record) do
30
+ # self refers to `ActiveRecord::Base` here
31
+ env_configs = configurations.configs_for env_name: Rails.env
32
+ remaining_configs = configurations.configurations.reject { |configuration| env_configs.include? configuration }
33
+ if env_configs.one?
34
+ config = env_configs.first
35
+ reader = ActiveRecord::DatabaseConfigurations::HashConfig.new(
36
+ Rails.env, "reader", config.configuration_hash.merge(readonly: true)
37
+ )
38
+ writer = ActiveRecord::DatabaseConfigurations::HashConfig.new(
39
+ Rails.env, "writer", config.configuration_hash.merge(pool: 1)
40
+ )
41
+
42
+ # Replace the single production configuration with two separate reader and writer configurations
43
+ self.configurations = remaining_configs + [writer, reader]
44
+ else
45
+ reader = env_configs.find { |config| config.name == "reader" }
46
+ writer = env_configs.find { |config| config.name == "writer" }
47
+
48
+ # Ensure that that there is a reader and writer configuration for the current Rails environment
49
+ raise Error.new("#{Rails.env} has #{env_configs.size} configurations") unless reader && writer
50
+ end
51
+
52
+ connects_to database: {writing: :writer, reading: :reader}
53
+ end
54
+
55
+ # Since we aren't actually using separate databases, only separate connections,
56
+ # we don't need to ensure that requests "read your own writes" with a `delay`
57
+ config.active_record.database_selector = {delay: 0}
58
+ # Use our custom resolver to ensure that benchmarking requests are sent to the reading database connection
59
+ config.active_record.database_resolver = EnhancedSQLite3::Resolver
60
+ # Keep Rails' default resolver context
61
+ config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
62
+ end
16
63
  end
17
64
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EnhancedSQLite3
4
+ class Resolver < ActiveRecord::Middleware::DatabaseSelector::Resolver
5
+ def reading_request?(request)
6
+ true
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # see: https://github.com/rails/rails/pull/49290
4
+ module EnhancedSQLite3
5
+ module SupportsInsertReturning
6
+ def self.apply!
7
+ EnhancedSQLite3::Adapter.include(Adapter)
8
+ ActiveRecord::ConnectionAdapters::SQLite3::DatabaseStatements.include(DatabaseStatements)
9
+ end
10
+
11
+ module Adapter
12
+ def supports_insert_returning?
13
+ database_version >= "3.35.0"
14
+ end
15
+
16
+ def return_value_after_insert?(column) # :nodoc:
17
+ column.auto_populated?
18
+ end
19
+
20
+ def use_insert_returning?
21
+ @use_insert_returning ||= @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
22
+ end
23
+
24
+ def build_insert_sql(insert) # :nodoc:
25
+ sql = super(insert)
26
+ sql << " RETURNING #{insert.returning}" if insert.returning
27
+ sql
28
+ end
29
+ end
30
+
31
+ module DatabaseStatements
32
+ def sql_for_insert(sql, pk, binds, returning) # :nodoc:
33
+ if supports_insert_returning?
34
+ if pk.nil?
35
+ # Extract the table from the insert sql. Yuck.
36
+ table_ref = extract_table_ref_from_insert_sql(sql)
37
+ pk = primary_key(table_ref) if table_ref
38
+ end
39
+
40
+ returning_columns = returning || Array(pk)
41
+
42
+ returning_columns_statement = returning_columns.map { |c| quote_column_name(c) }.join(", ")
43
+ sql = "#{sql} RETURNING #{returning_columns_statement}" if returning_columns.any?
44
+ end
45
+
46
+ [sql, binds]
47
+ end
48
+
49
+ def extract_table_ref_from_insert_sql(sql)
50
+ if sql =~ /into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im
51
+ $1.strip
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,3 +1,3 @@
1
1
  module EnhancedSQLite3
2
- VERSION = "0.5.0"
2
+ VERSION = "0.7.0"
3
3
  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.5.0
4
+ version: 0.7.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-12-23 00:00:00.000000000 Z
11
+ date: 2024-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -28,14 +28,14 @@ dependencies:
28
28
  name: sqlite3
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '1.6'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.6'
41
41
  - !ruby/object:Gem::Dependency
@@ -111,7 +111,9 @@ files:
111
111
  - lib/activerecord-enhancedsqlite3-adapter.rb
112
112
  - lib/enhanced_sqlite3/adapter.rb
113
113
  - lib/enhanced_sqlite3/railtie.rb
114
+ - lib/enhanced_sqlite3/resolver.rb
114
115
  - lib/enhanced_sqlite3/supports_deferrable_constraints.rb
116
+ - lib/enhanced_sqlite3/supports_insert_returning.rb
115
117
  - lib/enhanced_sqlite3/supports_virtual_columns.rb
116
118
  - lib/enhanced_sqlite3/version.rb
117
119
  homepage: https://github.com/fractaledmind/activerecord-enhancedsqlite3-adapter