wasmify-rails 0.1.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 +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +23 -0
- data/README.md +168 -0
- data/lib/active_record/connection_adapters/nulldb_adapter/checkpoint.rb +13 -0
- data/lib/active_record/connection_adapters/nulldb_adapter/column.rb +4 -0
- data/lib/active_record/connection_adapters/nulldb_adapter/configuration.rb +5 -0
- data/lib/active_record/connection_adapters/nulldb_adapter/core.rb +391 -0
- data/lib/active_record/connection_adapters/nulldb_adapter/empty_result.rb +41 -0
- data/lib/active_record/connection_adapters/nulldb_adapter/index_definition.rb +5 -0
- data/lib/active_record/connection_adapters/nulldb_adapter/null_object.rb +13 -0
- data/lib/active_record/connection_adapters/nulldb_adapter/quoting.rb +3 -0
- data/lib/active_record/connection_adapters/nulldb_adapter/statement.rb +15 -0
- data/lib/active_record/connection_adapters/nulldb_adapter/table_definition.rb +23 -0
- data/lib/active_record/connection_adapters/nulldb_adapter.rb +67 -0
- data/lib/active_record/connection_adapters/sqlite3_wasm_adapter.rb +163 -0
- data/lib/active_storage/null_delivery.rb +15 -0
- data/lib/generators/wasmify/install/USAGE +5 -0
- data/lib/generators/wasmify/install/install_generator.rb +73 -0
- data/lib/generators/wasmify/install/templates/config/environments/wasm.rb +30 -0
- data/lib/generators/wasmify/install/templates/config/wasmify.yml +17 -0
- data/lib/generators/wasmify/pwa/USAGE +5 -0
- data/lib/generators/wasmify/pwa/pwa_generator.rb +13 -0
- data/lib/generators/wasmify/pwa/templates/pwa/README.md +40 -0
- data/lib/generators/wasmify/pwa/templates/pwa/boot.html +102 -0
- data/lib/generators/wasmify/pwa/templates/pwa/boot.js +96 -0
- data/lib/generators/wasmify/pwa/templates/pwa/database.js +18 -0
- data/lib/generators/wasmify/pwa/templates/pwa/index.html +2 -0
- data/lib/generators/wasmify/pwa/templates/pwa/package.json.tt +20 -0
- data/lib/generators/wasmify/pwa/templates/pwa/rails.sw.js +115 -0
- data/lib/generators/wasmify/pwa/templates/pwa/vite.config.js +29 -0
- data/lib/image_processing/null.rb +22 -0
- data/lib/wasmify/rails/builder.rb +45 -0
- data/lib/wasmify/rails/configuration.rb +41 -0
- data/lib/wasmify/rails/packer.rb +60 -0
- data/lib/wasmify/rails/railtie.rb +11 -0
- data/lib/wasmify/rails/shim.rb +65 -0
- data/lib/wasmify/rails/shims/io/console/size.rb +0 -0
- data/lib/wasmify/rails/shims/io/console.rb +11 -0
- data/lib/wasmify/rails/shims/io/wait.rb +0 -0
- data/lib/wasmify/rails/shims/ipaddr.rb +7 -0
- data/lib/wasmify/rails/shims/nio.rb +0 -0
- data/lib/wasmify/rails/shims/nokogiri/nokogiri.rb +0 -0
- data/lib/wasmify/rails/shims/nokogiri.rb +0 -0
- data/lib/wasmify/rails/shims/rails-html-sanitizer.rb +163 -0
- data/lib/wasmify/rails/shims/socket.rb +21 -0
- data/lib/wasmify/rails/shims/sqlite3.rb +0 -0
- data/lib/wasmify/rails/tasks.rake +120 -0
- data/lib/wasmify/rails/version.rb +7 -0
- data/lib/wasmify/rails/wasmtimer.rb +31 -0
- data/lib/wasmify-rails.rb +25 -0
- metadata +200 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 442a1c699d7e906546d63b1d5c6c451652d65ecd651bd16fa72d98841a845acf
|
|
4
|
+
data.tar.gz: d8dac2abf684945cc8c9fbd10d3ed6c6236c9535c3f7c3974501638b7be6f607
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 14b9d7c161e577b80d5d9b92d6cb6a7294203f535f938423b194e4adf204541f0d93c4efa466847d2546bf8a919aa96c82037613fb31c7114f3fe1c4debaa5ee
|
|
7
|
+
data.tar.gz: 61c8ee0c9f26376f53e22bff246bba40c9d038094d846c17cc7b022f600344c70142777901d28d60eb41a5c8471c4dacbf6c188166a0aa76e1b88554c52900e9
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Copyright (c) 2024 Vladimir Dementyev
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
data/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
[](https://rubygems.org/gems/wasmify-rails)
|
|
2
|
+
|
|
3
|
+
# Wasmify Rails
|
|
4
|
+
|
|
5
|
+
This gem provides tools and extensions to compile Rails applications to WebAssembly.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Adding to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Gemfile
|
|
13
|
+
gem "wasmify-rails", group: [:development, :wasm]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
This gem comes with a few commands (Rake tasks) to help you get started with packing your Rails app into a Wasm module.
|
|
19
|
+
|
|
20
|
+
### Step 1: `bin/rails wasmify:install`
|
|
21
|
+
|
|
22
|
+
Run the following command to preconfigure your project for _wasmification_:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
bin/rails wasmify:install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The script will tweak you configuration files and create a couple of new ones:
|
|
29
|
+
|
|
30
|
+
- `config/environments/wasm.rb` — a dedicated Rails environment based on the `production` environment to be used in Wasm builds.
|
|
31
|
+
|
|
32
|
+
- `config/wasmify.yml` — this is a configuration file for different _wasmify_ commands (see below).
|
|
33
|
+
|
|
34
|
+
### Step 2: `bin/rails wasmify:build:core`
|
|
35
|
+
|
|
36
|
+
Now, it's time to build a ruby.wasm with all your project dependencies (from the Gemfile). This is gonna be a base Wasm module
|
|
37
|
+
for your project but not including the project files.
|
|
38
|
+
|
|
39
|
+
Prior to running the build command, you MUST update your Gemfile to mark the gems that you need in the Wasm environment
|
|
40
|
+
with the `:wasm` group. For example:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
# Rails is required.
|
|
44
|
+
# NOTE: don't forget about the default group — you need it for the regular Rails environments.
|
|
45
|
+
gem "rails", group: [:default, :wasm]
|
|
46
|
+
|
|
47
|
+
# We don't need Puma though
|
|
48
|
+
gem "puma"
|
|
49
|
+
|
|
50
|
+
# You can also use group ... do ... end syntax
|
|
51
|
+
group :default, :wasm do
|
|
52
|
+
gem "propshaft"
|
|
53
|
+
gem "importmap-rails"
|
|
54
|
+
gem "turbo-rails"
|
|
55
|
+
gem "stimulus-rails"
|
|
56
|
+
end
|
|
57
|
+
...
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**NOTE:** If you use `ruby file: ".ruby-version"` in your Gemfile, you should probably configure the Ruby version for Wasm platform
|
|
61
|
+
a bit differently (since patch versions might not match). For example:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
if RUBY_PLATFORM =~ /wasm/
|
|
65
|
+
ruby "3.3.3"
|
|
66
|
+
else
|
|
67
|
+
ruby file: ".ruby-version"
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Now, try to run the following command:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
$ bin/rails wasmify:build:core
|
|
75
|
+
|
|
76
|
+
...
|
|
77
|
+
INFO: Packaging gem: rails-7.2.1
|
|
78
|
+
...
|
|
79
|
+
INFO: Size: 77.92 MB
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
If it succeeds then you're good to go further. However, 99% that it will fail with some compilation error.
|
|
83
|
+
You must identify the gem that caused the problem (usually, it happens when we failed to compile a C extension to Wasm—not every C extension is Wasm-compilable).
|
|
84
|
+
Then, you can either update your Gemfile to exclude the problematic gem from the Wasm group or you can update your `config/wasmify.yml` and
|
|
85
|
+
add the gem to the `exclude_gems` list.
|
|
86
|
+
|
|
87
|
+
Repeat until the `bin/rails wasmify:build:core` succeeds.
|
|
88
|
+
|
|
89
|
+
You can also verify that the resulting Wasm module works by running:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
$ bin/rails wasmify:build:core:verify
|
|
93
|
+
|
|
94
|
+
Your Rails version is: 7.2.1 [wasm32-wasi]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
If this command fails, try to iterate on gem exclusions and rebuild the core module.
|
|
98
|
+
|
|
99
|
+
### Step 3: `bin/rails wasmify:pack:core`
|
|
100
|
+
|
|
101
|
+
Now, we're ready to pack the whole application into a single Wasm module.
|
|
102
|
+
For that, run the following command:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
$ bin/rails wasmify:pack:core
|
|
106
|
+
|
|
107
|
+
Packed the application to tmp/wasmify/app-core.wasm
|
|
108
|
+
Size: 103 MB
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
That should succeeds given that the previous step was successful.
|
|
112
|
+
|
|
113
|
+
Now, let's try to boot the application and see if it works:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
$ bin/rails wasmify:pack:core:verify
|
|
117
|
+
|
|
118
|
+
Initializing Rails application...
|
|
119
|
+
Rails application initialized!
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
If the verification passes, you can proceed to the final step — building the Wasm module to be used on the web.
|
|
123
|
+
If it fails, check out the error message and try to fix the issues (usually, configuration related).
|
|
124
|
+
|
|
125
|
+
### Step 4: `bin/rails wasmify:pwa`
|
|
126
|
+
|
|
127
|
+
We're ready to launch our Rails application fully within a browser!
|
|
128
|
+
|
|
129
|
+
For that, you can use our starter Vite PWA application that can be generated via the following command:
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
bin/rails wasmify:pwa
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Then, update your `config/wasmify.yml` to specify the path to the PWA app as the output:
|
|
136
|
+
|
|
137
|
+
```yml
|
|
138
|
+
output_dir: "pwa"
|
|
139
|
+
# ...
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Now, create the final Wasm module:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
bin/rails wasmify:pack
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
And go to the `pwa` for the instructions on how to launch the application.
|
|
149
|
+
|
|
150
|
+
## Roadmap
|
|
151
|
+
|
|
152
|
+
- PGLite support (see [this example](https://github.com/kateinoigakukun/mastodon/blob/fff2e4a626a20a616c546ddf4f91766abaf1133a/pwa/dist/pglite.rb#L1))
|
|
153
|
+
- Active Storage OPFS service
|
|
154
|
+
- File uploads support (multipart/form-data)
|
|
155
|
+
- Background jobs support
|
|
156
|
+
- WASI Preview 2 support (also [this](https://github.com/kateinoigakukun/mastodon/tree/katei/wasmify))
|
|
157
|
+
|
|
158
|
+
## Contributing
|
|
159
|
+
|
|
160
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/](https://github.com/).
|
|
161
|
+
|
|
162
|
+
## Credits
|
|
163
|
+
|
|
164
|
+
The `nulldb` adapter for Active Record (used for tests) is adopted from [this project](https://github.com/nulldb/nulldb).
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
class ActiveRecord::ConnectionAdapters::NullDBAdapter < ActiveRecord::ConnectionAdapters::AbstractAdapter
|
|
2
|
+
|
|
3
|
+
# A convenience method for integratinginto RSpec. See README for example of
|
|
4
|
+
# use.
|
|
5
|
+
def self.insinuate_into_spec(config)
|
|
6
|
+
config.before :all do
|
|
7
|
+
ActiveRecord::Base.establish_connection(:adapter => :nulldb)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
config.after :all do
|
|
11
|
+
ActiveRecord::Base.establish_connection(:test)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Recognized options:
|
|
16
|
+
#
|
|
17
|
+
# [+:schema+] path to the schema file, relative to Rails.root
|
|
18
|
+
# [+:table_definition_class_name+] table definition class
|
|
19
|
+
# (e.g. ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition for Postgres) or nil.
|
|
20
|
+
def initialize(config={})
|
|
21
|
+
@log = StringIO.new
|
|
22
|
+
@logger = Logger.new(@log)
|
|
23
|
+
@last_unique_id = 0
|
|
24
|
+
@tables = {'schema_info' => new_table_definition(nil)}
|
|
25
|
+
@indexes = Hash.new { |hash, key| hash[key] = [] }
|
|
26
|
+
@schema_path = config.fetch(:schema){ "db/schema.rb" }
|
|
27
|
+
@config = config.merge(:adapter => :nulldb)
|
|
28
|
+
super *initialize_args
|
|
29
|
+
@visitor ||= Arel::Visitors::ToSql.new self if defined?(Arel::Visitors::ToSql)
|
|
30
|
+
|
|
31
|
+
if config[:table_definition_class_name]
|
|
32
|
+
ActiveRecord::ConnectionAdapters::NullDBAdapter.send(:remove_const, 'TableDefinition')
|
|
33
|
+
ActiveRecord::ConnectionAdapters::NullDBAdapter.const_set('TableDefinition',
|
|
34
|
+
self.class.const_get(config[:table_definition_class_name]))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
register_types
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# A log of every statement that has been "executed" by this connection adapter
|
|
41
|
+
# instance.
|
|
42
|
+
def execution_log
|
|
43
|
+
(@execution_log ||= [])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# A log of every statement that has been "executed" since the last time
|
|
47
|
+
# #checkpoint! was called, or since the connection was created.
|
|
48
|
+
def execution_log_since_checkpoint
|
|
49
|
+
checkpoint_index = @execution_log.rindex(Checkpoint.new)
|
|
50
|
+
checkpoint_index = checkpoint_index ? checkpoint_index + 1 : 0
|
|
51
|
+
@execution_log[(checkpoint_index..-1)]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Inserts a checkpoint in the log. See also #execution_log_since_checkpoint.
|
|
55
|
+
def checkpoint!
|
|
56
|
+
self.execution_log << Checkpoint.new
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def adapter_name
|
|
60
|
+
"NullDB"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def supports_migrations?
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def connect
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def reconnect
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def create_table(table_name, options = {})
|
|
74
|
+
table_definition = new_table_definition(self, table_name, options.delete(:temporary), options)
|
|
75
|
+
|
|
76
|
+
unless options[:id] == false
|
|
77
|
+
table_definition.primary_key(options[:primary_key] || "id")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
yield table_definition if block_given?
|
|
81
|
+
|
|
82
|
+
@tables[table_name.to_s] = table_definition
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def rename_table(table_name, new_name)
|
|
86
|
+
table_definition = @tables.delete(table_name.to_s)
|
|
87
|
+
|
|
88
|
+
table_definition.name = new_name.to_s
|
|
89
|
+
@tables[new_name.to_s] = table_definition
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def add_index(table_name, column_names, **options)
|
|
93
|
+
options[:unique] = false unless options.key?(:unique)
|
|
94
|
+
column_names = Array.wrap(column_names).map(&:to_s)
|
|
95
|
+
|
|
96
|
+
index, index_type, ignore = add_index_options(table_name, column_names, **options)
|
|
97
|
+
|
|
98
|
+
if index.is_a?(ActiveRecord::ConnectionAdapters::IndexDefinition)
|
|
99
|
+
@indexes[table_name] << index
|
|
100
|
+
else
|
|
101
|
+
# Rails < 6.1
|
|
102
|
+
@indexes[table_name] << IndexDefinition.new(table_name, index, (index_type == 'UNIQUE'), column_names, [], [])
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Rails 6.1+
|
|
107
|
+
if ActiveRecord::VERSION::MAJOR >= 7 || (ActiveRecord::VERSION::MAJOR >= 6 and ActiveRecord::VERSION::MINOR > 0)
|
|
108
|
+
def remove_index(table_name, column_name = nil, **options )
|
|
109
|
+
index_name = index_name_for_remove(table_name, column_name, options)
|
|
110
|
+
index = @indexes[table_name].reject! { |index| index.name == index_name }
|
|
111
|
+
end
|
|
112
|
+
else
|
|
113
|
+
def remove_index(table_name, options = {} )
|
|
114
|
+
index_name = index_name_for_remove(table_name, options)
|
|
115
|
+
index = @indexes[table_name].reject! { |index| index.name == index_name }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def add_fk_constraint(*args)
|
|
120
|
+
# NOOP
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def add_pk_constraint(*args)
|
|
124
|
+
# NOOP
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def enable_extension(*)
|
|
128
|
+
# NOOP
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Retrieve the table names defined by the schema
|
|
132
|
+
def tables
|
|
133
|
+
@tables.keys.map(&:to_s)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def views
|
|
137
|
+
[] # TODO: Implement properly if needed - This is new method in rails
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Retrieve table columns as defined by the schema
|
|
141
|
+
def columns(table_name, name = nil)
|
|
142
|
+
if @tables.size <= 1
|
|
143
|
+
ActiveRecord::Migration.verbose = false
|
|
144
|
+
schema_path = if Pathname(@schema_path).absolute?
|
|
145
|
+
@schema_path
|
|
146
|
+
else
|
|
147
|
+
File.join(NullDB.configuration.project_root, @schema_path)
|
|
148
|
+
end
|
|
149
|
+
Kernel.load(schema_path)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
if table = @tables[table_name]
|
|
153
|
+
table.columns.map do |col_def|
|
|
154
|
+
col_args = default_column_arguments(col_def)
|
|
155
|
+
ActiveRecord::ConnectionAdapters::NullDBAdapter::Column.new(*col_args)
|
|
156
|
+
end
|
|
157
|
+
else
|
|
158
|
+
[]
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Retrieve table indexes as defined by the schema
|
|
163
|
+
def indexes(table_name, name = nil)
|
|
164
|
+
@indexes[table_name]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def execute(statement, name = nil)
|
|
168
|
+
self.execution_log << Statement.new(entry_point, statement)
|
|
169
|
+
NullObject.new
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def exec_query(statement, name = 'SQL', binds = [], options = {})
|
|
173
|
+
internal_exec_query(statement, name, binds, **options)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def internal_exec_query(statement, name = 'SQL', binds = [], prepare: false, async: false)
|
|
177
|
+
self.execution_log << Statement.new(entry_point, statement)
|
|
178
|
+
EmptyResult.new
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def select_rows(statement, name = nil, binds = [], async: false)
|
|
182
|
+
[].tap do
|
|
183
|
+
self.execution_log << Statement.new(entry_point, statement)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def insert(statement, name = nil, primary_key = nil, object_id = nil, sequence_name = nil, binds = [], returning: nil)
|
|
188
|
+
with_entry_point(:insert) do
|
|
189
|
+
super(statement, name, primary_key, object_id, sequence_name)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
result = object_id || next_unique_id
|
|
193
|
+
|
|
194
|
+
returning ? [result] : result
|
|
195
|
+
end
|
|
196
|
+
alias :create :insert
|
|
197
|
+
|
|
198
|
+
def update(statement, name=nil, binds = [])
|
|
199
|
+
with_entry_point(:update) do
|
|
200
|
+
super(statement, name)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def delete(statement, name=nil, binds = [])
|
|
205
|
+
with_entry_point(:delete) do
|
|
206
|
+
super(statement, name).size
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def select_all(statement, name=nil, binds = [], options = {})
|
|
211
|
+
with_entry_point(:select_all) do
|
|
212
|
+
super(statement, name)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def select_one(statement, name=nil, binds = [])
|
|
217
|
+
with_entry_point(:select_one) do
|
|
218
|
+
super(statement, name)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def select_value(statement, name=nil, binds = [])
|
|
223
|
+
with_entry_point(:select_value) do
|
|
224
|
+
super(statement, name)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def select_values(statement, name=nil)
|
|
229
|
+
with_entry_point(:select_values) do
|
|
230
|
+
super(statement, name)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def primary_key(table_name)
|
|
235
|
+
columns(table_name).detect { |col| col.type == :primary_key }.try(:name)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def add_column(table_name, column_name, type, **options)
|
|
239
|
+
super
|
|
240
|
+
|
|
241
|
+
table_meta = @tables[table_name.to_s]
|
|
242
|
+
return unless table_meta
|
|
243
|
+
|
|
244
|
+
table_meta.column column_name, type, **options
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def change_column(table_name, column_name, type, options = {})
|
|
248
|
+
table_meta = @tables[table_name.to_s]
|
|
249
|
+
column = table_meta.columns.find { |column| column.name == column_name.to_s }
|
|
250
|
+
return unless column
|
|
251
|
+
|
|
252
|
+
column.type = type
|
|
253
|
+
column.options = options if options
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def rename_column(table_name, column_name, new_column_name)
|
|
257
|
+
table_meta = @tables[table_name.to_s]
|
|
258
|
+
column = table_meta.columns.find { |column| column.name == column_name.to_s }
|
|
259
|
+
return unless column
|
|
260
|
+
|
|
261
|
+
column.name = new_column_name
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def change_column_default(table_name, column_name, default_or_changes)
|
|
265
|
+
table_meta = @tables[table_name.to_s]
|
|
266
|
+
column = table_meta.columns.find { |column| column.name == column_name.to_s }
|
|
267
|
+
|
|
268
|
+
return unless column
|
|
269
|
+
|
|
270
|
+
if default_or_changes.kind_of? Hash
|
|
271
|
+
column.default = default_or_changes[:to]
|
|
272
|
+
else
|
|
273
|
+
column.default = default_or_changes
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
protected
|
|
278
|
+
|
|
279
|
+
def select(statement, name = nil, binds = [], prepare: nil, async: nil, **)
|
|
280
|
+
EmptyResult.new.tap do |r|
|
|
281
|
+
r.bind_column_meta(columns_for(name))
|
|
282
|
+
self.execution_log << Statement.new(entry_point, statement)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
private
|
|
287
|
+
|
|
288
|
+
def columns_for(table_name)
|
|
289
|
+
table_meta = @tables[table_name]
|
|
290
|
+
return [] unless table_meta
|
|
291
|
+
table_meta.columns
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def next_unique_id
|
|
295
|
+
@last_unique_id += 1
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def with_entry_point(method)
|
|
299
|
+
if entry_point.nil?
|
|
300
|
+
with_thread_local_variable(:entry_point, method) do
|
|
301
|
+
yield
|
|
302
|
+
end
|
|
303
|
+
else
|
|
304
|
+
yield
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def entry_point
|
|
309
|
+
Thread.current[:entry_point]
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def with_thread_local_variable(name, value)
|
|
313
|
+
old_value = Thread.current[name]
|
|
314
|
+
Thread.current[name] = value
|
|
315
|
+
begin
|
|
316
|
+
yield
|
|
317
|
+
ensure
|
|
318
|
+
Thread.current[name] = old_value
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def includes_column?
|
|
323
|
+
false
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def new_table_definition(adapter = nil, table_name = nil, is_temporary = nil, options = {})
|
|
327
|
+
case ::ActiveRecord::VERSION::MAJOR
|
|
328
|
+
when 6, 7
|
|
329
|
+
TableDefinition.new(self, table_name, temporary: is_temporary, options: options.except(:id))
|
|
330
|
+
when 5
|
|
331
|
+
TableDefinition.new(table_name, is_temporary, options.except(:id), nil)
|
|
332
|
+
else
|
|
333
|
+
raise "Unsupported ActiveRecord version #{::ActiveRecord::VERSION::STRING}"
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def default_column_arguments(col_def)
|
|
338
|
+
[
|
|
339
|
+
col_def.name.to_s,
|
|
340
|
+
col_def.default.present? ? col_def.default.to_s : nil,
|
|
341
|
+
sql_type_definition(col_def),
|
|
342
|
+
col_def.null.nil? || col_def.null
|
|
343
|
+
]
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def sql_type_definition(col_def)
|
|
347
|
+
ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(
|
|
348
|
+
type: col_def.type,
|
|
349
|
+
sql_type: col_def.type.to_s,
|
|
350
|
+
limit: col_def.limit
|
|
351
|
+
)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def initialize_args
|
|
355
|
+
[nil, @logger, @config]
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Register types only once to avoid ActiveRecord::TypeConflictError
|
|
359
|
+
# in ActiveRecord::Type::Registration#<=>
|
|
360
|
+
REGISTRATION_MUTEX = Mutex.new
|
|
361
|
+
|
|
362
|
+
def register_types
|
|
363
|
+
REGISTRATION_MUTEX.synchronize do
|
|
364
|
+
return if self.class.types_registered
|
|
365
|
+
|
|
366
|
+
self.class.types_registered = true
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
ActiveRecord::Type.register(
|
|
370
|
+
:primary_key,
|
|
371
|
+
ActiveModel::Type::Integer,
|
|
372
|
+
adapter: adapter_name,
|
|
373
|
+
override: true
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
ActiveRecord::Type.add_modifier({ array: true }, DummyOID, adapter: :nulldb)
|
|
377
|
+
ActiveRecord::Type.add_modifier({ range: true }, DummyOID, adapter: :nulldb)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
class << self
|
|
381
|
+
attr_accessor :types_registered
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
class DummyOID < ActiveModel::Type::Value
|
|
385
|
+
attr_reader :subtype
|
|
386
|
+
|
|
387
|
+
def initialize(*args)
|
|
388
|
+
@subtype = args.first
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
class ActiveRecord::ConnectionAdapters::NullDBAdapter
|
|
2
|
+
|
|
3
|
+
class EmptyResult < Array
|
|
4
|
+
attr_reader :column_types
|
|
5
|
+
|
|
6
|
+
def bind_column_meta(columns)
|
|
7
|
+
@columns = columns
|
|
8
|
+
return if columns.empty?
|
|
9
|
+
|
|
10
|
+
@column_types = columns.reduce({}) do |ctypes, col|
|
|
11
|
+
ctypes[col.name] = ActiveRecord::Type.lookup(col.type)
|
|
12
|
+
ctypes
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def columns
|
|
17
|
+
@columns ||= []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def column_types
|
|
21
|
+
@column_types ||= {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def cast_values(type_overrides = nil)
|
|
25
|
+
rows
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def rows
|
|
29
|
+
[]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def >(num)
|
|
33
|
+
rows.size > num
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def includes_column?(name)
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|