activerecord-clickhouse-adapter 0.2.0 → 0.2.1

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: 3f16a02cdd51358de67461ac920dcecdb2dcec096b7c47898a91e2d555b5f0b0
4
- data.tar.gz: 49fdee335e90e3bdae9a28a5bce316d024f2161b45758128cdd7c424611da7d8
3
+ metadata.gz: 961d1de0d406b53d1244395ce2159a5629a335eda4f74e71b5bcdbf489c3ed01
4
+ data.tar.gz: e7022e7db80a7520c083c88fd058d98ddb79d0c4d8450628ba954304a104dbaf
5
5
  SHA512:
6
- metadata.gz: f13d3bcfce5f752f6e6c2b2ba4a559bfdae3b192d04ac27034e2d49b1151a4c659d392d005a44e53dc03d997a8cbc9631650a5d80756bb23bb4f416bd8ddffdb
7
- data.tar.gz: 2be555427541cf9329053c10acd00265e46780b6dd401b8309333e8d606b4d7a235fb18a35aacb4331c56abccbe48b725664dab2ceb91f81910d16b3fe314b90
6
+ metadata.gz: 3b8e3c624094591a222e1fc8bdc0f74abf8a8681bc9732c08b1c587b68a57f89273dbd1fb7f551e40d6ac8aa6f890cc98e531a329c4be45fa2b7a0db1efd458f
7
+ data.tar.gz: 2598a56859fd5b5c1882da961b8d333fe32e8131abea8173c176a0765cb7df49e733b3a037781cae349c0bc24fa191c50b5a2e5eaf55bb7f15dba8b0ee14cf57
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ # 0.2.1 (2026-07-24)
2
+
3
+ Fixed:
4
+
5
+ - Fixed comment hoisting corrupting `INSERT ... VALUES` when a string value contains an
6
+ unclosed `/*` (e.g. a request path of `/*`). The trailing-comment scan is now
7
+ string-literal aware, so a `/*` inside a value is never taken for the start of a trailing
8
+ sqlcommenter tag. Previously such a value mangled the statement and the server rejected the
9
+ whole batch with a `SYNTAX_ERROR`, dropping every row in it
10
+
1
11
  # 0.2.0 (2026-07-23)
2
12
 
3
13
  Breaking-ish (schema diff noise, not data changes):
