rubydb-activerecord 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/README.md +133 -0
- data/lib/active_record/connection_adapters/rubydb_adapter.rb +893 -0
- data/rubydb-activerecord.gemspec +32 -0
- metadata +124 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: becb1537aee9af710911f9a816a27b272dc5bde4889dbed351c1131ba9184224
|
|
4
|
+
data.tar.gz: dc0e6243c319dbaaa738a0470032655f772fa98339de1af8f24066b66e321e3e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 72aeae225241636cfa38e01553fa35a79a0d411330ef1bd894d8bbf88f5497e900e71924db4f7cf5b7bb797ab974e4d07f316cea6b3d56255a75314aaafe6db3
|
|
7
|
+
data.tar.gz: e09c7ce51be379034426dc82f7047b74a954b8979d6c716f37cca70d6add2669158f9f552b688adad67aa501959fa54c2a89d4e1b28dafaca3bd50b58dbb971f
|
data/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# rubydb-activerecord
|
|
2
|
+
|
|
3
|
+
`rubydb-activerecord` is the ActiveRecord adapter for [RubyDB](https://github.com/aldanedev-create/rubydb). It lets Rails applications use RubyDB through the normal model, relation, transaction, migration, schema, and connection-pool APIs.
|
|
4
|
+
|
|
5
|
+
This gem is an adapter to RubyDB's SQL engine. It is not a PostgreSQL, MySQL, or SQLite wire-protocol compatibility layer, and it does not make arbitrary dialect-specific SQL portable. Validate your application's queries and migrations against the exact Ruby, Rails, RubyDB, operating-system, and deployment versions you will run.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Ruby 3.3 or newer
|
|
10
|
+
- RubyDB 0.1.x
|
|
11
|
+
- ActiveRecord 7.1, 7.2, or 8.0 (the dependency range is `>= 7.1`, `< 8.1`)
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
Add both gems to the Rails application's `Gemfile`:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "rubydb"
|
|
19
|
+
gem "rubydb-activerecord"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then run:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
bundle install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The adapter registers itself under the `rubydb` adapter name when it is
|
|
29
|
+
required by Bundler. For a manually loaded application, require it explicitly:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require "rubydb"
|
|
33
|
+
require "active_record/connection_adapters/rubydb_adapter"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Development: embedded database
|
|
37
|
+
|
|
38
|
+
Embedded mode is convenient for local development and a controlled
|
|
39
|
+
single-process application. The process owns the database file:
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
# config/database.yml
|
|
43
|
+
development:
|
|
44
|
+
adapter: rubydb
|
|
45
|
+
embedded: true
|
|
46
|
+
database: <%= Rails.root.join("tmp/development.rdb") %>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Run migrations normally:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
bin/rails db:migrate
|
|
53
|
+
bin/rails console
|
|
54
|
+
bin/rails server
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Do not open the same embedded path from multiple independent Rails processes.
|
|
58
|
+
For multiple web workers, job workers, or hosts, use a managed RubyDB server.
|
|
59
|
+
|
|
60
|
+
## Production: managed server
|
|
61
|
+
|
|
62
|
+
Use a network connection when the application has more than one process or
|
|
63
|
+
when the database must be operated independently from the application. Keep
|
|
64
|
+
credentials in the platform secret manager and map them into `database.yml`:
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
# config/database.yml
|
|
68
|
+
production:
|
|
69
|
+
adapter: rubydb
|
|
70
|
+
embedded: false
|
|
71
|
+
url: <%= ENV.fetch("RUBYDB_URL") %>
|
|
72
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS", "5") %>
|
|
73
|
+
timeout: <%= ENV.fetch("RUBYDB_TIMEOUT", "30") %>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Use `rubydb://` for a plain private-network connection or `rubydbs://` for TLS:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
rubydbs://app_user:URL_ENCODED_PASSWORD@db.internal.example:7432/app?verify_peer=true&ca_file=%2Fetc%2Frubydb%2Ftls%2Fca.crt
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The server must be deployed separately with persistent storage,
|
|
83
|
+
authentication, TLS, backups, monitoring, resource limits, and a tested
|
|
84
|
+
restore procedure. Size its connection limit for the total Rails pool across
|
|
85
|
+
all application processes, with headroom for administration and replication.
|
|
86
|
+
See the repository's [Rails database configuration](../../docs/rails/database-yml.md)
|
|
87
|
+
and [production guidance](../../docs/rails/production.md).
|
|
88
|
+
|
|
89
|
+
## Adapter surface
|
|
90
|
+
|
|
91
|
+
The release test suite exercises:
|
|
92
|
+
|
|
93
|
+
- ActiveRecord CRUD, binds, quoted identifiers, type casting, and false/zero values
|
|
94
|
+
- transactions and savepoints
|
|
95
|
+
- `joins`, qualified filters, ordering, eager loading, and nested associations
|
|
96
|
+
- table/column/index/foreign-key introspection
|
|
97
|
+
- Rails migrations, defaults, indexes, schema dumps, and populated-table changes
|
|
98
|
+
- embedded and network connection setup paths
|
|
99
|
+
|
|
100
|
+
RubyDB currently has documented SQL and schema boundaries. Features outside
|
|
101
|
+
the tested surface—such as dialect-specific extensions, generated columns,
|
|
102
|
+
complex table rebuilds, or application-specific Arel—need explicit tests
|
|
103
|
+
before deployment.
|
|
104
|
+
|
|
105
|
+
## Validation
|
|
106
|
+
|
|
107
|
+
Run the adapter's integration suite from this directory:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
bundle install
|
|
111
|
+
bundle exec rspec spec
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Run the repository's Rails-focused checks from the repository root:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
bundle exec rspec spec/rails_adapter_live_engine_spec.rb spec/rails_adapter_schema_dump_spec.rb spec/rails_schema_statements_spec.rb
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Before a production release, run a real migration and smoke query through the
|
|
121
|
+
same server URL, TLS settings, pool size, and secret-management path used by
|
|
122
|
+
the deployment. Keep a verified backup before migrations and rehearse restore
|
|
123
|
+
and rollback on a representative copy.
|
|
124
|
+
|
|
125
|
+
## Support and release policy
|
|
126
|
+
|
|
127
|
+
The adapter version is coupled to the RubyDB 0.1.x release line. Pin both gems
|
|
128
|
+
in the application lockfile, upgrade them together, and run the adapter suite
|
|
129
|
+
before upgrading Rails. Report reproducible adapter or engine issues at the
|
|
130
|
+
project's [issue tracker](https://github.com/aldanedev-create/rubydb/issues)
|
|
131
|
+
with Ruby/Rails/RubyDB versions, the SQL or migration involved, and a redacted
|
|
132
|
+
configuration summary. Never include passwords, connection URLs, database
|
|
133
|
+
files, or private keys in reports.
|
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "active_record/connection_adapters/abstract_adapter"
|
|
5
|
+
require "active_record/connection_adapters/abstract/schema_definitions"
|
|
6
|
+
require "active_record/connection_adapters/abstract/schema_statements"
|
|
7
|
+
|
|
8
|
+
require "rubydb"
|
|
9
|
+
require "rubydb/rails/adapter"
|
|
10
|
+
require "rubydb/rails/connection"
|
|
11
|
+
require "rubydb/rails/database_statements"
|
|
12
|
+
require "rubydb/rails/schema_statements"
|
|
13
|
+
require "rubydb/rails/quoting"
|
|
14
|
+
require "rubydb/rails/type"
|
|
15
|
+
require "rubydb/rails/result"
|
|
16
|
+
|
|
17
|
+
module ActiveRecord
|
|
18
|
+
module ConnectionAdapters
|
|
19
|
+
# RubyDB adapter for ActiveRecord
|
|
20
|
+
class RubyDBAdapter < AbstractAdapter
|
|
21
|
+
include RubyDB::Rails::DatabaseStatements
|
|
22
|
+
include RubyDB::Rails::SchemaStatements
|
|
23
|
+
include RubyDB::Rails::Quoting
|
|
24
|
+
|
|
25
|
+
ADAPTER_NAME = "RubyDB"
|
|
26
|
+
|
|
27
|
+
# ActiveRecord 7.2 uses adapter-level methods while compiling hash-form
|
|
28
|
+
# order clauses (for example, `order(created_at: :desc)`). Keep these
|
|
29
|
+
# independent of a live connection so relation construction is safe
|
|
30
|
+
# during schema-cache and query setup as well.
|
|
31
|
+
def self.quote_table_name(name)
|
|
32
|
+
quote_column_name(name)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.quote_column_name(name)
|
|
36
|
+
"\"#{name.to_s.gsub('"', '""')}\""
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
NATIVE_DATABASE_TYPES = {
|
|
40
|
+
primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT",
|
|
41
|
+
string: { name: "VARCHAR", limit: 255 },
|
|
42
|
+
text: { name: "TEXT" },
|
|
43
|
+
integer: { name: "INTEGER" },
|
|
44
|
+
bigint: { name: "BIGINT" },
|
|
45
|
+
smallint: { name: "SMALLINT" },
|
|
46
|
+
float: { name: "FLOAT" },
|
|
47
|
+
decimal: { name: "DECIMAL", precision: 10, scale: 2 },
|
|
48
|
+
datetime: { name: "TIMESTAMP" },
|
|
49
|
+
timestamp: { name: "TIMESTAMP" },
|
|
50
|
+
time: { name: "TIME" },
|
|
51
|
+
date: { name: "DATE" },
|
|
52
|
+
binary: { name: "BLOB" },
|
|
53
|
+
boolean: { name: "BOOLEAN" },
|
|
54
|
+
json: { name: "JSON" },
|
|
55
|
+
uuid: { name: "UUID" }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# ActiveRecord 7.2 constructs adapters with a single configuration hash.
|
|
59
|
+
# Accept trailing deprecated arguments so applications upgrading from older
|
|
60
|
+
# ActiveRecord versions do not fail during connection establishment.
|
|
61
|
+
def initialize(config, *)
|
|
62
|
+
super(config)
|
|
63
|
+
|
|
64
|
+
@connection = RubyDB::Rails::Connection.new(config)
|
|
65
|
+
@connection.connect
|
|
66
|
+
|
|
67
|
+
@prepared_statements = {}
|
|
68
|
+
@transaction_depth = 0
|
|
69
|
+
@query_cache_enabled = false
|
|
70
|
+
@query_cache = {}
|
|
71
|
+
@statements = {}
|
|
72
|
+
@statement_counter = 0
|
|
73
|
+
# AbstractAdapter uses this monitor while creating transactions. It
|
|
74
|
+
# must be re-entrant because ActiveRecord acquires it recursively.
|
|
75
|
+
@lock = Monitor.new
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def adapter_name
|
|
79
|
+
ADAPTER_NAME
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def supports_migrations?
|
|
83
|
+
true
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def supports_primary_key?
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def supports_index_sort_order?
|
|
91
|
+
true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def supports_transactions?
|
|
95
|
+
true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def supports_savepoints?
|
|
99
|
+
true
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def supports_foreign_keys?
|
|
103
|
+
true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def supports_views?
|
|
107
|
+
true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def supports_json?
|
|
111
|
+
true
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def supports_uuid?
|
|
115
|
+
true
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def supports_bulk_alter?
|
|
119
|
+
false
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def native_database_types
|
|
123
|
+
NATIVE_DATABASE_TYPES
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# ==================== SCHEMA METHODS ====================
|
|
127
|
+
|
|
128
|
+
def primary_key(table_name)
|
|
129
|
+
return @connection.engine.table_columns(table_name).find(&:primary_key?)&.name&.to_s || "id" if embedded?
|
|
130
|
+
|
|
131
|
+
result = execute("PRAGMA table_info(#{quote_table_name(table_name)})")
|
|
132
|
+
row = result.find { |r| r["pk"] == 1 }
|
|
133
|
+
row ? row["name"] : "id"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def tables
|
|
137
|
+
return @connection.engine.list_tables.map(&:to_s) if embedded?
|
|
138
|
+
|
|
139
|
+
result = execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
|
140
|
+
result.map { |row| row["name"] }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def table_exists?(table_name)
|
|
144
|
+
tables.include?(table_name.to_s)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# ActiveRecord's schema cache asks for these before a model is first
|
|
148
|
+
# instantiated. The embedded engine has catalog metadata already, so
|
|
149
|
+
# avoid generating unsupported SQLite catalog queries.
|
|
150
|
+
def data_sources
|
|
151
|
+
return tables if embedded?
|
|
152
|
+
|
|
153
|
+
super
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def data_source_exists?(name)
|
|
157
|
+
return table_exists?(name) if embedded?
|
|
158
|
+
|
|
159
|
+
super
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def views
|
|
163
|
+
return [] if embedded?
|
|
164
|
+
|
|
165
|
+
super
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def indexes(table_name)
|
|
169
|
+
if embedded?
|
|
170
|
+
return @connection.engine.index_manager.get_indexes_for_table(table_name.to_s).map do |index|
|
|
171
|
+
ActiveRecord::ConnectionAdapters::IndexDefinition.new(
|
|
172
|
+
table_name.to_s,
|
|
173
|
+
index.name.to_s,
|
|
174
|
+
index.unique,
|
|
175
|
+
index.columns.map(&:to_s)
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
result = execute("SELECT name, sql FROM sqlite_master WHERE type='index' AND tbl_name=?", [table_name])
|
|
181
|
+
result.map do |row|
|
|
182
|
+
{
|
|
183
|
+
name: row["name"],
|
|
184
|
+
columns: parse_index_columns(row["sql"]),
|
|
185
|
+
unique: row["sql"].include?("UNIQUE")
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def columns(table_name)
|
|
191
|
+
return embedded_columns(table_name) if embedded?
|
|
192
|
+
|
|
193
|
+
result = execute("PRAGMA table_info(#{quote_table_name(table_name)})")
|
|
194
|
+
result.map do |row|
|
|
195
|
+
ActiveRecord::ConnectionAdapters::Column.new(
|
|
196
|
+
row["name"],
|
|
197
|
+
row["default"],
|
|
198
|
+
RubyDB::Rails::Type.to_rails(row["type"]),
|
|
199
|
+
{
|
|
200
|
+
null: row["notnull"] == 0,
|
|
201
|
+
primary_key: row["pk"] == 1,
|
|
202
|
+
limit: extract_limit(row["type"])
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def column_exists?(table_name, column_name)
|
|
209
|
+
columns(table_name).any? { |c| c.name == column_name }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# ==================== QUERY METHODS ====================
|
|
213
|
+
|
|
214
|
+
def execute(sql, name = nil)
|
|
215
|
+
sql = sql_for_execution(sql)
|
|
216
|
+
log(sql, name) do
|
|
217
|
+
@connection.execute(sql)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def exec_query(sql, name = nil, binds = [])
|
|
222
|
+
sql = sql_for_execution(sql)
|
|
223
|
+
log(sql, name) do
|
|
224
|
+
params = bind_values(binds)
|
|
225
|
+
active_record_result(@connection.execute(sql, params))
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def exec_delete(sql, name = nil, binds = [])
|
|
230
|
+
sql = sql_for_execution(sql)
|
|
231
|
+
log(sql, name) do
|
|
232
|
+
params = bind_values(binds)
|
|
233
|
+
result = @connection.execute(sql, params)
|
|
234
|
+
result.affected_rows
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def exec_update(sql, name = nil, binds = [])
|
|
239
|
+
sql = sql_for_execution(sql)
|
|
240
|
+
log(sql, name) do
|
|
241
|
+
params = bind_values(binds)
|
|
242
|
+
result = @connection.execute(sql, params)
|
|
243
|
+
result.affected_rows
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil)
|
|
248
|
+
sql = sql_for_execution(sql)
|
|
249
|
+
log(sql, name) do
|
|
250
|
+
params = bind_values(binds)
|
|
251
|
+
result = @connection.execute(sql, params)
|
|
252
|
+
id = result.row_id
|
|
253
|
+
ActiveRecord::Result.new([pk || "id"], id.nil? ? [] : [[id]])
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [], returning: nil)
|
|
258
|
+
sql, binds = to_sql_and_binds(arel, binds)
|
|
259
|
+
result = exec_insert(sql, name, binds, pk, sequence_name, returning: returning)
|
|
260
|
+
return returning_column_values(result) unless returning.nil?
|
|
261
|
+
|
|
262
|
+
id_value || last_inserted_id(result)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def update(arel, name = nil, binds = [])
|
|
266
|
+
sql, binds = to_sql_and_binds(arel, binds)
|
|
267
|
+
exec_update(sql, name, binds)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def delete(arel, name = nil, binds = [])
|
|
271
|
+
sql, binds = to_sql_and_binds(arel, binds)
|
|
272
|
+
exec_delete(sql, name, binds)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def select_all(sql, name = nil, binds = [], preparable: nil, async: false, allow_retry: false)
|
|
276
|
+
sql, binds = to_sql_and_binds(sql, binds)
|
|
277
|
+
exec_query(sql, name, binds)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def select_one(sql, name = nil, binds = [])
|
|
281
|
+
sql, binds = to_sql_and_binds(sql, binds)
|
|
282
|
+
result = exec_query(sql, name, binds)
|
|
283
|
+
result.first
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def select_value(sql, name = nil, binds = [])
|
|
287
|
+
sql, binds = to_sql_and_binds(sql, binds)
|
|
288
|
+
result = exec_query(sql, name, binds)
|
|
289
|
+
result.first&.values&.first
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def select_values(sql, name = nil, binds = [])
|
|
293
|
+
sql, binds = to_sql_and_binds(sql, binds)
|
|
294
|
+
result = exec_query(sql, name, binds)
|
|
295
|
+
result.map { |row| row.values.first }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def select_rows(sql, name = nil, binds = [])
|
|
299
|
+
sql, binds = to_sql_and_binds(sql, binds)
|
|
300
|
+
result = exec_query(sql, name, binds)
|
|
301
|
+
result.map { |row| row.values }
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# ==================== TRANSACTION METHODS ====================
|
|
305
|
+
|
|
306
|
+
def begin_db_transaction
|
|
307
|
+
@transaction_depth += 1
|
|
308
|
+
@connection.begin_db_transaction if @transaction_depth == 1
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def commit_db_transaction
|
|
312
|
+
return if @transaction_depth <= 0
|
|
313
|
+
|
|
314
|
+
@transaction_depth -= 1
|
|
315
|
+
@connection.commit_db_transaction if @transaction_depth == 0
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def rollback_db_transaction
|
|
319
|
+
return if @transaction_depth <= 0
|
|
320
|
+
|
|
321
|
+
@transaction_depth -= 1
|
|
322
|
+
@connection.rollback_db_transaction if @transaction_depth == 0
|
|
323
|
+
@transaction_depth = 0 if @transaction_depth < 0
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def create_savepoint(name)
|
|
327
|
+
execute("SAVEPOINT #{name}")
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def rollback_to_savepoint(name)
|
|
331
|
+
execute("ROLLBACK TO SAVEPOINT #{name}")
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def release_savepoint(name)
|
|
335
|
+
execute("RELEASE SAVEPOINT #{name}")
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def in_transaction?
|
|
339
|
+
@transaction_depth > 0
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def transaction_joinable?
|
|
343
|
+
true
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def transactional?
|
|
347
|
+
true
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# ==================== SCHEMA STATEMENT METHODS ====================
|
|
351
|
+
|
|
352
|
+
def create_table(table_name, **options, &block)
|
|
353
|
+
RubyDB::Rails::SchemaStatements.instance_method(:create_table).bind_call(self, table_name, options, &block)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def drop_table(table_name, **options)
|
|
357
|
+
sql = +"DROP TABLE"
|
|
358
|
+
sql << " IF EXISTS" if options[:if_exists]
|
|
359
|
+
sql << " #{quote_table_name(table_name)}"
|
|
360
|
+
sql << " CASCADE" if options[:cascade]
|
|
361
|
+
execute(sql)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def add_column(table_name, column_name, type, **options)
|
|
365
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)}"
|
|
366
|
+
sql << " ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options)}"
|
|
367
|
+
|
|
368
|
+
if options[:null] == false
|
|
369
|
+
sql << " NOT NULL"
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
if options[:default]
|
|
373
|
+
sql << " DEFAULT #{quote_default(options[:default])}"
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
if options[:primary_key]
|
|
377
|
+
sql << " PRIMARY KEY"
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
execute(sql)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def remove_column(table_name, column_name, type = nil, **options)
|
|
384
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)}"
|
|
385
|
+
sql << " DROP COLUMN #{quote_column_name(column_name)}"
|
|
386
|
+
sql << " CASCADE" if options[:cascade]
|
|
387
|
+
execute(sql)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def change_column(table_name, column_name, type, **options)
|
|
391
|
+
# Change column type
|
|
392
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)}"
|
|
393
|
+
sql << " ALTER COLUMN #{quote_column_name(column_name)}"
|
|
394
|
+
sql << " TYPE #{type_to_sql(type, options)}"
|
|
395
|
+
execute(sql)
|
|
396
|
+
|
|
397
|
+
# Change nullability
|
|
398
|
+
if options.key?(:null)
|
|
399
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)}"
|
|
400
|
+
sql << " ALTER COLUMN #{quote_column_name(column_name)}"
|
|
401
|
+
sql << (options[:null] ? " DROP" : " SET") + " NOT NULL"
|
|
402
|
+
execute(sql)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Change default
|
|
406
|
+
if options.key?(:default)
|
|
407
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)}"
|
|
408
|
+
sql << " ALTER COLUMN #{quote_column_name(column_name)}"
|
|
409
|
+
if options[:default].nil?
|
|
410
|
+
sql << " DROP DEFAULT"
|
|
411
|
+
else
|
|
412
|
+
sql << " SET DEFAULT #{quote_default(options[:default])}"
|
|
413
|
+
end
|
|
414
|
+
execute(sql)
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def rename_column(table_name, column_name, new_column_name)
|
|
419
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)}"
|
|
420
|
+
sql << " RENAME COLUMN #{quote_column_name(column_name)}"
|
|
421
|
+
sql << " TO #{quote_column_name(new_column_name)}"
|
|
422
|
+
execute(sql)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def rename_table(old_name, new_name)
|
|
426
|
+
sql = "ALTER TABLE #{quote_table_name(old_name)}"
|
|
427
|
+
sql << " RENAME TO #{quote_table_name(new_name)}"
|
|
428
|
+
execute(sql)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def add_index(table_name, column_name, **options)
|
|
432
|
+
index_name = options[:name] || "idx_#{table_name}_#{Array(column_name).join('_')}"
|
|
433
|
+
sql = +"CREATE"
|
|
434
|
+
sql << " UNIQUE" if options[:unique]
|
|
435
|
+
sql << " INDEX #{quote_column_name(index_name)}"
|
|
436
|
+
sql << " ON #{quote_table_name(table_name)}"
|
|
437
|
+
sql << " (#{Array(column_name).map { |c| quote_column_name(c) }.join(', ')})"
|
|
438
|
+
sql << " WHERE #{options[:where]}" if options[:where]
|
|
439
|
+
execute(sql)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def remove_index(table_name, column_name = nil, **options)
|
|
443
|
+
index_name = options[:name]
|
|
444
|
+
if index_name.nil?
|
|
445
|
+
column_name ||= options[:column] || options[:columns]
|
|
446
|
+
index_name = "idx_#{table_name}_#{Array(column_name).join('_')}"
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
sql = "DROP INDEX #{quote_column_name(index_name)}"
|
|
450
|
+
execute(sql)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def add_foreign_key(from_table, to_table, **options)
|
|
454
|
+
fk_name = options[:name] || "fk_#{from_table}_to_#{to_table}"
|
|
455
|
+
sql = "ALTER TABLE #{quote_table_name(from_table)}"
|
|
456
|
+
sql << " ADD CONSTRAINT #{quote_column_name(fk_name)}"
|
|
457
|
+
sql << " FOREIGN KEY (#{quote_column_name(options[:column] || :id)})"
|
|
458
|
+
sql << " REFERENCES #{quote_table_name(to_table)}"
|
|
459
|
+
sql << " (#{quote_column_name(options[:primary_key] || :id)})"
|
|
460
|
+
sql << " ON DELETE #{options[:on_delete]}" if options[:on_delete]
|
|
461
|
+
sql << " ON UPDATE #{options[:on_update]}" if options[:on_update]
|
|
462
|
+
execute(sql)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def foreign_keys(table_name)
|
|
466
|
+
return super unless embedded?
|
|
467
|
+
|
|
468
|
+
constraints = @connection.engine.table_metadata[table_name.to_s]&.fetch(:constraints, []) || []
|
|
469
|
+
constraints.filter_map do |constraint|
|
|
470
|
+
type = constraint[:type] || constraint["type"]
|
|
471
|
+
next unless type.to_s.upcase == "FOREIGN_KEY"
|
|
472
|
+
|
|
473
|
+
columns = constraint[:columns] || constraint["columns"] || []
|
|
474
|
+
reference_table = constraint[:reference_table] || constraint["reference_table"]
|
|
475
|
+
reference_columns = constraint[:reference_columns] || constraint["reference_columns"] || ["id"]
|
|
476
|
+
options = {
|
|
477
|
+
column: Array(columns).first.to_s,
|
|
478
|
+
primary_key: Array(reference_columns).first.to_s,
|
|
479
|
+
name: constraint[:name] || constraint["name"]
|
|
480
|
+
}
|
|
481
|
+
options[:on_delete] = (constraint[:on_delete] || constraint["on_delete"]).to_s if constraint[:on_delete] || constraint["on_delete"]
|
|
482
|
+
options[:on_update] = (constraint[:on_update] || constraint["on_update"]).to_s if constraint[:on_update] || constraint["on_update"]
|
|
483
|
+
ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name.to_s, reference_table.to_s, options)
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def remove_foreign_key(from_table, **options)
|
|
488
|
+
fk_name = options[:name] || "fk_#{from_table}_to_#{options[:to_table]}"
|
|
489
|
+
sql = "ALTER TABLE #{quote_table_name(from_table)}"
|
|
490
|
+
sql << " DROP CONSTRAINT #{quote_column_name(fk_name)}"
|
|
491
|
+
execute(sql)
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def add_timestamps(table_name, **options)
|
|
495
|
+
add_column(table_name, :created_at, :datetime, options)
|
|
496
|
+
add_column(table_name, :updated_at, :datetime, options)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def remove_timestamps(table_name, **options)
|
|
500
|
+
remove_column(table_name, :updated_at, options)
|
|
501
|
+
remove_column(table_name, :created_at, options)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
|
505
|
+
if default
|
|
506
|
+
sql = "UPDATE #{quote_table_name(table_name)}"
|
|
507
|
+
sql << " SET #{quote_column_name(column_name)} = #{quote(default)}"
|
|
508
|
+
sql << " WHERE #{quote_column_name(column_name)} IS NULL"
|
|
509
|
+
execute(sql)
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)}"
|
|
513
|
+
sql << " ALTER COLUMN #{quote_column_name(column_name)}"
|
|
514
|
+
sql << (null ? " DROP" : " SET") + " NOT NULL"
|
|
515
|
+
execute(sql)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def change_column_default(table_name, column_name, default)
|
|
519
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)}"
|
|
520
|
+
sql << " ALTER COLUMN #{quote_column_name(column_name)}"
|
|
521
|
+
if default.nil?
|
|
522
|
+
sql << " DROP DEFAULT"
|
|
523
|
+
else
|
|
524
|
+
sql << " SET DEFAULT #{quote_default(default)}"
|
|
525
|
+
end
|
|
526
|
+
execute(sql)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# ==================== QUOTING METHODS ====================
|
|
530
|
+
|
|
531
|
+
def quote(value, column = nil)
|
|
532
|
+
@connection.quote(value, column)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def quote_table_name(name)
|
|
536
|
+
@connection.quote_table_name(name)
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def quote_column_name(name)
|
|
540
|
+
@connection.quote_column_name(name)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def quote_default(value)
|
|
544
|
+
quote(value)
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# ==================== TYPE CASTING ====================
|
|
548
|
+
|
|
549
|
+
def type_cast(value, type)
|
|
550
|
+
RubyDB::Rails::Type.serialize(value, type)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def type_cast_from_database(value, type)
|
|
554
|
+
RubyDB::Rails::Type.deserialize(value, type)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# ==================== SCHEMA VERSION ====================
|
|
558
|
+
|
|
559
|
+
def schema_version
|
|
560
|
+
result = execute("SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1")
|
|
561
|
+
result.first ? result.first["version"] : nil
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def schema_migrations
|
|
565
|
+
result = execute("SELECT version FROM schema_migrations ORDER BY version")
|
|
566
|
+
result.map { |row| row["version"] }
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def dump_schema
|
|
570
|
+
schema = +""
|
|
571
|
+
tables.each do |table|
|
|
572
|
+
table_columns = columns(table)
|
|
573
|
+
primary_key_name = if embedded?
|
|
574
|
+
@connection.engine.table_columns(table).find(&:primary_key?)&.name
|
|
575
|
+
else
|
|
576
|
+
primary_key(table)
|
|
577
|
+
end
|
|
578
|
+
primary_key = table_columns.find { |column| column.name.to_s == primary_key_name.to_s } if primary_key_name
|
|
579
|
+
automatic_id = primary_key && primary_key_name.to_s == "id" && primary_key.type.to_sym == :integer
|
|
580
|
+
table_options = automatic_id ? "" : ", id: false"
|
|
581
|
+
schema << "create_table \"#{table}\"#{table_options} do |t|\n"
|
|
582
|
+
table_columns.each do |col|
|
|
583
|
+
next if automatic_id && primary_key && col.name.to_s == primary_key.name.to_s
|
|
584
|
+
|
|
585
|
+
type = RubyDB::Rails::Type.to_rails(col.type)
|
|
586
|
+
schema << " t.#{type} \"#{col.name}\""
|
|
587
|
+
schema << ", primary_key: true" if primary_key && col.name.to_s == primary_key.name.to_s
|
|
588
|
+
schema << ", default: #{schema_literal(col.default, col.type)}" unless col.default.nil?
|
|
589
|
+
schema << ", null: false" unless col.null
|
|
590
|
+
schema << "\n"
|
|
591
|
+
end
|
|
592
|
+
schema << "end\n\n"
|
|
593
|
+
|
|
594
|
+
indexes(table).each do |index|
|
|
595
|
+
schema << "add_index \"#{table}\", #{index.columns.map(&:to_s).inspect}"
|
|
596
|
+
schema << ", unique: true" if index.unique
|
|
597
|
+
schema << ", name: #{index.name.to_s.inspect}\n"
|
|
598
|
+
end
|
|
599
|
+
schema << "\n" if indexes(table).any?
|
|
600
|
+
end
|
|
601
|
+
schema
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def schema_literal(value, type = nil)
|
|
605
|
+
if type.to_sym == :boolean && value.is_a?(String) && %w[true false].include?(value.downcase)
|
|
606
|
+
return value.downcase
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
case value
|
|
610
|
+
when true then "true"
|
|
611
|
+
when false then "false"
|
|
612
|
+
when Numeric then value.to_s
|
|
613
|
+
else value.to_s.inspect
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
# ==================== CONNECTION MANAGEMENT ====================
|
|
618
|
+
|
|
619
|
+
def reset!
|
|
620
|
+
@connection.disconnect
|
|
621
|
+
@connection.connect
|
|
622
|
+
@prepared_statements.clear
|
|
623
|
+
@query_cache.clear
|
|
624
|
+
@statements.clear
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def disconnect!
|
|
628
|
+
@connection.disconnect
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def reconnect!
|
|
632
|
+
reset!
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def active?
|
|
636
|
+
@connection.connected?
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def close
|
|
640
|
+
@connection.disconnect
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# ==================== QUERY CACHE ====================
|
|
644
|
+
|
|
645
|
+
def clear_cache!
|
|
646
|
+
@query_cache.clear
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def enable_query_cache!
|
|
650
|
+
@query_cache_enabled = true
|
|
651
|
+
@query_cache.clear
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def disable_query_cache!
|
|
655
|
+
@query_cache_enabled = false
|
|
656
|
+
@query_cache.clear
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def query_cache_enabled
|
|
660
|
+
@query_cache_enabled
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# ==================== PREPARED STATEMENTS ====================
|
|
664
|
+
|
|
665
|
+
def prepare_statement(sql)
|
|
666
|
+
@lock.synchronize do
|
|
667
|
+
stmt_id = "stmt_#{Time.now.to_i}_#{@statement_counter}"
|
|
668
|
+
@statement_counter += 1
|
|
669
|
+
|
|
670
|
+
result = @connection.prepare(sql)
|
|
671
|
+
@prepared_statements[stmt_id] = {
|
|
672
|
+
id: result.statement_id,
|
|
673
|
+
sql: sql,
|
|
674
|
+
created_at: Time.now
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
stmt_id
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
def execute_prepared_statement(stmt_id, params = [])
|
|
682
|
+
@lock.synchronize do
|
|
683
|
+
stmt = @prepared_statements[stmt_id]
|
|
684
|
+
return nil unless stmt
|
|
685
|
+
|
|
686
|
+
@connection.execute_prepared(stmt[:id], params)
|
|
687
|
+
end
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def close_statement(stmt_id)
|
|
691
|
+
@lock.synchronize do
|
|
692
|
+
stmt = @prepared_statements.delete(stmt_id)
|
|
693
|
+
if stmt
|
|
694
|
+
@connection.close_statement(stmt[:id])
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
# ==================== VERSION INFORMATION ====================
|
|
700
|
+
|
|
701
|
+
def dbms_version
|
|
702
|
+
RubyDB::VERSION
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
# ==================== FEATURE SUPPORT ====================
|
|
706
|
+
|
|
707
|
+
def supports_datetime_with_precision?
|
|
708
|
+
true
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
def supports_materialized_views?
|
|
712
|
+
false
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
def supports_common_table_expressions?
|
|
716
|
+
false
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
# ==================== PRIVATE METHODS ====================
|
|
720
|
+
|
|
721
|
+
private
|
|
722
|
+
|
|
723
|
+
def embedded?
|
|
724
|
+
!@connection.engine.nil?
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
def embedded_columns(table_name)
|
|
728
|
+
@connection.engine.table_columns(table_name).map do |column|
|
|
729
|
+
ActiveRecord::ConnectionAdapters::Column.new(
|
|
730
|
+
column.name.to_s,
|
|
731
|
+
column.has_default? ? rails_default_value(column.default) : nil,
|
|
732
|
+
ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(
|
|
733
|
+
sql_type: column.type.to_s.upcase,
|
|
734
|
+
type: rails_type_for(column.type),
|
|
735
|
+
limit: column.options[:limit]
|
|
736
|
+
),
|
|
737
|
+
column.nullable?
|
|
738
|
+
)
|
|
739
|
+
end
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def rails_type_for(type)
|
|
743
|
+
case type.to_sym
|
|
744
|
+
when :integer, :bigint, :smallint then :integer
|
|
745
|
+
when :float then :float
|
|
746
|
+
when :decimal then :decimal
|
|
747
|
+
when :boolean then :boolean
|
|
748
|
+
when :date then :date
|
|
749
|
+
when :time then :time
|
|
750
|
+
when :datetime, :timestamp then :datetime
|
|
751
|
+
when :binary, :blob then :binary
|
|
752
|
+
when :json then :json
|
|
753
|
+
else :string
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
# ActiveRecord's generic Column deduplication is string-oriented. RubyDB
|
|
758
|
+
# persists typed defaults, so serialize scalar defaults at this boundary
|
|
759
|
+
# and let ActiveRecord cast them through the column type map.
|
|
760
|
+
def rails_default_value(value)
|
|
761
|
+
value.is_a?(String) ? value : value.to_s
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
def sql_for_execution(sql)
|
|
765
|
+
sql = sql.to_sql if sql.respond_to?(:to_sql)
|
|
766
|
+
sql
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
def active_record_result(result)
|
|
770
|
+
rows = result.to_a
|
|
771
|
+
columns = if rows.first.respond_to?(:keys)
|
|
772
|
+
rows.first.keys.map(&:to_s)
|
|
773
|
+
else
|
|
774
|
+
result.columns.map do |column|
|
|
775
|
+
column.is_a?(Hash) ? (column[:name] || column["name"] || column) : column
|
|
776
|
+
end.map(&:to_s)
|
|
777
|
+
end
|
|
778
|
+
values = rows.map do |row|
|
|
779
|
+
columns.map do |column|
|
|
780
|
+
if row.respond_to?(:key?) && row.key?(column)
|
|
781
|
+
row[column]
|
|
782
|
+
elsif row.respond_to?(:key?) && row.key?(column.to_sym)
|
|
783
|
+
row[column.to_sym]
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
end
|
|
787
|
+
ActiveRecord::Result.new(columns, values)
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
# ActiveRecord normally supplies QueryAttribute objects, but migration
|
|
791
|
+
# and schema code can also pass raw values or two-element bind pairs.
|
|
792
|
+
# Normalize all supported forms at the adapter boundary.
|
|
793
|
+
def bind_values(binds)
|
|
794
|
+
binds.map do |bind|
|
|
795
|
+
value = if bind.respond_to?(:value_for_database)
|
|
796
|
+
bind.value_for_database
|
|
797
|
+
elsif bind.respond_to?(:value)
|
|
798
|
+
bind.value
|
|
799
|
+
elsif bind.is_a?(Array) && bind.length == 2
|
|
800
|
+
bind.last
|
|
801
|
+
else
|
|
802
|
+
bind
|
|
803
|
+
end
|
|
804
|
+
value.respond_to?(:value_for_database) ? value.value_for_database : value
|
|
805
|
+
end
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
def parse_index_columns(sql)
|
|
809
|
+
if sql =~ /\(([^)]+)\)/
|
|
810
|
+
$1.split(",").map(&:strip)
|
|
811
|
+
else
|
|
812
|
+
[]
|
|
813
|
+
end
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
def extract_limit(type)
|
|
817
|
+
if type =~ /VARCHAR\((\d+)\)/
|
|
818
|
+
$1.to_i
|
|
819
|
+
else
|
|
820
|
+
nil
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
def log(sql, name = nil, &block)
|
|
825
|
+
start_time = Time.now
|
|
826
|
+
result = block.call
|
|
827
|
+
elapsed_ms = (Time.now - start_time) * 1000
|
|
828
|
+
|
|
829
|
+
if @logger
|
|
830
|
+
@logger.debug " #{name || 'SQL'} (#{elapsed_ms.round(2)}ms) #{sql}"
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
result
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
def type_to_sql(type, options = {})
|
|
837
|
+
case type.to_sym
|
|
838
|
+
when :integer
|
|
839
|
+
"INTEGER"
|
|
840
|
+
when :bigint
|
|
841
|
+
"BIGINT"
|
|
842
|
+
when :smallint
|
|
843
|
+
"SMALLINT"
|
|
844
|
+
when :float
|
|
845
|
+
"FLOAT"
|
|
846
|
+
when :decimal
|
|
847
|
+
precision = options[:precision] || 10
|
|
848
|
+
scale = options[:scale] || 2
|
|
849
|
+
"DECIMAL(#{precision}, #{scale})"
|
|
850
|
+
when :boolean
|
|
851
|
+
"BOOLEAN"
|
|
852
|
+
when :text
|
|
853
|
+
"TEXT"
|
|
854
|
+
when :string
|
|
855
|
+
limit = options[:limit] || 255
|
|
856
|
+
"VARCHAR(#{limit})"
|
|
857
|
+
when :binary
|
|
858
|
+
"BLOB"
|
|
859
|
+
when :date
|
|
860
|
+
"DATE"
|
|
861
|
+
when :time
|
|
862
|
+
"TIME"
|
|
863
|
+
when :datetime, :timestamp
|
|
864
|
+
"TIMESTAMP"
|
|
865
|
+
when :json
|
|
866
|
+
"JSON"
|
|
867
|
+
when :uuid
|
|
868
|
+
"UUID"
|
|
869
|
+
else
|
|
870
|
+
"TEXT"
|
|
871
|
+
end
|
|
872
|
+
end
|
|
873
|
+
end
|
|
874
|
+
end
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
# Rails 7.2 introduced explicit adapter registration. Rails 7.1 loads custom
|
|
878
|
+
# adapters through the conventional `rubydb_connection` hook instead.
|
|
879
|
+
if ActiveRecord::ConnectionAdapters.respond_to?(:register)
|
|
880
|
+
ActiveRecord::ConnectionAdapters.register("rubydb", "ActiveRecord::ConnectionAdapters::RubyDBAdapter")
|
|
881
|
+
else
|
|
882
|
+
module ActiveRecord
|
|
883
|
+
module ConnectionHandling
|
|
884
|
+
def rubydb_adapter_class
|
|
885
|
+
ConnectionAdapters::RubyDBAdapter
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
def rubydb_connection(config)
|
|
889
|
+
rubydb_adapter_class.new(config)
|
|
890
|
+
end
|
|
891
|
+
end
|
|
892
|
+
end
|
|
893
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "rubydb-activerecord"
|
|
5
|
+
spec.version = "0.1.0"
|
|
6
|
+
spec.authors = ["Aldane Hutchinson"]
|
|
7
|
+
spec.email = ["aldanehutchinson5@gmail.com"]
|
|
8
|
+
|
|
9
|
+
spec.summary = "ActiveRecord adapter for RubyDB"
|
|
10
|
+
spec.description = "ActiveRecord adapter for the RubyDB database"
|
|
11
|
+
spec.homepage = "https://github.com/aldanedev-create/rubydb"
|
|
12
|
+
spec.license = "MIT"
|
|
13
|
+
# rubydb itself requires Ruby 3.3 or newer. Keep the adapter's runtime
|
|
14
|
+
# contract aligned so Bundler cannot select an unsupported combination.
|
|
15
|
+
spec.required_ruby_version = ">= 3.3.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = "#{spec.homepage}/tree/main/adapters/activerecord"
|
|
19
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/aldanedev-create/rubydb/issues"
|
|
20
|
+
spec.metadata["changelog_uri"] = "https://github.com/aldanedev-create/rubydb/blob/main/CHANGELOG.md"
|
|
21
|
+
spec.metadata["documentation_uri"] = "https://github.com/aldanedev-create/rubydb/tree/main/docs"
|
|
22
|
+
|
|
23
|
+
spec.files = Dir.glob("lib/**/*.rb") + %w[README.md rubydb-activerecord.gemspec]
|
|
24
|
+
spec.require_paths = ["lib"]
|
|
25
|
+
|
|
26
|
+
spec.add_dependency "activerecord", ">= 7.1", "< 8.1"
|
|
27
|
+
spec.add_dependency "rubydb", "~> 0.1.0"
|
|
28
|
+
|
|
29
|
+
spec.add_development_dependency "rake", "~> 13.2"
|
|
30
|
+
spec.add_development_dependency "rspec", "~> 3.13"
|
|
31
|
+
spec.add_development_dependency "rubocop", "~> 1.60"
|
|
32
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rubydb-activerecord
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Aldane Hutchinson
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activerecord
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '8.1'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '7.1'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '8.1'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: rubydb
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: 0.1.0
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: 0.1.0
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: rake
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - "~>"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '13.2'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '13.2'
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: rspec
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '3.13'
|
|
67
|
+
type: :development
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '3.13'
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: rubocop
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '1.60'
|
|
81
|
+
type: :development
|
|
82
|
+
prerelease: false
|
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - "~>"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '1.60'
|
|
88
|
+
description: ActiveRecord adapter for the RubyDB database
|
|
89
|
+
email:
|
|
90
|
+
- aldanehutchinson5@gmail.com
|
|
91
|
+
executables: []
|
|
92
|
+
extensions: []
|
|
93
|
+
extra_rdoc_files: []
|
|
94
|
+
files:
|
|
95
|
+
- README.md
|
|
96
|
+
- lib/active_record/connection_adapters/rubydb_adapter.rb
|
|
97
|
+
- rubydb-activerecord.gemspec
|
|
98
|
+
homepage: https://github.com/aldanedev-create/rubydb
|
|
99
|
+
licenses:
|
|
100
|
+
- MIT
|
|
101
|
+
metadata:
|
|
102
|
+
homepage_uri: https://github.com/aldanedev-create/rubydb
|
|
103
|
+
source_code_uri: https://github.com/aldanedev-create/rubydb/tree/main/adapters/activerecord
|
|
104
|
+
bug_tracker_uri: https://github.com/aldanedev-create/rubydb/issues
|
|
105
|
+
changelog_uri: https://github.com/aldanedev-create/rubydb/blob/main/CHANGELOG.md
|
|
106
|
+
documentation_uri: https://github.com/aldanedev-create/rubydb/tree/main/docs
|
|
107
|
+
rdoc_options: []
|
|
108
|
+
require_paths:
|
|
109
|
+
- lib
|
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - ">="
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: 3.3.0
|
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
116
|
+
requirements:
|
|
117
|
+
- - ">="
|
|
118
|
+
- !ruby/object:Gem::Version
|
|
119
|
+
version: '0'
|
|
120
|
+
requirements: []
|
|
121
|
+
rubygems_version: 4.0.16
|
|
122
|
+
specification_version: 4
|
|
123
|
+
summary: ActiveRecord adapter for RubyDB
|
|
124
|
+
test_files: []
|