briefly 0.2.1 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6c561612c29ff83aaaccdf483e6e563633e54d3dc81c92856857db2f26978b3
4
- data.tar.gz: a257e3be54bb8e1bb076009dc5982a7c15a6add60f16225bb774e9e3100f3db4
3
+ metadata.gz: 74e48b9af79f6a7c0b0f1ff2f601a7718e3a8071d314ee305aa36d0d43db7e77
4
+ data.tar.gz: fbdfadc26d3c27bd2c7393c1e03771313cf263911dbe392d712868219451d269
5
5
  SHA512:
6
- metadata.gz: 4496b43e5d42fb6b90d0b8f528092207ff867dfbab5febe1d36b69451c307527bd1a056da118a20a48770b77601c49c8965018fa76c904fc203642a4bd5dc178
7
- data.tar.gz: 430188265df3b33c124f90cbf28de0b34f27fa3090174e74d60a94a63d32a97a4bb905bfb46a72e50263977a44c89cecd29a66799f25847e520ee268071ea18e
6
+ metadata.gz: d62e84d079845a5206409e9d5897b1ab55eadc64676791b07ce178a73135c440714b784439a960a434eb3c84aa355e9265ff0fde2a4ed98259364f9e03424d14
7
+ data.tar.gz: 40a3f01ecf038df93220d73df675c5e25422d0f483950100d976ecd9e8dc4bf5b01dca50db8fb7b750a2c19ff5b35d0074489bd46a850a991c18dc9cdc29beb0
data/CHANGELOG.md CHANGED
@@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## v0.3.0 (2026-10-06)
10
+
11
+ ### Added
12
+ - `App.db.value(sql, *binds)` returns the first column of the first row through `select_value`, or
13
+ `nil` for no row or SQL NULL. It uses the same positional and named binds as `select` and `query`.
14
+
15
+ ### Changed
16
+ - **BREAKING:** the minimum supported Ruby is now 3.3. Ruby 3.2 reached end of life in March 2026, and the CI matrix no longer tests it.
17
+
9
18
  ## v0.2.1 (2026-07-31)
10
19
 
11
20
  No change to `lib/` or `sig/`: this release carries the documentation the gem ships and the process