data/README.md CHANGED
@@ -8,6 +8,7 @@ A fully featured Active Record adapter for [ClickHouse](https://clickhouse.com)
8
8
  - OLAP query surface: `FINAL`, `PREWHERE`, `SAMPLE`, `LIMIT BY`, time bucketing, approximate aggregates
9
9
  - Real instrumentation: rows read, bytes read, and server elapsed time on every query
10
10
  - Fast wire: RowBinary reads and chunked streaming inserts
11
+ - Multi-replica failover and server-enforced read-only connections
11
12
 
12
13
  Tested against a live ClickHouse server only — no mocked responses, ever.
13
14
 
@@ -80,6 +81,8 @@ end
80
81
 
81
82
  Columns are non-nullable by default, matching ClickHouse. Use `null: true` for `Nullable(...)`.
82
83
 
84
+ `t.datetime` defaults to precision 6 (microseconds, Rails' convention); pass `precision: nil` for the second-precision base `DateTime`. `t.binary`/`t.blob` map to `String` — ClickHouse strings are arbitrary byte sequences.
85
+
83
86
  Rails-style id tables also work — `id: :bigint` (plain Int64, no autoincrement) or `id: :uuid` creates the pk column as its own sorting key, ids arrive client-generated, and models auto-detect the primary key:
84
87
 
85
88
  ```ruby
@@ -298,6 +301,26 @@ clickhouse:
298
301
  - **Mutation counts are best-effort.** `update_all`/`delete_all` return a pre-mutation `SELECT count()` — ClickHouse reports no affected-row counts.
299
302
  - **Eventual merges.** `ReplacingMergeTree` deduplicates at merge time; read with `.final` when you need collapsed rows.
300
303
  - **Failover never replays a request.** With `hosts:`, only connect-phase failures (refused, unreachable, open timeout) move to the next replica — those cannot have executed anything. A mid-flight failure (read timeout, reset) raises, because the statement may already have run.
304
+ - **Read-only is server-enforced.** `read_only: true` stamps `readonly=2` on every request; the server's refusal (code 164) raises `ActiveRecord::ReadOnlyError` — the same class Rails' `while_preventing_writes` uses — including for server-configured readonly users. Grant refusals (code 497) raise `ClickHouse::AccessDenied`.
305
+ - **`matches` renders `ILIKE`.** ClickHouse `LIKE` is case-sensitive (unlike MySQL), so Arel's case-insensitive `matches`/`does_not_match` use native `ILIKE`.
306
+ - **No row locks.** Reads are isolated snapshots of parts, so `FOR UPDATE` renders as nothing and shared `lock`/`with_lock` code runs instead of dying. Optimistic locking via `lock_version` works end-to-end.
307
+
308
+ ## Migrating from clickhouse-activerecord
309
+
310
+ This gem registers the same `"clickhouse"` adapter name as [clickhouse-activerecord](https://github.com/PNixx/clickhouse-activerecord) and accepts its `database.yml` shape — swap the gem and the config carries over. The two gems cannot be loaded at once.
311
+
312
+ Schema-version bookkeeping does not carry over. The incumbent records applied versions in a `(version, active, ver)` ReplacingMergeTree with `active = 0` tombstones; this adapter uses Rails' plain `schema_migrations` table and does not read that shape, so a sink migrated by the incumbent reports zero applied versions.
313
+
314
+ Never re-run migrations over a live sink to fix that — replaying DDL over final-state tables can corrupt data (enum narrowing, for one). Adopt the version state once instead:
315
+
316
+ ```ruby
317
+ versions = connection.select_values("SELECT version FROM schema_migrations FINAL WHERE active = 1")
318
+ connection.execute("RENAME TABLE schema_migrations TO schema_migrations_legacy")
319
+ pool.schema_migration.create_table
320
+ versions.each { |v| pool.schema_migration.create_version(v) }
321
+ ```
322
+
323
+ Pending migrations run normally afterwards. To roll back the adoption, drop the new `schema_migrations` table and rename the legacy one back.
301
324
 
302
325
  ## Development
303
326
 
@@ -317,7 +340,7 @@ RAILS_SOURCE=edge bundle install
317
340
  RAILS_SOURCE=edge bundle exec rspec
318
341
  ```
319
342
 
320
- The suite includes a Rails compatibility harness that runs vendored upstream Active Record test suites (~4,500 tests) against the adapter. See `spec/rails_compat/`.
343
+ The suite includes a Rails compatibility harness that runs vendored upstream Active Record test suites (~5,600 tests — 5,558 runs, 447 skips, each skip documenting the dialect truth behind it) against the adapter. See `spec/rails_compat/`.
321
344
 
322
345
  ## History
323
346
 
@@ -295,24 +295,45 @@ module ActiveRecord
295
295
  end
296
296
  end
297
297
 
298
- TRAILING_COMMENTS = %r{(?:\s*/\*(?:[^*]|\*(?!/))*\*/)+\s*\z}
298
+ COMMENT = %r{/\*(?:[^*]|\*(?!/))*\*/}m
299
299
  INSERT_STATEMENT = /\A\s*INSERT\b/i
300
- private_constant :TRAILING_COMMENTS, :INSERT_STATEMENT
301
-
302
- # ClickHouse reads everything after VALUES with the Values input format, which
303
- # rejects trailing comments (probed 2026-07-13), so sqlcommenter tags appended
304
- # by Rails' QueryLogs are hoisted to the front of INSERT statements.
305
- def hoist_trailing_comments(sql)
306
- # ClickHouse strings are byte sequences; SQL carrying invalid UTF-8 must reach
307
- # the server rather than die in these regexes, so it drops to binary (the
308
- # patterns are pure ASCII and match byte-wise).
300
+ # Data outside strings/comments: any non-space run, but a `/` only when it doesn't open a comment.
301
+ DATA_RUN = %r{(?:[^\s'`/]|/(?!\*))+}
302
+ private_constant :COMMENT, :INSERT_STATEMENT, :DATA_RUN
303
+
304
+ # ClickHouse reads everything after VALUES with the Values input format, which rejects
305
+ # trailing comments (probed 2026-07-13), so sqlcommenter tags Rails' QueryLogs appends
306
+ # are hoisted to the front of INSERTs. Scans string-aware a /* inside a value (a request
307
+ # path like "/*") is opaque and never taken for a comment, so the VALUES list stays intact.
308
+ def hoist_trailing_comments(sql) # rubocop:disable Metrics/MethodLength
309
+ # ClickHouse strings are byte sequences; SQL carrying invalid UTF-8 must reach the server
310
+ # rather than die in the scan, so it drops to binary (the patterns are pure ASCII).
309
311
  sql = sql.b unless sql.valid_encoding?
310
312
  return sql unless INSERT_STATEMENT.match?(sql)
311
313
 
312
- comments = sql[TRAILING_COMMENTS]
313
- return sql unless comments
314
+ scanner = StringScanner.new(sql)
315
+ data_end = 0
316
+ trailing = []
317
+ until scanner.eos?
318
+ next if scanner.skip(/\s+/)
319
+
320
+ if (comment = scanner.scan(COMMENT))
321
+ trailing << comment
322
+ else
323
+ consume_data_token(scanner)
324
+ trailing.clear
325
+ data_end = scanner.pos
326
+ end
327
+ end
328
+ return sql if trailing.empty?
329
+
330
+ "#{trailing.join(" ")} #{sql[0...data_end]}"
331
+ end
314
332
 
315
- "#{comments.strip} #{sql[0...-comments.length]}"
333
+ # One opaque data token: a string literal or backtick identifier (skipped whole, so a /*
334
+ # inside never reads as a comment), an ordinary data run, or a lone char that starts none.
335
+ def consume_data_token(scanner)
336
+ scanner.scan(STRING_LITERAL) || scanner.scan(BACKTICK_IDENTIFIER) || scanner.scan(DATA_RUN) || scanner.getch
316
337
  end
317
338
 
318
339
  # Rails main (8.2) funnels execution through a QueryIntent object; released
@@ -3,7 +3,7 @@
3
3
  module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module ClickHouse
6
- VERSION = "0.2.0"
6
+ VERSION = "0.2.1"
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-clickhouse-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ikraam Ghoor
@@ -76,7 +76,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
76
  - !ruby/object:Gem::Version
77
77
  version: '0'
78
78
  requirements: []
79
- rubygems_version: 4.0.10
79
+ rubygems_version: 4.0.3
80
80
  specification_version: 4
81
81
  summary: ClickHouse database adapter for Active Record
82
82
  test_files: []