activerecord-virgodb-adapter 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 757fecfe8176d3354cf42df451f013c814c2699b3ac1206a055a3ada8196fd10
4
- data.tar.gz: 945f9ab23d59f055951dcbe55786545f106d7c0c90a26c23699a9e61a32cbec2
3
+ metadata.gz: d6376b065e29146c096491b6ce2a68e4331e806161a570b6549b068561a85f1f
4
+ data.tar.gz: 1555e75fdf0c2418c1d3439fb1940f7dd284e48e28109248ffd249454b41dc55
5
5
  SHA512:
6
- metadata.gz: 24f8356243e4ab3349e1b94c4fb4d561d28e86575442bca4e7185cb1c2a39c745c3f98a25b7bdca0e06f09b5f9cf50af121963fba72d021d2ab3e61b53461dc3
7
- data.tar.gz: f9c6ced6922d7254082c043e2317507d303b63fb4708a957ca8cf33f6fb462711e72690a19074639f37138f5d0e80c884ec13cc0e08d4fd9255edc99ad0df026
6
+ metadata.gz: ab5ef2e0113e6c38295495cb10d1c2ca4bb77d135b65a636cf58aee8777146bc29795b1c7d3d2950898735a81367e2d88e3ce235a3d4f567b7e934ae8d69e885
7
+ data.tar.gz: 7375e48809a1b05cc4a1ae8add6ed5e1a36d33a4564e0c711a8a8228e910e4f79d9ad039aaeccd66b839b88a4ff31640aec9e5d0f9ac246398d320b6b1224611
@@ -37,9 +37,13 @@ module ActiveRecord
37
37
  #
38
38
  # Additive-only, matching `Manifest::register_schema_version`'s own
39
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.
40
+ # `rename_column`, `change_column`, and `rename_table` are all disabled
41
+ # outright, and `assert_additive!` (below) catches every OTHER way an
42
+ # existing column can be altered (`change_column_null`,
43
+ # `change_column_default`, `remove_columns`, and anything else that
44
+ # reaches `create_table` via SQLite3Adapter's rebuild-the-table
45
+ # fallback) -- all rather than silently letting the real SQLite table
46
+ # drift out of sync with what `schema_versions` describes.
43
47
 
44
48
  # virgodb's raw "Timestamp" type (crates/compact/src/schema.rs) is plain
45
49
  # UNIX seconds -- an Integer, not a real SQL DATETIME/TIMESTAMP string --
@@ -50,9 +54,8 @@ module ActiveRecord
50
54
  # real failure modes from that mismatch, not just cosmetic ones:
51
55
  #
52
56
  # 1. `Type::DateTime.new.cast("0")` -- "0" being a perfectly valid
53
- # epoch-seconds default (e.g. exit_page_at/split_test_id_at's own
54
- # DEFAULT 0 sentinel, see
55
- # db/migrate_virgodb/20260727000002_create_ahoy_visits.rb in hintpot)
57
+ # epoch-seconds default (e.g. a real host app declaring `DEFAULT 0`
58
+ # as a sentinel on a nullable-in-spirit timestamp column)
56
59
  # -- returns nil. `Column#default` itself is unaffected (DateTime is
57
60
  # `mutable?`, so `Column#initialize` keeps the raw string rather than
58
61
  # deserializing it -- see ActiveModel::Type::DateTime#mutable?'s own
@@ -158,9 +161,9 @@ module ActiveRecord
158
161
  # falls through to `Type.default_value`, an untyped
159
162
  # `ActiveModel::Type::Value` whose `.type` is nil.
160
163
  # 2. An `AnyLast <type> <order_by_column>` column (the two-scalar-
161
- # column tuple-workaround pairs -- see
162
- # db/migrate_virgodb/20260727000002_create_ahoy_visits.rb in
163
- # hintpot) can get a WRONG match instead of no match: Rails'
164
+ # column tuple-workaround pairs a real host app might use to
165
+ # emulate ClickHouse-style last-touch columns) can get a WRONG
166
+ # match instead of no match: Rails'
164
167
  # %r(date)i is an unanchored substring regex, and
165
168
  # "AnyLast String updated_at" contains the substring "date"
166
169
  # (from "upDATEd_at") purely by accident, mis-casting a String
@@ -233,6 +236,70 @@ module ActiveRecord
233
236
  raise NotImplementedError, "virgodb schemas are additive-only -- an existing column's type can't change"
234
237
  end
235
238
 