data/README.md CHANGED
@@ -6,9 +6,9 @@ A terse, curated facade over your application's most reached-for objects.
6
6
  [![CI](https://github.com/svyatov/briefly/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/svyatov/briefly/actions/workflows/main.yml)
7
7
  [![coverage](https://codecov.io/gh/svyatov/briefly/branch/main/graph/badge.svg)](https://app.codecov.io/gh/svyatov/briefly)
8
8
 
9
- - **Ruby 3.2 and up.** Rails is optional: the gem does not declare it, and `Briefly::Rails` is
9
+ - **Ruby 3.3 and up.** Rails is optional: the gem does not declare it, and `Briefly::Rails` is
10
10
  autoloaded only when you name it. The Rails packs themselves need Rails 7.2 or newer.
11
- - **25 shortcuts across six packs.** Config, env, view, db, instrument and reload, each usable on
11
+ - **26 shortcuts across six packs.** Config, env, view, db, instrument and reload, each usable on
12
12
  its own or taken together through the `"rails"` umbrella.
13
13
  - **One runtime dependency.** [candor](https://github.com/svyatov/candor), which is itself
14
14
  dependency-free.
@@ -349,7 +349,7 @@ end
349
349
  | `Briefly::Rails::Config` | `"rails/config"` | `config`, `config_x`, `root`, `cache`, `logger`, `credentials`, `error`, `config_for` |
350
350
  | `Briefly::Rails::Env` | `"rails/env"` | `env` and its predicates |
351
351
  | `Briefly::Rails::View` | `"rails/view"` | `helpers`, `routes`, `renderer`, `render` |
352
- | `Briefly::Rails::DB` | `"rails/db"` | `connection`, `transaction`, `select`, `query`, `connected_to`, `reading`, `writing` |
352
+ | `Briefly::Rails::DB` | `"rails/db"` | `connection`, `transaction`, `select`, `value`, `query`, `connected_to`, `reading`, `writing` |
353
353
  | `Briefly::Rails::Instrument` | `"rails/instrument"` | `instrument` |
354
354
  | `Briefly::Rails::Reload` | `"rails/reload"` | none; clears memos on every code reload |
355
355
 
@@ -370,6 +370,7 @@ end
370
370
  | `connection` | `conn` | forwards keywords and the block to `base.with_connection`, yielding the connection and auto-releasing |
371
371
  | `transaction` | `txn` | forwards keywords and the block to `base.transaction` |
372
372
  | `select` | | `base.with_connection { \|c\| c.select_all(sql) }`, a read (SELECT) on the cache-aware path |
373
+ | `value` | | `base.with_connection { \|c\| c.select_value(sql) }`, the first column of the first row, or `nil` for no row or SQL NULL |
373
374
  | `query` | | `base.with_connection { \|c\| c.exec_query(sql) }`, arbitrary SQL, writes and DDL included |
374
375
  | `connected_to` | | forwards every argument to `base.connected_to` (`role:`, `shard:`, `prevent_writes:`) |
375
376
  | `reading` | | runs the block under the `:reading` role |
@@ -384,22 +385,27 @@ App = Briefly.define do
384
385
  end
385
386
 
386
387
  App.db.txn { App.db.select("select * from users where id = ?", 1) }
387
- App.db.conn { |c| c.select_value("select count(*) from users") }
388
+ App.db.value("select count(*) from users")
388
389
  ```
389
390
 
390
- `select` and `query` are the two raw-SQL helpers, differing only in which adapter path they take.
391
+ `select`, `value` and `query` are the raw-SQL helpers, differing only in which adapter path they take.
391
392
  `select(sql, *binds)` runs a read through `select_all`, the path Rails recommends for a raw SELECT,
392
393
  returning an `ActiveRecord::Result` without clearing the query cache. `query(sql, *binds)` runs
393
394
  arbitrary SQL through `exec_query`: reads, writes, and DDL all execute. The name tells you which
394
395
  you're reaching for; neither polices the SQL it's handed, so `select` will happily run a write you
395
396
  give it. The split is name and cache-path, not a runtime guard.
396
397
 
397
- Both sanitize through `base.sanitize_sql_array` when binds are given, and pass the statement through
398
+ `value(sql, *binds)` reads through `select_value` when you need a single result, such as a count.
399
+ It returns the first column of the first row, or `nil` for no row or SQL NULL. Its arguments follow
400
+ the same bind convention as `select` and `query`, rather than the adapter's full signature.
401
+
402
+ All three sanitize through `base.sanitize_sql_array` when binds are given, and pass the statement through
398
403
  untouched when they are not. Positional and named binds both work, from application code:
399
404
 
400
405
  ```ruby
401
406
  App.db.select("select * from users where name like '%ada%'") # no binds, passed through
402
407
  App.db.select("select * from users where id = ?", 123) # positional
408
+ App.db.value("select count(*) from users where active = :active", active: true)
403
409
  App.db.query("update users set active = true where id = :id", id: 1) # named, a write via `query`
404
410
  ```
405
411
 
data/briefly.gemspec CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
15
15
  spec.homepage = "https://github.com/svyatov/briefly"
16
16
  spec.license = "MIT"
17
17
 
18
- spec.required_ruby_version = ">= 3.2.0"
18
+ spec.required_ruby_version = ">= 3.3.0"
19
19
 
20
20
  spec.require_paths = ["lib"]
21
21
  spec.files = Dir["lib/**/*.{rb,tt}"] + Dir["sig/**/*"] +
@@ -9,7 +9,7 @@ module Briefly
9
9
  # A validated {#compile!} pass, ready for a {Briefly::Facade} to install. +child_plans+ holds one
10
10
  # +[child_facade, child_plan]+ pair per namespace this pass collected, so +__commit+ can walk the
11
11
  # tree children first.
12
- Plan = Struct.new(:defs, :rescue_entries, :children, :child_plans)
12
+ Plan = Data.define(:defs, :rescue_entries, :children, :child_plans)
13
13
 
14
14
  # @return [Briefly::Facade] the facade under construction, for packs that need lifecycle hooks
15
15
  attr_reader :facade
@@ -16,7 +16,7 @@ module Briefly
16
16
  # the captured class would go stale on the first code reload — permanently, since {Reload} clears
17
17
  # memos, not closures. A Module is accepted for applications outside the autoloader, with that caveat.
18
18
  #
19
- # Nothing here memoizes: the block-form shortcuts can't be memoized, and +select+/+query+ take
19
+ # Nothing here memoizes: the block-form shortcuts can't be memoized, and +select+/+value+/+query+ take
20
20
  # arguments. So the pack does not wire {Reload}, and works without a booted application.
21
21
  #
22
22
  # +connected_to+ forwards every argument to +base.connected_to+ — +role:+, +shard:+, +prevent_writes:+,
@@ -48,11 +48,12 @@ module Briefly
48
48
  # always reads, while `shard:` and `prevent_writes:` still forward.
49
49
  builder.shortcut(:reading) { |**opts, &blk| connected_to(**opts, role: :reading, &blk) }
50
50
  builder.shortcut(:writing) { |**opts, &blk| connected_to(**opts, role: :writing, &blk) }
51
- # `select` and `query` share one closure, differing only by the adapter method. `select` runs a
51
+ # `select`, `value` and `query` share one closure, differing only by the adapter method. `select` runs a
52
52
  # read through `select_all` — the read-optimized path for a raw SELECT, returning an
53
53
  # `ActiveRecord::Result` without clearing the query cache. `query` runs arbitrary SQL through
54
54
  # `exec_query` — writes and DDL included. Neither polices the SQL it's given; the split is name
55
55
  # and cache-path, not a runtime read/write guard.
56
+ # `value` uses `select_value` for the first column of the first row, or nil for no row or SQL NULL.
56
57
  #
57
58
  # `with_connection`, not `connection`: the latter is soft-deprecated, and raises outright under
58
59
  # `ActiveRecord.permanent_connection_checkout = :disallowed`.
@@ -60,7 +61,7 @@ module Briefly
60
61
  # A bindless statement must skip `sanitize_sql_array`, which would fall through to its
61
62
  # `statement % values` branch and raise on any literal `%` — `... like '%foo%'`.
62
63
  #
63
- # No `**` parameter on either shortcut, deliberately: taking no keywords is what makes Ruby pack
64
+ # No `**` parameter on these shortcuts, deliberately: taking no keywords is what makes Ruby pack
64
65
  # `select(sql, id: 1)` into `binds` as a trailing Hash, which is how named binds reach
65
66
  # `sanitize_sql_array`. A `**opts` would swallow them and send the statement unbound.
66
67
  run = lambda do |method, sql, binds|
@@ -69,6 +70,7 @@ module Briefly
69
70
  record.with_connection { |connection| connection.public_send(method, statement) }
70
71
  end
71
72
  builder.shortcut(:select) { |sql, *binds| run.call(:select_all, sql, binds) }
73
+ builder.shortcut(:value) { |sql, *binds| run.call(:select_value, sql, binds) }
72
74
  builder.shortcut(:query) { |sql, *binds| run.call(:exec_query, sql, binds) }
73
75
  builder
74
76
  end
@@ -10,7 +10,7 @@ module Briefly
10
10
  # mutex, because rebinding alone would drop concurrent registrations.
11
11
  class Rescues
12
12
  # A single registration.
13
- Entry = Struct.new(:klass, :handler)
13
+ Entry = Data.define(:klass, :handler)
14
14
 
15
15
  def initialize
16
16
  @entries = [].freeze
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Briefly
4
4
  # The gem version.
5
- VERSION = "0.2.1"
5
+ VERSION = "0.3.0"
6
6
  end
@@ -39,6 +39,7 @@ end
39
39
  # db <%= name %>.db.connection { |c| }, <%= name %>.db.conn { |c| } -> yields a connection, auto-releases
40
40
  # <%= name %>.db.transaction { }, <%= name %>.db.txn { } -> wraps the block in a transaction
41
41
  # <%= name %>.db.select(sql, id: 1) / select(sql, 1) -> a read (SELECT); named or positional binds
42
+ # <%= name %>.db.value(sql, id: 1) / value(sql, 1) -> first column of first row; nil for no row or SQL NULL
42
43
  # <%= name %>.db.query(sql, id: 1) / query(sql, 1) -> arbitrary SQL (writes, DDL); same binds
43
44
  # <%= name %>.db.connected_to(role:, shard:, prevent_writes:) { }
44
45
  # <%= name %>.db.reading { }, <%= name %>.db.writing { } -> run the block under a role
@@ -2,11 +2,11 @@ module Briefly
2
2
  # Receives the DSL. Blocks are `instance_eval`'d here, never on the facade.
3
3
  class Builder
4
4
  # A validated `compile!` pass, ready for a `Facade` to install.
5
- class Plan < ::Struct[untyped]
6
- attr_accessor defs: Hash[Symbol, Shortcut]
7
- attr_accessor rescue_entries: Array[Briefly::rescue_entry]
8
- attr_accessor children: Hash[Symbol, Facade]
9
- attr_accessor child_plans: Array[[ Facade, Plan ]]
5
+ class Plan < ::Data
6
+ attr_reader defs: Hash[Symbol, Shortcut]
7
+ attr_reader rescue_entries: Array[Briefly::rescue_entry]
8
+ attr_reader children: Hash[Symbol, Facade]
9
+ attr_reader child_plans: Array[[ Facade, Plan ]]
10
10
 
11
11
  def self.new: (Hash[Symbol, Shortcut] defs, Array[Briefly::rescue_entry] rescue_entries, Hash[Symbol, Facade] children, Array[[ Facade, Plan ]] child_plans) -> Plan
12
12
  end
@@ -2,9 +2,9 @@ module Briefly
2
2
  # An ordered list of facade-wide and global `rescue_from` registrations, queried at dispatch time.
3
3
  class Rescues
4
4
  # A single registration.
5
- class Entry < ::Struct[untyped]
6
- attr_accessor klass: Class
7
- attr_accessor handler: Briefly::handler
5
+ class Entry < ::Data
6
+ attr_reader klass: Class
7
+ attr_reader handler: Briefly::handler
8
8
 
9
9
  def self.new: (Class klass, Briefly::handler handler) -> Entry
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: briefly
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leonid Svyatov
@@ -77,14 +77,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
77
77
  requirements:
78
78
  - - ">="
79
79
  - !ruby/object:Gem::Version
80
- version: 3.2.0
80
+ version: 3.3.0
81
81
  required_rubygems_version: !ruby/object:Gem::Requirement
82
82
  requirements:
83
83
  - - ">="
84
84
  - !ruby/object:Gem::Version
85
85
  version: '0'
86
86
  requirements: []
87
- rubygems_version: 4.0.16
87
+ rubygems_version: 4.0.20
88
88
  specification_version: 4
89
89
  summary: A terse, curated facade over your application's most reached-for objects.
90
90
  test_files: []