activerecord-virgodb-adapter 0.1.0.dev.9f07a74
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/LICENSE +5 -0
- data/lib/active_record/connection_adapters/virgodb_adapter.rb +383 -0
- metadata +72 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bfdc3dd91096a4323088bf5ee3891543c706a2b6e34c85456cd07eb5a98391cf
|
|
4
|
+
data.tar.gz: d046ad46492f2de29816497f566095f77d48defe0343f1bf6c44c9a455edc0bf
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1576c825c6f2653d7d0cf641676fe71c179d1f53a90389ec91592892eec8eac5bf7560358e99768aa6cbdb24b4f0413888a784576d8b114246e904af0cedfbf8
|
|
7
|
+
data.tar.gz: fd8ff15112deb4bae56553011e7f2ac6e5d73a3d8847b276fdda719e4480a1f5a8d4b691c0316ee1056aa6d292ca3f9fa316f12fb046ec2423ae7927a8946d7a
|
data/LICENSE
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Copyright (c) 2026 Sumit Kumar. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software and associated documentation files (the "Software") are proprietary. You may install and use the compiled binary for personal, non-commercial purposes only. Any commercial use, redistribution, modification, or reverse engineering of the Software is strictly prohibited without explicit written permission.
|
|
4
|
+
|
|
5
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record/connection_adapters/sqlite3_adapter"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module ActiveRecord
|
|
7
|
+
module ConnectionAdapters
|
|
8
|
+
# ActiveRecord adapter for virgodb migrations -- lets any Rails app
|
|
9
|
+
# define and evolve a virgodb table's schema the same way it already
|
|
10
|
+
# does for SQLite (bin/rails db:migrate) and ClickHouse (the
|
|
11
|
+
# clickhouse-activerecord gem): real ActiveRecord::Migration subclasses,
|
|
12
|
+
# raw type strings, picked up by `rails db:migrate` alongside every
|
|
13
|
+
# other database.
|
|
14
|
+
#
|
|
15
|
+
# virgodb's manifest is already a real SQLite file, so subclassing
|
|
16
|
+
# SQLite3Adapter (not building from AbstractAdapter) gets
|
|
17
|
+
# structure_dump/structure_load -- the :sql schema format -- for free,
|
|
18
|
+
# zero custom dumper code. Only create_table/add_column need
|
|
19
|
+
# overriding: call `super` for a real, if never-written-to, SQLite
|
|
20
|
+
# table (SQLite's type-name grammar accepts arbitrary bare-word type
|
|
21
|
+
# declarations -- verified against a real connection, see
|
|
22
|
+
# crates/compact/src/schema.rs's doc comment for the exact grammar and
|
|
23
|
+
# why it has to be `"AnyLast String updated_at"`, not
|
|
24
|
+
# `"AnyLast(String, updated_at)"`), and additionally record the column
|
|
25
|
+
# in a `schema_versions` table matching exactly what
|
|
26
|
+
# `compact::schema_from_columns` on the Rust side expects to parse.
|
|
27
|
+
#
|
|
28
|
+
# One manifest can back several virgodb tables sharing a single
|
|
29
|
+
# `database.yml` connection and migrations path -- the same way a host
|
|
30
|
+
# app's `clickhouse:` connection might already hold all of its ClickHouse
|
|
31
|
+
# tables under one connection. `schema_versions` (and the Rust-side
|
|
32
|
+
# manifest's `run_files`/`inline_batches`/`orphaned_files`/
|
|
33
|
+
# `maintenance_lease` catalog tables) are scoped by `table_name`
|
|
34
|
+
# specifically to make this safe: each logical table's schema history
|
|
35
|
+
# and physical run files stay independent even though they live in the
|
|
36
|
+
# same SQLite file.
|
|
37
|
+
#
|
|
38
|
+
# Additive-only, matching `Manifest::register_schema_version`'s own
|
|
39
|
+
# rule and virgodb's immutable-Parquet philosophy: `remove_column`,
|
|
40
|
+
# `rename_column`, and `change_column` are all disabled outright rather
|
|
41
|
+
# than silently letting the real SQLite table drift out of sync with
|
|
42
|
+
# what `schema_versions` describes.
|
|
43
|
+
class VirgodbAdapter < SQLite3Adapter
|
|
44
|
+
ADAPTER_NAME = "Virgodb"
|
|
45
|
+
|
|
46
|
+
# Real bug, found (not assumed) by testing: setting `auto_vacuum =
|
|
47
|
+
# INCREMENTAL` from `create_table` -- even from create_table's very
|
|
48
|
+
# FIRST call on a brand-new manifest, before any table exists -- was
|
|
49
|
+
# already too late. SQLite3Adapter's own `configure_connection` sets
|
|
50
|
+
# `journal_mode = wal` via DEFAULT_PRAGMAS (`super`, below) as part of
|
|
51
|
+
# establishing the connection itself, which happens before this
|
|
52
|
+
# adapter's `create_table` ever runs -- and switching journal_mode to
|
|
53
|
+
# WAL requires SQLite to actually write to the file (creating the -wal
|
|
54
|
+
# file, an implicit checkpoint), which "poisons" the database from
|
|
55
|
+
# auto_vacuum's perspective the same way any other content would.
|
|
56
|
+
# Confirmed directly: a manual `PRAGMA auto_vacuum = INCREMENTAL`
|
|
57
|
+
# issued immediately after `establish_connection`, before ANY table or
|
|
58
|
+
# migration ran, still silently failed. The pragma has to be set
|
|
59
|
+
# before `super` runs here, not merely before the first CREATE TABLE.
|
|
60
|
+
def configure_connection
|
|
61
|
+
@raw_connection.auto_vacuum = "incremental" if @raw_connection.respond_to?(:auto_vacuum=)
|
|
62
|
+
super
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def create_table(table_name, **options, &block)
|
|
66
|
+
result = super
|
|
67
|
+
record_schema_version!(table_name)
|
|
68
|
+
result
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def add_column(table_name, column_name, type, **options)
|
|
72
|
+
result = super
|
|
73
|
+
record_added_column!(table_name, column_name, type, options)
|
|
74
|
+
result
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def remove_column(table_name, column_name, type = nil, **options)
|
|
78
|
+
raise NotImplementedError, "virgodb schemas are additive-only -- columns can be added, never removed (see schema_versions)"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def rename_column(table_name, column_name, new_column_name)
|
|
82
|
+
raise NotImplementedError, "virgodb schemas are additive-only -- columns can't be renamed once created"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def change_column(table_name, column_name, type, **options)
|
|
86
|
+
raise NotImplementedError, "virgodb schemas are additive-only -- an existing column's type can't change"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# Reads the table back via ActiveRecord's own column introspection --
|
|
92
|
+
# `sql_type` round-trips the raw virgodb type string verbatim, proven
|
|
93
|
+
# against a real SQLite connection immediately after a fresh CREATE
|
|
94
|
+
# TABLE -- and writes a new `schema_versions` row: the full current
|
|
95
|
+
# column list, versioned one higher than whatever was there before FOR
|
|
96
|
+
# THIS table_name. Mirrors Manifest::register_schema_version's shape
|
|
97
|
+
# exactly (same table_name scoping, same column names, same JSON shape
|
|
98
|
+
# for `columns`) so the Rust side can read whatever Ruby wrote with
|
|
99
|
+
# zero translation. Scoping `MAX(version)` by `table_name` is what makes
|
|
100
|
+
# it safe for several logical tables to share one manifest/connection:
|
|
101
|
+
# without it, the second table's first migration would start at
|
|
102
|
+
# version 2 (or worse, silently share version numbers with an
|
|
103
|
+
# unrelated table). Rails' OWN bookkeeping tables (`schema_migrations`,
|
|
104
|
+
# `ar_internal_metadata`) go through this SAME overridden `create_table`
|
|
105
|
+
# the first time either is lazily created -- there's nothing
|
|
106
|
+
# virgodb-specific about them, and recording a schema_versions row for
|
|
107
|
+
# them is pure noise (found while dumping a real structure.sql and
|
|
108
|
+
# seeing them show up as rows next to the real logical tables). Guarded
|
|
109
|
+
# here, not by checking `table_name` against `TABLES` in some caller --
|
|
110
|
+
# this adapter has no such registry, `create_table`'s own argument is
|
|
111
|
+
# the one place that reliably sees every table, virgodb or not.
|
|
112
|
+
#
|
|
113
|
+
# ONLY safe for `create_table` (a brand-new table with no prior
|
|
114
|
+
# history). NOT reused by `add_column` -- see `record_added_column!`'s
|
|
115
|
+
# own doc comment for the real bug this avoids: `columns(table_name)`
|
|
116
|
+
# stops reliably returning the ORIGINAL declared type text for
|
|
117
|
+
# pre-existing columns once the table has been through certain ALTER
|
|
118
|
+
# operations.
|
|
119
|
+
RAILS_INTERNAL_TABLES = %w[schema_migrations ar_internal_metadata].freeze
|
|
120
|
+
|
|
121
|
+
def record_schema_version!(table_name)
|
|
122
|
+
return if RAILS_INTERNAL_TABLES.include?(table_name.to_s)
|
|
123
|
+
|
|
124
|
+
ensure_schema_versions_table!
|
|
125
|
+
|
|
126
|
+
cols = columns(table_name).map do |c|
|
|
127
|
+
{ "name" => c.name, "virgodb_type" => c.sql_type, "nullable" => c.null }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
insert_schema_version!(table_name, cols)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# `add_column`'s schema_versions update -- deliberately does NOT
|
|
134
|
+
# re-introspect the whole table via `columns(table_name)` the way
|
|
135
|
+
# `record_schema_version!` does. Real bug, found (not assumed) by
|
|
136
|
+
# testing: `super`'s real `ALTER TABLE ADD COLUMN` -- specifically for
|
|
137
|
+
# a `null: false` column, which SQLite's native ALTER can't add without
|
|
138
|
+
# a default -- makes Rails' SQLite3Adapter fall back to its "rebuild
|
|
139
|
+
# the table" strategy (copy to a temp table, drop, rename). Confirmed
|
|
140
|
+
# directly: introspecting the table immediately afterward, even via a
|
|
141
|
+
# completely fresh connection/process (so not a Rails-process-level
|
|
142
|
+
# schema cache artifact), returns Rails' own GENERIC type names for
|
|
143
|
+
# every PRE-EXISTING column ("INTEGER"/"datetime"/"" instead of
|
|
144
|
+
# "Int64"/"Timestamp"/"LowCardinality String") -- the rebuild
|
|
145
|
+
# reconstructs the physical table's DDL from Rails' normalized column
|
|
146
|
+
# metadata, silently discarding the original literal type text for
|
|
147
|
+
# every column it didn't just add. Re-deriving the full column list
|
|
148
|
+
# via introspection after ANY add_column would therefore corrupt every
|
|
149
|
+
# already-correct entry in `schema_versions`.
|
|
150
|
+
#
|
|
151
|
+
# The fix: never re-derive what's already known to be correct. Read
|
|
152
|
+
# the CURRENT (highest-version) column list straight out of
|
|
153
|
+
# `schema_versions` itself (the one thing guaranteed not to have been
|
|
154
|
+
# touched by the ALTER), and append only the ONE new column -- whose
|
|
155
|
+
# name/type/nullability are already fully known from `add_column`'s own
|
|
156
|
+
# arguments, no introspection needed for it either.
|
|
157
|
+
def record_added_column!(table_name, column_name, type, options)
|
|
158
|
+
return if RAILS_INTERNAL_TABLES.include?(table_name.to_s)
|
|
159
|
+
|
|
160
|
+
ensure_schema_versions_table!
|
|
161
|
+
|
|
162
|
+
current_json = select_value("SELECT columns FROM schema_versions WHERE table_name = #{quote(table_name)} ORDER BY version DESC LIMIT 1")
|
|
163
|
+
current_cols = current_json ? JSON.parse(current_json) : []
|
|
164
|
+
|
|
165
|
+
new_col = { "name" => column_name.to_s, "virgodb_type" => type.to_s, "nullable" => options[:null] != false }
|
|
166
|
+
insert_schema_version!(table_name, current_cols + [ new_col ])
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def insert_schema_version!(table_name, cols)
|
|
170
|
+
next_version = (select_value("SELECT MAX(version) FROM schema_versions WHERE table_name = #{quote(table_name)}") || 0).to_i + 1
|
|
171
|
+
|
|
172
|
+
execute(<<~SQL)
|
|
173
|
+
INSERT INTO schema_versions (table_name, version, columns, applied_at)
|
|
174
|
+
VALUES (#{quote(table_name)}, #{next_version}, #{quote(cols.to_json)}, #{Time.now.to_i})
|
|
175
|
+
SQL
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Same DDL as Manifest::open()'s bootstrap on the Rust side (same
|
|
179
|
+
# table/column names, including the `table_name` scoping column) --
|
|
180
|
+
# IF NOT EXISTS makes this safe to call whether virgodb's own Rust
|
|
181
|
+
# code or this Ruby adapter created the manifest file first. Plain
|
|
182
|
+
# INTEGER PRIMARY KEY, not AUTOINCREMENT -- see the matching comment
|
|
183
|
+
# in manifest/src/lib.rs for why (breaks a real structure_dump/
|
|
184
|
+
# structure_load round trip the moment any host app's
|
|
185
|
+
# ActiveRecord::SchemaDumper.ignore_tables is non-empty).
|
|
186
|
+
def ensure_schema_versions_table!
|
|
187
|
+
execute(<<~SQL)
|
|
188
|
+
CREATE TABLE IF NOT EXISTS schema_versions (
|
|
189
|
+
id INTEGER PRIMARY KEY,
|
|
190
|
+
table_name TEXT NOT NULL,
|
|
191
|
+
version INTEGER NOT NULL,
|
|
192
|
+
columns TEXT NOT NULL,
|
|
193
|
+
applied_at INTEGER NOT NULL
|
|
194
|
+
)
|
|
195
|
+
SQL
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
ActiveRecord::ConnectionAdapters.register(
|
|
202
|
+
"virgodb",
|
|
203
|
+
"ActiveRecord::ConnectionAdapters::VirgodbAdapter",
|
|
204
|
+
"active_record/connection_adapters/virgodb_adapter"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
module ActiveRecord
|
|
208
|
+
module Tasks
|
|
209
|
+
# `ActiveRecord::Tasks::SQLiteDatabaseTasks#structure_dump` just shells
|
|
210
|
+
# out to the real `sqlite3` CLI (`.schema --nosys`, or a raw `SELECT sql
|
|
211
|
+
# FROM sqlite_master` when a host app has any
|
|
212
|
+
# `ActiveRecord::SchemaDumper.ignore_tables` configured -- e.g. excluding
|
|
213
|
+
# tables owned by a separate replication/backup mechanism) against the
|
|
214
|
+
# configured database file --
|
|
215
|
+
# DDL only. `ActiveRecord::Tasks::DatabaseTasks.dump_schema` then
|
|
216
|
+
# special-cases exactly one table on top of that: if `schema_migrations`
|
|
217
|
+
# exists, it appends that table's real row data (`connection
|
|
218
|
+
# .dump_schema_versions`) to the same dump file, because Rails' own
|
|
219
|
+
# migration bookkeeping needs those rows to survive a `db:schema:load`/
|
|
220
|
+
# `structure_load`, not just the table shape.
|
|
221
|
+
#
|
|
222
|
+
# `schema_versions` (this adapter's own bookkeeping table, plural,
|
|
223
|
+
# distinct from Rails' singular `schema_migrations`) needs exactly the
|
|
224
|
+
# same treatment and doesn't get it for free: it holds real data (the
|
|
225
|
+
# versioned column list `compact::schema_from_columns` on the Rust side
|
|
226
|
+
# depends on), not just structure, and Rails has no way to know that a
|
|
227
|
+
# table it didn't create needs the same row-preserving special case.
|
|
228
|
+
# Confirmed empirically: without this, a fresh database prepared via
|
|
229
|
+
# `structure_load` (which Rails does automatically whenever a `db:migrate`
|
|
230
|
+
# invocation for a later environment/connection finds an existing,
|
|
231
|
+
# version-matching dump already on disk from an earlier one -- not
|
|
232
|
+
# something this adapter can opt out of) ends up with the real
|
|
233
|
+
# host-app table but an EMPTY `schema_versions`, so
|
|
234
|
+
# `Manifest::current_schema_version()` on the Rust side returns `None`
|
|
235
|
+
# even though the SQLite table itself is correct.
|
|
236
|
+
class VirgodbDatabaseTasks < SQLiteDatabaseTasks
|
|
237
|
+
class << self
|
|
238
|
+
# Optional host-app hook: `->(sql_table_name) { {sort_by:, key_column:,
|
|
239
|
+
# partition_column:} or nil }`. Purely a documentation comment written
|
|
240
|
+
# above CREATE TABLE at dump time -- nothing on the Rust or Ruby side
|
|
241
|
+
# ever reads it back. This adapter gem stays host-app-agnostic (see
|
|
242
|
+
# the class doc comment above: "lets any Rails app..."), so it can't
|
|
243
|
+
# hardcode any one host app's sort_by/key_column config (that's
|
|
244
|
+
# app/services/virgodb/registry.rb's job, which already has this
|
|
245
|
+
# exact data for real -- passed to Virgodb.start_maintenance on every
|
|
246
|
+
# boot). A host app that wants it surfaced sets this once, e.g. in an
|
|
247
|
+
# initializer; left nil, structure_dump behaves exactly as before.
|
|
248
|
+
attr_accessor :table_layout_provider
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def structure_dump(filename, extra_flags)
|
|
252
|
+
super
|
|
253
|
+
reformat_create_table_statements!(filename)
|
|
254
|
+
annotate_physical_layout!(filename)
|
|
255
|
+
dump_schema_versions!(filename)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
private
|
|
259
|
+
|
|
260
|
+
# Only the LATEST schema_versions row per table_name is dumped.
|
|
261
|
+
# `Manifest::current_schema_version` (manifest/src/lib.rs) only ever
|
|
262
|
+
# reads `ORDER BY version DESC LIMIT 1` -- confirmed directly, no Rust
|
|
263
|
+
# or Ruby code anywhere reads an older version row, so keeping full
|
|
264
|
+
# history here isn't a correctness requirement. It also isn't a
|
|
265
|
+
# readability win: each version's `columns` re-lists every column from
|
|
266
|
+
# scratch, so N migrations means N near-duplicate JSON blobs, longer
|
|
267
|
+
# every time. The version count/dates are still worth one line for
|
|
268
|
+
# orientation -- the full column list per version isn't; the
|
|
269
|
+
# migration files themselves (db/migrate_virgodb_*/*.rb, timestamped,
|
|
270
|
+
# under git blame) are already the authoritative "what changed when"
|
|
271
|
+
# record. `register_schema_version`'s additive-only check is
|
|
272
|
+
# unaffected: it only ever compares a new migration's proposed columns
|
|
273
|
+
# against the CURRENT version, which is exactly what's still dumped.
|
|
274
|
+
def dump_schema_versions!(filename)
|
|
275
|
+
return unless connection.data_source_exists?("schema_versions")
|
|
276
|
+
|
|
277
|
+
rows = connection.select_all("SELECT id, table_name, version, columns, applied_at FROM schema_versions ORDER BY table_name, version")
|
|
278
|
+
return if rows.to_a.empty?
|
|
279
|
+
|
|
280
|
+
File.open(filename, "a") do |f|
|
|
281
|
+
f.puts
|
|
282
|
+
rows.to_a.group_by { |r| r["table_name"] }.each do |table_name, versions|
|
|
283
|
+
latest = versions.max_by { |r| r["version"] }
|
|
284
|
+
history = versions.map { |r| "v#{r['version']} (#{Time.at(r['applied_at'].to_i).utc.strftime('%Y-%m-%d')})" }.join(", ")
|
|
285
|
+
|
|
286
|
+
f.puts "-- #{table_name} schema history: #{history} -- only the current version is dumped below; see db/migrate_virgodb_*/ for the rest"
|
|
287
|
+
pretty_columns = JSON.pretty_generate(JSON.parse(latest["columns"]))
|
|
288
|
+
f.puts(
|
|
289
|
+
"INSERT INTO \"schema_versions\" (id, table_name, version, columns, applied_at) VALUES (\n" \
|
|
290
|
+
" #{latest['id']}, #{connection.quote(table_name)}, #{latest['version']},\n" \
|
|
291
|
+
" #{connection.quote(pretty_columns)},\n" \
|
|
292
|
+
" #{latest['applied_at']}\n" \
|
|
293
|
+
");"
|
|
294
|
+
)
|
|
295
|
+
f.puts
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Writes `table_layout_provider`'s sort_by/key_column/partition_column
|
|
301
|
+
# as a comment directly above each table's (already reformatted,
|
|
302
|
+
# one-column-per-line) CREATE TABLE -- the closest a SQLite-flavored
|
|
303
|
+
# dump can get to ClickHouse's own `ORDER BY`/`PARTITION BY` clauses
|
|
304
|
+
# being right there in the DDL. No-op (and no dump format change at
|
|
305
|
+
# all) when no provider is registered.
|
|
306
|
+
def annotate_physical_layout!(filename)
|
|
307
|
+
provider = self.class.table_layout_provider
|
|
308
|
+
return unless provider
|
|
309
|
+
|
|
310
|
+
content = File.read(filename)
|
|
311
|
+
content.gsub!(/^CREATE TABLE (IF NOT EXISTS )?"?(\w+)"? \(/) do
|
|
312
|
+
statement_start = Regexp.last_match(0)
|
|
313
|
+
table_name = Regexp.last_match(2)
|
|
314
|
+
layout = provider.call(table_name)
|
|
315
|
+
next statement_start unless layout
|
|
316
|
+
|
|
317
|
+
comment = [
|
|
318
|
+
"-- Physical layout (app/services/virgodb/registry.rb):",
|
|
319
|
+
"-- sort_by: #{layout[:sort_by].join(', ')}",
|
|
320
|
+
"-- key_column: #{layout[:key_column]} (run file min/max pruning bounds)",
|
|
321
|
+
"-- partition_column: #{layout[:partition_column]}"
|
|
322
|
+
].join("\n")
|
|
323
|
+
|
|
324
|
+
"#{comment}\n#{statement_start}"
|
|
325
|
+
end
|
|
326
|
+
File.write(filename, content)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# `SQLite3Adapter#structure_dump`'s raw `SELECT sql FROM sqlite_master`
|
|
330
|
+
# path (used whenever `ActiveRecord::SchemaDumper.ignore_tables` is
|
|
331
|
+
# non-empty, e.g. excluding tables owned by a separate replication/
|
|
332
|
+
# backup mechanism) reproduces each
|
|
333
|
+
# CREATE TABLE exactly as ActiveRecord originally generated it --
|
|
334
|
+
# one long line, every column crammed together. Fine for SQLite to
|
|
335
|
+
# load back (structure_load doesn't care about whitespace), useless to
|
|
336
|
+
# a human reading the dump next to `db/clickhouse_structure.sql`
|
|
337
|
+
# (ClickHouse's own `SHOW CREATE TABLE` is naturally one-column-per-line,
|
|
338
|
+
# for comparison). Purely cosmetic, safe to do unconditionally: only
|
|
339
|
+
# rewrites the exact same statement with different whitespace, no DDL
|
|
340
|
+
# semantics change. Skips statements that already span multiple lines
|
|
341
|
+
# (e.g. this adapter's own hand-written `schema_versions` DDL, created
|
|
342
|
+
# via a real newline-containing heredoc) -- nothing to reformat there.
|
|
343
|
+
def reformat_create_table_statements!(filename)
|
|
344
|
+
content = File.read(filename)
|
|
345
|
+
content.gsub!(/^CREATE TABLE (IF NOT EXISTS )?("[\w]+"|\w+) \((.*)\);$/) do
|
|
346
|
+
if_not_exists = Regexp.last_match(1)
|
|
347
|
+
table_name = Regexp.last_match(2)
|
|
348
|
+
columns = split_top_level_commas(Regexp.last_match(3))
|
|
349
|
+
next Regexp.last_match(0) if columns.size <= 1
|
|
350
|
+
|
|
351
|
+
"CREATE TABLE #{if_not_exists}#{table_name} (\n" + columns.map { |c| " #{c.strip}" }.join(",\n") + "\n);"
|
|
352
|
+
end
|
|
353
|
+
File.write(filename, content)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Splits a CREATE TABLE column list on commas that are NOT nested
|
|
357
|
+
# inside a type's own parens (e.g. `Decimal(18,2)`) -- a plain
|
|
358
|
+
# `String#split(",")` would wrongly cut `Decimal(18` and `2)` apart.
|
|
359
|
+
def split_top_level_commas(str)
|
|
360
|
+
parts = []
|
|
361
|
+
depth = 0
|
|
362
|
+
current = +""
|
|
363
|
+
str.each_char do |ch|
|
|
364
|
+
case ch
|
|
365
|
+
when "(" then depth += 1; current << ch
|
|
366
|
+
when ")" then depth -= 1; current << ch
|
|
367
|
+
when ","
|
|
368
|
+
depth.zero? ? (parts << current; current = +"") : current << ch
|
|
369
|
+
else
|
|
370
|
+
current << ch
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
parts << current unless current.strip.empty?
|
|
374
|
+
parts
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Registering our own subclass (not plain SQLiteDatabaseTasks) so
|
|
381
|
+
# schema_versions' row data survives the dump/load round trip -- see the
|
|
382
|
+
# class doc comment above for why a real Rails app can't avoid hitting this.
|
|
383
|
+
ActiveRecord::Tasks::DatabaseTasks.register_task(/virgodb/, "ActiveRecord::Tasks::VirgodbDatabaseTasks")
|
metadata
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: activerecord-virgodb-adapter
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0.dev.9f07a74
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- virgodb
|
|
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.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: sqlite3
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.4'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.4'
|
|
40
|
+
description: |
|
|
41
|
+
Lets any Rails app write real ActiveRecord::Migration subclasses against a virgodb manifest,
|
|
42
|
+
picked up by `rails db:migrate` the same way SQLite and ClickHouse migrations already are.
|
|
43
|
+
Subclasses ActiveRecord::ConnectionAdapters::SQLite3Adapter (virgodb's manifest is already a
|
|
44
|
+
real SQLite file), so structure_dump/the :sql schema format work for free.
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- LICENSE
|
|
50
|
+
- lib/active_record/connection_adapters/virgodb_adapter.rb
|
|
51
|
+
licenses:
|
|
52
|
+
- Nonstandard
|
|
53
|
+
metadata: {}
|
|
54
|
+
rdoc_options: []
|
|
55
|
+
require_paths:
|
|
56
|
+
- lib
|
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.0'
|
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0'
|
|
67
|
+
requirements: []
|
|
68
|
+
rubygems_version: 4.0.16
|
|
69
|
+
specification_version: 4
|
|
70
|
+
summary: ActiveRecord adapter for defining and evolving virgodb table schemas via
|
|
71
|
+
real migrations
|
|
72
|
+
test_files: []
|