239
+ # Not caught by `assert_additive!` -- SQLite renames a table via a
240
+ # native, lightweight `ALTER TABLE ... RENAME TO ...` (unlike
241
+ # `remove_columns`/`change_column_null`/`change_column_default`,
242
+ # never `alter_table`'s rebuild-the-table fallback), so it never
243
+ # reaches `record_schema_version!` at all. Confirmed by direct
244
+ # repro: renaming left `schema_versions` (and, on the real Rust
245
+ # side, `run_files`/`inline_batches`/every other manifest catalog
246
+ # table `manifest::Manifest` scopes by `table_name`) still keyed
247
+ # under the OLD name while the real SQLite table now answers to the
248
+ # new one -- every future migration or query against either name
249
+ # would silently miss the other's history. Blocked outright, same
250
+ # as the other identity-changing operations above: nothing in this
251
+ # codebase's fixed table set (events/visits/engagements/heatmaps,
252
+ # hardcoded in `Virgodb::Registry::TABLES` on the Ruby side and
253
+ # implicitly on the Rust side by whatever `table_name` callers pass)
254
+ # has a legitimate reason to rename.
255
+ def rename_table(table_name, new_name, **options)
256
+ raise NotImplementedError, "virgodb schemas are additive-only -- a table can't be renamed once created (schema_versions and the Rust-side manifest catalog are both keyed by table_name)"
257
+ end
258
+
259
+ # `ActiveRecord::Tasks::DatabaseTasks.truncate_tables(db_config)` --
260
+ # what `db:truncate_all` and, through it, `db:seed:replant` both run
261
+ # for every configured database -- passes `conn.tables` (literally
262
+ # everything `create_table` has ever touched, unfiltered) straight
263
+ # into this method. `AbstractAdapter::DatabaseStatements
264
+ # #truncate_tables` already subtracts exactly two names from that
265
+ # list before truncating: `pool.schema_migration.table_name` and
266
+ # `pool.internal_metadata.table_name` -- Rails' OWN bookkeeping
267
+ # tables. `schema_versions` (this adapter's bookkeeping table) and
268
+ # the Rust side's manifest catalog tables (`Manifest::open()`'s
269
+ # bootstrap, crates/manifest/src/lib.rs: run_files, inline_batches,
270
+ # orphaned_files, flush_completions, maintenance_lease, tombstones)
271
+ # get no such exemption -- Rails has no way to know this adapter's
272
+ # `tables` list contains anything other than real app data.
273
+ #
274
+ # Found by a real host app, not assumed: its CI ran
275
+ # `db:seed:replant` (truncate_all + reseed) as its last step, which
276
+ # silently zeroed out schema_versions with nothing to reload it
277
+ # afterward -- `ActiveRecord::Migration.maintain_test_schema!` only
278
+ # checks for PENDING migrations, saw none, and left the now-empty
279
+ # table alone. The NEXT `bin/rails test` run then failed immediately:
280
+ # nothing left to read a table's committed column list from.
281
+ # Confirmed directly against a real manifest: truncate_all correctly
282
+ # left schema_migrations/ar_internal_metadata alone but zeroed
283
+ # schema_versions (4 rows -> 0).
284
+ #
285
+ # Fixed the same way Rails protects its own two tables: subtract
286
+ # this adapter's catalog tables before delegating to `super`. All 7
287
+ # names are excluded on principle, even though a manifest built from
288
+ # pure-Ruby migrations alone only ever populates schema_versions
289
+ # (the other 6 only exist once the Rust side's `Manifest::open()`
290
+ # has bootstrapped them) -- truncating any of them out from under a
291
+ # running maintenance thread (crates/compact/src/maintenance.rs)
292
+ # would be exactly as destructive as this bug, just harder to
293
+ # trigger from Ruby alone.
294
+ MANIFEST_CATALOG_TABLES = %w[
295
+ schema_versions run_files inline_batches orphaned_files
296
+ flush_completions maintenance_lease tombstones
297
+ ].freeze
298
+
299
+ def truncate_tables(*table_names)
300
+ super(*(table_names - MANIFEST_CATALOG_TABLES))
301
+ end
302
+
236
303
  private
237
304
 
238
305
  # Reads the table back via ActiveRecord's own column introspection --
@@ -274,9 +341,61 @@ module ActiveRecord
274
341
  { "name" => c.name, "virgodb_type" => c.sql_type, "nullable" => c.null, "default" => stringify_default(c.default) }
275
342
  end
276
343
 
344
+ assert_additive!(table_name, cols)
277
345
  insert_schema_version!(table_name, cols)
278
346
  end
279
347
 
348
+ # `remove_column`/`rename_column`/`change_column` are blocked outright
349
+ # above, but SQLite3Adapter has THREE other public methods
350
+ # (`change_column_null`, `change_column_default`, `remove_columns`
351
+ # -- plural) that were never individually overridden and reach the
352
+ # exact same place: none of them support SQLite's native ALTER
353
+ # TABLE, so `SQLite3Adapter#alter_table` falls back to its
354
+ # rebuild-the-table strategy (`move_table`/`copy_table`), which
355
+ # calls `create_table` -- THIS adapter's overridden one -- as part
356
+ # of that rebuild. Confirmed empirically, not assumed: calling
357
+ # `change_column_null` on a real virgodb-backed table raised
358
+ # nothing at all, silently succeeded, and left `schema_versions`
359
+ # with a brand-new "version" whose columns had both the intended
360
+ # change AND every untouched column's `virgodb_type` corrupted to
361
+ # Rails' generic SQL type name ("INTEGER" instead of "Int64") --
362
+ # the exact same re-introspection landmine `record_added_column!`'s
363
+ # own doc comment already documents and specifically avoids for
364
+ # `add_column`. Without this check, that corrupted row would reach
365
+ # the Rust side's `manifest::register_schema_version`, either
366
+ # getting rejected there too (in which case the Ruby-side SQLite
367
+ # table and the Rust-side manifest catalog would have already
368
+ # silently diverged -- the migration "succeeded" from Rails'
369
+ # perspective) or, worse, if a future version happened to look
370
+ # additive by coincidence, corrupting every future read of that
371
+ # column's stored type.
372
+ #
373
+ # Rather than hunt down and individually block every current and
374
+ # future Rails method that can reach `alter_table`'s rebuild path,
375
+ # this enforces the actual invariant (mirroring `manifest::Manifest
376
+ # ::register_schema_version`'s own `columns.starts_with(&current)`
377
+ # check exactly) at the one place ALL of them funnel through:
378
+ # `record_schema_version!`, called by the overridden `create_table`
379
+ # on every real table AND on every rebuild `create_table` call
380
+ # `alter_table` makes internally. A genuinely new table (no prior
381
+ # schema_versions row) always passes trivially. Runs inside the
382
+ # same transaction `alter_table` already wraps its rebuild in, so
383
+ # raising here rolls back the real SQLite table change too --
384
+ # verified directly: after a rejected `change_column_null` call,
385
+ # the table's real column list is byte-for-byte what it was before
386
+ # the call, not left half-rebuilt.
387
+ def assert_additive!(table_name, new_cols)
388
+ current_json = select_value("SELECT columns FROM schema_versions WHERE table_name = #{quote(table_name)} ORDER BY version DESC LIMIT 1")
389
+ return unless current_json
390
+
391
+ current_cols = JSON.parse(current_json)
392
+ return if new_cols.first(current_cols.length) == current_cols
393
+
394
+ raise NotImplementedError,
395
+ "virgodb schemas are additive-only -- table `#{table_name}`'s existing columns can't be removed, reordered, renamed, or have their type/nullable/default changed (only add_column is supported for evolving an existing table). " \
396
+ "current columns: #{current_cols.inspect}; attempted: #{new_cols.inspect}"
397
+ end
398
+
280
399
  # A migration's `default:` (e.g. `t.column :clid, "String", null:
281
400
  # false, default: ""`) round-trips through `columns(table_name).first
282
401
  # .default` as a real, correctly-typed Ruby value now (String/Integer/
@@ -113,6 +113,14 @@ module ActiveRecord
113
113
  raw = SQLite3::Database.new(path, readonly: true)
114
114
  raw.busy_timeout = 5_000
115
115
  raw.execute("SELECT DISTINCT table_name FROM schema_versions").flatten
116
+ rescue SQLite3::SQLException
117
+ # File.exist? isn't proof schema_versions exists -- SQLite
118
+ # auto-creates an empty file the instant something connects to a
119
+ # path that doesn't exist yet, and Rails' parallel-test setup
120
+ # (ActiveRecord::TestDatabases.create_and_load_schema, one file per
121
+ # worker) does exactly that via schema_up_to_date? moments before
122
+ # calling drop. Nothing to clean up in that case.
123
+ []
116
124
  ensure
117
125
  raw&.close
118
126
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-virgodb-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - virgodb