activerecord-trino-adapter 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b38f0bee0dc9764f269632d9b138de7aff8c4fb3fb696c8862bd8de23a34999e
4
+ data.tar.gz: 8a24ba5209ee11e82ee43d67182798599b96b1a9f1a621b4f60edf0a2ae3c0ed
5
+ SHA512:
6
+ metadata.gz: 83cba7a9d4c291d021d94729bd2e1fbb37e101ec19749163b4eebcee99969360cd46e3831e0d952eb63770d6db53e46abce9627fa82208f3103b38361c15148b
7
+ data.tar.gz: fd39bfe9b1025178a0a95e35b03bbd9617a3859931e7eea02af7a8ce96a8cf02dd7090469fe08ceca19208a1c9f53a20d83345ca78229e2adb426408102fa624
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright Power Home Remodeling Group, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # activerecord-trino-adapter
2
+
3
+ A read-only ActiveRecord SQL adapter for [Trino](https://trino.io/), built on top of the [`trino-client`](https://rubygems.org/gems/trino-client) gem.
4
+
5
+ Lets a Rails application query a Trino data warehouse using familiar ActiveRecord scopes and `where` chains while preventing accidental writes. Designed for analytical use cases where the warehouse is the source of truth and the application only needs to read from it.
6
+
7
+ ## Features
8
+
9
+ - **Read-only by design.** All write paths (`insert`, `update`, `delete`, transactions, migrations, schema changes) raise `ActiveRecord::Trino::ReadOnlyError`.
10
+ - **ActiveRecord-native.** Plugs into Rails 7.1+ multi-database via `database.yml` and `connects_to`.
11
+ - **Opinionated safety belts.** `find_each` / `find_in_batches` are banned (they don't fit Trino's pagination model); hard query timeouts default to 150 seconds; slow queries emit `ActiveSupport::Notifications` for any subscriber to pick up.
12
+ - **SQL-injection conscious.** Trino has no parameterized queries, so every literal flows through a tight, fuzz-tested `quote` implementation.
13
+ - **Schema introspection** via Trino's `information_schema.columns`, with a small but practical type map (varchar, integer, decimal, boolean, date, timestamp, timestamp with time zone, json, etc.).
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem "activerecord-trino-adapter"
21
+ ```
22
+
23
+ Then in your `config/database.yml`:
24
+
25
+ ```yaml
26
+ warehouse:
27
+ adapter: trino
28
+ host: <%= ENV["TRINO_HOST"] %>
29
+ port: <%= ENV.fetch("TRINO_PORT", 8080) %>
30
+ ssl: <%= ENV.fetch("TRINO_SSL", "false") %>
31
+ user: <%= ENV["TRINO_USER"] %>
32
+ password: <%= ENV["TRINO_PASSWORD"] %>
33
+ catalog: <%= ENV["TRINO_CATALOG"] %>
34
+ schema: <%= ENV["TRINO_SCHEMA"] %>
35
+ query_timeout: 150
36
+ plan_timeout: 30
37
+ ```
38
+
39
+ And an abstract record that connects to it:
40
+
41
+ ```ruby
42
+ class WarehouseRecord < ActiveRecord::Base
43
+ self.abstract_class = true
44
+ connects_to database: { reading: :warehouse }
45
+ end
46
+
47
+ class SalesByDay < WarehouseRecord
48
+ self.table_name = "sales_by_day"
49
+ end
50
+ ```
51
+
52
+ Now standard AR queries work against Trino:
53
+
54
+ ```ruby
55
+ SalesByDay.where(month: "2026-04").order(:territory_id).limit(100)
56
+ ```
57
+
58
+ ## What is and is not supported
59
+
60
+ Supported:
61
+ - `where`, `order`, `limit`, `offset`, `select`, `pluck`, `find_by`, `count`, `sum`, `average`
62
+ - Scopes, including chained scopes
63
+ - Type-cast reads for varchar, integer (all widths), real/double, decimal, boolean, date, timestamp, timestamp with time zone, time, json, uuid
64
+
65
+ Not supported (raises):
66
+ - Any write path: `save`, `update`, `delete`, `destroy`, `insert`, `create_table`, transactions, savepoints
67
+ - `find_each` / `find_in_batches` — Trino's pagination model is incompatible; use explicit `LIMIT`/`OFFSET` or `pluck` aggregates
68
+ - Trino composite types (`array`, `map`, `row`) in result casting — select scalar columns or extract via Trino SQL (`element_at`, dot access)
69
+
70
+ Out of design scope:
71
+ - `joins` are not a design goal. The adapter passes SQL through to Trino, so a join across two Trino-backed models technically works, but it is not tested and the intended usage pattern is to query flat denormalized warehouse tables.
72
+ - Cross-database joins (e.g., a MySQL model joined to a Trino model) do not work — Rails 7.1+ disallows joins across connection handles.
73
+
74
+ ## Configuration options
75
+
76
+ All keys are read from the `database.yml` entry:
77
+
78
+ | Key | Default | Description |
79
+ |---|---|---|
80
+ | `host` | _required_ | Trino server hostname (e.g. `trino.example.com`) — no scheme, no port |
81
+ | `port` | `8080` (HTTP) / `443` (HTTPS) | Trino server port |
82
+ | `ssl` | `false` | Whether to use HTTPS. When `true`, also passing `password` requires the connection to be HTTPS (trino-client policy) |
83
+ | `user` | _required_ | Trino user |
84
+ | `password` | _nil_ | Optional basic-auth password |
85
+ | `catalog` | _required_ | Default Trino catalog |
86
+ | `schema` | _required_ | Default Trino schema |
87
+ | `query_timeout` | `150` | Hard ceiling on query duration, in seconds. Cap lower for user-facing paths and higher for backfills |
88
+ | `plan_timeout` | `30` | Ceiling on Trino query-planning phase, in seconds |
89
+ | `slow_query_threshold_seconds` | `20` | Threshold above which an `active_record_trino.slow_query` notification is emitted |
90
+
91
+ ## Instrumentation
92
+
93
+ The adapter uses ActiveRecord's standard `AbstractAdapter#log` for query instrumentation, so any `ActiveSupport::Notifications` subscriber on `sql.active_record` picks up Trino queries automatically.
94
+
95
+ In addition, queries exceeding `slow_query_threshold_seconds` emit an `active_record_trino.slow_query` notification with payload `{ sql:, duration:, query_id:, info_uri: }`. The `info_uri` deep-links to the query's stats page in the Trino web UI, which is handy for diagnosing slow paths.
96
+
97
+ ## Diagnostics
98
+
99
+ For one-off latency investigation, `ActiveRecord::Trino::Diagnostics.profile(model_class)` runs a sample query and reports where the time went, broken down between the Ruby/AR side and Trino's own per-query stats:
100
+
101
+ ```ruby
102
+ ActiveRecord::Trino::Diagnostics.profile(SalesByDay)
103
+ # => {
104
+ # schema_time: 0.45, # Ruby-side seconds for information_schema.columns
105
+ # query_time: 1.12, # Ruby-side seconds for the sample SELECT
106
+ # query_id: "20260520_...", # Trino query_id (deep-link via info_uri)
107
+ # info_uri: "https://...", # URL to the query's stats page
108
+ # queued_time_ms: 50, # Trino-side: queued waiting for resources
109
+ # elapsed_time_ms: 800, # Trino-side: total wall clock
110
+ # cpu_time_ms: 200, # Trino-side: CPU time spent
111
+ # wall_time_ms: 750, # Trino-side: execution wall time
112
+ # state: "FINISHED"
113
+ # }
114
+ ```
115
+
116
+ The same metadata is available on the connection after any query:
117
+
118
+ ```ruby
119
+ SalesByDay.first
120
+ connection = SalesByDay.connection
121
+ connection.last_query_id # Trino query_id of the most recent query
122
+ connection.last_query_info_uri # Direct URL to the Trino UI for that query
123
+ connection.last_query_stats # Hash of state, queued_time_millis, elapsed_time_millis, etc.
124
+ ```
125
+
126
+ ## Development
127
+
128
+ 1. Clone the repository.
129
+ 2. Install dependencies: `bundle install`.
130
+ 3. Run the test suite: `bundle exec rspec`.
131
+ 4. Run the linter: `bundle exec rubocop`.
132
+
133
+ ### Testing across Rails versions
134
+
135
+ Use Appraisal to run the suite against each supported Rails minor:
136
+
137
+ ```bash
138
+ bundle exec appraisal install
139
+ bundle exec appraisal rspec
140
+ ```
141
+
142
+ ### Testing changes locally in another app
143
+
144
+ ```ruby
145
+ # In the consuming application's Gemfile
146
+ gem "activerecord-trino-adapter", path: "path/to/activerecord-trino-adapter"
147
+ ```
148
+
149
+ ## License
150
+
151
+ This project is licensed under the MIT License — see the [LICENSE.txt](LICENSE.txt) file for details.
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/connection_adapters/column"
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module Trino
8
+ class Column < ActiveRecord::ConnectionAdapters::Column
9
+ def initialize(name:, sql_type:, type:, null: true)
10
+ metadata = ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(
11
+ sql_type: sql_type,
12
+ type: type.type
13
+ )
14
+ super(name.to_s, nil, metadata, null)
15
+ @cast_type = type
16
+ end
17
+
18
+ attr_reader :cast_type
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Trino
6
+ module DatabaseStatements
7
+ SLOW_QUERY_NOTIFICATION = "active_record_trino.slow_query"
8
+
9
+ STAT_FIELDS = %i[
10
+ state
11
+ queued_time_millis
12
+ elapsed_time_millis
13
+ cpu_time_millis
14
+ wall_time_millis
15
+ ].freeze
16
+
17
+ InternalResult = Struct.new(:column_names, :rows, :column_types, keyword_init: true)
18
+
19
+ def execute(sql, name = nil, **_kwargs)
20
+ log(sql, name) { run_trino_query(sql) }
21
+ end
22
+
23
+ # Rails 7.1 public path.
24
+ def exec_query(sql, name = "SQL", binds = [], prepare: false, async: false)
25
+ internal_exec_query(sql, name, binds, prepare: prepare, async: async)
26
+ end
27
+
28
+ # Rails 7.2+/8.0 canonical path that AR's select_all/select route through.
29
+ # rubocop:disable Lint/UnusedMethodArgument, Metrics/ParameterLists
30
+ def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false)
31
+ unless binds.empty?
32
+ raise ActiveRecord::Trino::Error,
33
+ "activerecord-trino-adapter: bind variables are not supported; got #{binds.size} bind(s)"
34
+ end
35
+
36
+ internal = log(sql, name) { run_trino_query(sql) }
37
+ ActiveRecord::Result.new(internal.column_names, internal.rows, internal.column_types)
38
+ end
39
+ # rubocop:enable Lint/UnusedMethodArgument, Metrics/ParameterLists
40
+
41
+ def select_value(arel, name = nil, binds = [])
42
+ result = select_all(arel, name, binds)
43
+ result.rows.first&.first
44
+ end
45
+
46
+ def select_values(arel, name = nil, binds = [])
47
+ result = select_all(arel, name, binds)
48
+ result.rows.map(&:first)
49
+ end
50
+
51
+ private
52
+
53
+ # rubocop:disable Metrics/AbcSize
54
+ def run_trino_query(sql)
55
+ start = monotonic_now
56
+ query = client.query(sql)
57
+ internal = consume_query(query)
58
+ capture_query_metadata(query)
59
+ notify_slow_query(sql, monotonic_now - start)
60
+ internal
61
+ rescue ::Trino::Client::TrinoQueryTimeoutError => e
62
+ raise ActiveRecord::StatementTimeout.new(e.message, sql: sql)
63
+ rescue ::Trino::Client::TrinoQueryError => e
64
+ raise ActiveRecord::StatementInvalid.new(e.message, sql: sql)
65
+ rescue ::Trino::Client::TrinoHttpError => e
66
+ raise ActiveRecord::ConnectionFailed, e.message
67
+ ensure
68
+ query&.close if defined?(query) && query
69
+ end
70
+ # rubocop:enable Metrics/AbcSize
71
+
72
+ def consume_query(query)
73
+ columns = query.columns || []
74
+ rows = []
75
+ query.each_row_chunk { |chunk| rows.concat(chunk) if chunk }
76
+
77
+ column_names = columns.map(&:name)
78
+ column_types = columns.to_h { |c| [c.name, type_map.lookup(c.type)] }
79
+ InternalResult.new(column_names: column_names, rows: rows, column_types: column_types)
80
+ end
81
+
82
+ # Pull the Trino-side query metadata off of the final QueryResults
83
+ # page so callers can cross-reference in the Trino UI. Defensive
84
+ # against minor model differences across trino-client versions.
85
+ def capture_query_metadata(query)
86
+ results = query.current_results
87
+ return unless results
88
+
89
+ @last_query_id = results.id if results.respond_to?(:id)
90
+ @last_query_info_uri = results.info_uri if results.respond_to?(:info_uri)
91
+ @last_query_stats = extract_stats(results.stats) if results.respond_to?(:stats)
92
+ end
93
+
94
+ def extract_stats(stats)
95
+ return {} unless stats
96
+
97
+ STAT_FIELDS.each_with_object({}) do |field, acc|
98
+ acc[field] = stats.public_send(field) if stats.respond_to?(field)
99
+ end
100
+ end
101
+
102
+ def notify_slow_query(sql, duration)
103
+ return if duration < @slow_query_threshold
104
+
105
+ ActiveSupport::Notifications.instrument(
106
+ SLOW_QUERY_NOTIFICATION,
107
+ sql: sql,
108
+ duration: duration,
109
+ query_id: @last_query_id,
110
+ info_uri: @last_query_info_uri
111
+ )
112
+ end
113
+
114
+ def monotonic_now
115
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "date"
5
+ require "time"
6
+
7
+ module ActiveRecord
8
+ module ConnectionAdapters
9
+ module Trino
10
+ module Quoting
11
+ QUOTED_TRUE = "true"
12
+ QUOTED_FALSE = "false"
13
+ QUOTED_NULL = "NULL"
14
+
15
+ TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S.%3N"
16
+ DATE_FORMAT = "%Y-%m-%d"
17
+
18
+ NUL_BYTE = /\x00/
19
+
20
+ # rubocop:disable Metrics/CyclomaticComplexity, Lint/DuplicateBranch
21
+ def quote(value)
22
+ case value
23
+ when nil then QUOTED_NULL
24
+ when true then QUOTED_TRUE
25
+ when false then QUOTED_FALSE
26
+ when BigDecimal then value.to_s("F")
27
+ when Numeric then value.to_s
28
+ when ::Date then "DATE '#{value.strftime(DATE_FORMAT)}'"
29
+ when ::Time, ::DateTime then quote_time(value)
30
+ when Symbol, String then quote_string_literal(value.to_s)
31
+ else quote_string_literal(value.to_s)
32
+ end
33
+ end
34
+ # rubocop:enable Metrics/CyclomaticComplexity, Lint/DuplicateBranch
35
+
36
+ def quote_string(string)
37
+ str = string.to_s
38
+ if str.match?(NUL_BYTE)
39
+ raise ActiveRecord::Trino::Error,
40
+ "activerecord-trino-adapter: NUL byte detected in literal; refusing to quote"
41
+ end
42
+ str.gsub("'", "''")
43
+ end
44
+
45
+ def quote_column_name(name)
46
+ %("#{name.to_s.gsub('"', '""')}")
47
+ end
48
+
49
+ def quote_table_name(name)
50
+ name.to_s.split(".").map { |part| quote_column_name(part) }.join(".")
51
+ end
52
+
53
+ def quoted_true
54
+ QUOTED_TRUE
55
+ end
56
+
57
+ def quoted_false
58
+ QUOTED_FALSE
59
+ end
60
+
61
+ def quoted_date(value)
62
+ value.strftime(DATE_FORMAT)
63
+ end
64
+
65
+ private
66
+
67
+ def quote_string_literal(string)
68
+ "'#{quote_string(string)}'"
69
+ end
70
+
71
+ def quote_time(value)
72
+ time = value.respond_to?(:utc) ? value.utc : value
73
+ "TIMESTAMP '#{time.strftime(TIMESTAMP_FORMAT)}'"
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Trino
6
+ module ReadOnly
7
+ WRITE_METHODS = %i[
8
+ insert
9
+ update
10
+ delete
11
+ exec_insert
12
+ exec_update
13
+ exec_delete
14
+ insert_fixture
15
+ insert_fixtures_set
16
+ truncate
17
+ truncate_tables
18
+ begin_db_transaction
19
+ commit_db_transaction
20
+ rollback_db_transaction
21
+ exec_rollback_db_transaction
22
+ begin_isolated_db_transaction
23
+ create_savepoint
24
+ exec_rollback_to_savepoint
25
+ release_savepoint
26
+ create_table
27
+ drop_table
28
+ create_join_table
29
+ drop_join_table
30
+ rename_table
31
+ add_column
32
+ remove_column
33
+ change_column
34
+ change_column_default
35
+ change_column_null
36
+ rename_column
37
+ add_index
38
+ remove_index
39
+ rename_index
40
+ add_foreign_key
41
+ remove_foreign_key
42
+ add_reference
43
+ remove_reference
44
+ add_belongs_to
45
+ remove_belongs_to
46
+ add_check_constraint
47
+ remove_check_constraint
48
+ ].freeze
49
+
50
+ WRITE_METHODS.each do |method|
51
+ define_method(method) do |*_args, **_kwargs, &_block|
52
+ raise ActiveRecord::Trino::ReadOnlyError,
53
+ "activerecord-trino-adapter: #{method} is not supported by the Trino adapter " \
54
+ "(activerecord-trino-adapter is read-only)"
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Trino
6
+ # Behaviors that constrain AR's typical OLTP-shaped query patterns so they
7
+ # don't accidentally hammer a Trino warehouse. Currently:
8
+ #
9
+ # - `find_each` / `find_in_batches` raise: Trino's pagination model does
10
+ # not fit AR's batching cursor.
11
+ #
12
+ # Timeouts and slow-query notifications live in DatabaseStatements; they
13
+ # apply regardless of caller pattern.
14
+ module SafetyBelts
15
+ BATCH_METHODS = %i[find_each find_in_batches in_batches].freeze
16
+
17
+ BATCH_METHODS.each do |method|
18
+ define_method(method) do |*_args, **_kwargs, &_block|
19
+ raise ActiveRecord::Trino::Error,
20
+ "activerecord-trino-adapter: #{method} is not supported on Trino-backed models. " \
21
+ "Use explicit LIMIT/OFFSET pagination or pluck aggregates instead."
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Trino
6
+ module SchemaStatements
7
+ def columns(table_name)
8
+ rows = run_trino_query(columns_query(table_name.to_s)).rows
9
+ rows.map do |name, data_type, is_nullable|
10
+ Trino::Column.new(
11
+ name: name,
12
+ sql_type: data_type,
13
+ type: type_map.lookup(data_type),
14
+ null: nullable?(is_nullable)
15
+ )
16
+ end
17
+ end
18
+
19
+ def data_sources
20
+ run_trino_query("SHOW TABLES").rows.map(&:first)
21
+ end
22
+ alias tables data_sources
23
+
24
+ def table_exists?(table_name)
25
+ data_sources.include?(table_name.to_s)
26
+ end
27
+ alias data_source_exists? table_exists?
28
+
29
+ def primary_key(_table_name)
30
+ nil
31
+ end
32
+
33
+ def indexes(_table_name)
34
+ []
35
+ end
36
+
37
+ def foreign_keys(_table_name)
38
+ []
39
+ end
40
+
41
+ def views
42
+ []
43
+ end
44
+
45
+ def view_exists?(_view_name)
46
+ false
47
+ end
48
+
49
+ def schema_cache
50
+ @schema_cache ||= ActiveRecord::ConnectionAdapters::SchemaCache.new(self)
51
+ end
52
+
53
+ private
54
+
55
+ def columns_query(table_name)
56
+ <<~SQL.strip
57
+ SELECT column_name, data_type, is_nullable
58
+ FROM information_schema.columns
59
+ WHERE table_catalog = #{quote(trino_catalog)}
60
+ AND table_schema = #{quote(trino_schema)}
61
+ AND table_name = #{quote(table_name)}
62
+ ORDER BY ordinal_position
63
+ SQL
64
+ end
65
+
66
+ def trino_catalog
67
+ @client_options[:catalog]
68
+ end
69
+
70
+ def trino_schema
71
+ @client_options[:schema]
72
+ end
73
+
74
+ def nullable?(value)
75
+ value.to_s.casecmp?("yes")
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module Trino
8
+ class TypeMap
9
+ DECIMAL_PATTERN = /\Adecimal\((\d+)\s*(?:,\s*(\d+))?\)\z/i
10
+ TIMESTAMP_TZ_PATTERN = /\Atimestamp(?:\(\d+\))?\s+with\s+time\s+zone\z/i
11
+ COMPOSITE_PATTERN = /\A(?:array|map|row)\(/i
12
+
13
+ def self.build
14
+ new
15
+ end
16
+
17
+ def initialize
18
+ @cache = {}
19
+ end
20
+
21
+ def lookup(trino_type)
22
+ @cache[trino_type] ||= build_type(trino_type.to_s)
23
+ end
24
+
25
+ private
26
+
27
+ def build_type(sql_type)
28
+ normalized = sql_type.strip.downcase
29
+
30
+ string_type(normalized) ||
31
+ integer_type(normalized) ||
32
+ float_type(normalized) ||
33
+ date_time_type(normalized) ||
34
+ decimal_type(normalized) ||
35
+ scalar_type(normalized) ||
36
+ unsupported_type(normalized)
37
+ end
38
+
39
+ def string_type(normalized)
40
+ case normalized
41
+ when "varchar", /\Avarchar\(\d+\)\z/,
42
+ "char", /\Achar\(\d+\)\z/,
43
+ "varbinary", /\Avarbinary\(\d+\)\z/,
44
+ "uuid", "ipaddress", "hyperloglog", "qdigest"
45
+ ActiveModel::Type::String.new
46
+ end
47
+ end
48
+
49
+ def integer_type(normalized)
50
+ case normalized
51
+ when "tinyint" then ActiveModel::Type::Integer.new(limit: 1)
52
+ when "smallint" then ActiveModel::Type::Integer.new(limit: 2)
53
+ when "integer", "int" then ActiveModel::Type::Integer.new(limit: 4)
54
+ when "bigint" then ActiveModel::Type::Integer.new(limit: 8)
55
+ end
56
+ end
57
+
58
+ def float_type(normalized)
59
+ case normalized
60
+ when "real", "double" then ActiveModel::Type::Float.new
61
+ end
62
+ end
63
+
64
+ def date_time_type(normalized)
65
+ case normalized
66
+ when "date" then ActiveModel::Type::Date.new
67
+ when "time", /\Atime\(\d+\)\z/ then ActiveModel::Type::Time.new
68
+ when TIMESTAMP_TZ_PATTERN then ActiveRecord::Trino::Type::TimestampWithZone.new
69
+ when "timestamp", /\Atimestamp\(\d+\)\z/ then ActiveModel::Type::DateTime.new
70
+ end
71
+ end
72
+
73
+ def decimal_type(normalized)
74
+ return unless (match = DECIMAL_PATTERN.match(normalized))
75
+
76
+ ActiveModel::Type::Decimal.new(
77
+ precision: match[1].to_i,
78
+ scale: (match[2] || 0).to_i
79
+ )
80
+ end
81
+
82
+ def scalar_type(normalized)
83
+ case normalized
84
+ when "boolean" then ActiveModel::Type::Boolean.new
85
+ when "json" then ActiveRecord::Trino::Type::Json.new
86
+ end
87
+ end
88
+
89
+ def unsupported_type(normalized)
90
+ ActiveRecord::Trino::Type::Unsupported.new(normalized)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "active_record/connection_adapters/abstract_adapter"
5
+ require "trino-client"
6
+
7
+ require "active_record/trino"
8
+
9
+ require_relative "trino/quoting"
10
+ require_relative "trino/type_map"
11
+ require_relative "trino/column"
12
+ require_relative "trino/database_statements"
13
+ require_relative "trino/schema_statements"
14
+ require_relative "trino/read_only"
15
+ require_relative "trino/safety_belts"
16
+
17
+ module ActiveRecord
18
+ module ConnectionAdapters
19
+ class TrinoAdapter < AbstractAdapter
20
+ ADAPTER_NAME = "Trino"
21
+
22
+ include Trino::Quoting
23
+ include Trino::DatabaseStatements
24
+ include Trino::SchemaStatements
25
+ include Trino::ReadOnly
26
+
27
+ def initialize(...)
28
+ super
29
+ @client_options = ActiveRecord::Trino::Config.client_options(@config)
30
+ @slow_query_threshold = ActiveRecord::Trino::Config.slow_query_threshold(@config)
31
+ @client = build_client
32
+ install_safety_belts!
33
+ end
34
+
35
+ def adapter_name
36
+ ADAPTER_NAME
37
+ end
38
+
39
+ def active?
40
+ !@client.nil?
41
+ end
42
+
43
+ def reconnect!
44
+ disconnect!
45
+ @client = build_client
46
+ end
47
+
48
+ def disconnect!
49
+ @client = nil
50
+ end
51
+
52
+ def supports_transactions?
53
+ false
54
+ end
55
+
56
+ def supports_savepoints?
57
+ false
58
+ end
59
+
60
+ def supports_lazy_transactions?
61
+ false
62
+ end
63
+
64
+ def supports_advisory_locks?
65
+ false
66
+ end
67
+
68
+ def supports_explain?
69
+ true
70
+ end
71
+
72
+ def supports_migrations?
73
+ false
74
+ end
75
+
76
+ def supports_ddl_transactions?
77
+ false
78
+ end
79
+
80
+ def supports_views?
81
+ false
82
+ end
83
+
84
+ # ActiveRecord's adapter API expects this method name without a question mark.
85
+ def prepared_statements # rubocop:disable Naming/PredicateMethod
86
+ false
87
+ end
88
+
89
+ def requires_reloading?
90
+ false
91
+ end
92
+
93
+ def native_database_types
94
+ {}
95
+ end
96
+
97
+ def lookup_cast_type(sql_type)
98
+ type_map.lookup(sql_type)
99
+ end
100
+
101
+ def lookup_cast_type_from_column(column)
102
+ column.cast_type
103
+ end
104
+
105
+ attr_reader :client, :last_query_id, :last_query_info_uri, :last_query_stats
106
+
107
+ private
108
+
109
+ def build_client
110
+ ::Trino::Client.new(@client_options)
111
+ end
112
+
113
+ def type_map
114
+ @type_map ||= Trino::TypeMap.build
115
+ end
116
+
117
+ def install_safety_belts!
118
+ return unless defined?(::ActiveRecord::Relation)
119
+
120
+ return if ::ActiveRecord::Relation.include?(RelationSafetyBelts)
121
+
122
+ ::ActiveRecord::Relation.prepend(RelationSafetyBelts)
123
+ end
124
+
125
+ # The safety belts override AR::Relation batch methods only when the relation's
126
+ # connection is a TrinoAdapter, so they don't leak into other databases used in
127
+ # the same process.
128
+ module RelationSafetyBelts
129
+ Trino::SafetyBelts::BATCH_METHODS.each do |method|
130
+ define_method(method) do |*args, **kwargs, &block|
131
+ if connection.is_a?(::ActiveRecord::ConnectionAdapters::TrinoAdapter)
132
+ raise ActiveRecord::Trino::Error,
133
+ "activerecord-trino-adapter: #{method} is not supported on Trino-backed models. " \
134
+ "Use explicit LIMIT/OFFSET pagination or pluck aggregates instead."
135
+ else
136
+ super(*args, **kwargs, &block)
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Trino
5
+ module Config
6
+ DEFAULT_QUERY_TIMEOUT = 150
7
+ DEFAULT_PLAN_TIMEOUT = 30
8
+ DEFAULT_SLOW_QUERY_THRESHOLD_SECONDS = 20
9
+ DEFAULT_HTTP_PORT = 8080
10
+ DEFAULT_HTTPS_PORT = 443
11
+
12
+ REQUIRED_KEYS = %i[host user catalog schema].freeze
13
+
14
+ module_function
15
+
16
+ def client_options(config)
17
+ symbolized = symbolize(config)
18
+ validate!(symbolized)
19
+ ssl = symbolized.fetch(:ssl, false)
20
+ port = symbolized.fetch(:port, default_port(ssl))
21
+ {
22
+ server: "#{symbolized[:host]}:#{port}",
23
+ user: symbolized[:user],
24
+ password: symbolized[:password],
25
+ catalog: symbolized[:catalog],
26
+ schema: symbolized[:schema],
27
+ ssl: ssl,
28
+ http_proxy: symbolized[:http_proxy],
29
+ time_zone: symbolized[:time_zone],
30
+ query_timeout: symbolized.fetch(:query_timeout, DEFAULT_QUERY_TIMEOUT),
31
+ plan_timeout: symbolized.fetch(:plan_timeout, DEFAULT_PLAN_TIMEOUT),
32
+ }.compact
33
+ end
34
+
35
+ def default_port(ssl)
36
+ ssl ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT
37
+ end
38
+
39
+ def slow_query_threshold(config)
40
+ symbolized = symbolize(config)
41
+ symbolized.fetch(:slow_query_threshold_seconds, DEFAULT_SLOW_QUERY_THRESHOLD_SECONDS).to_f
42
+ end
43
+
44
+ def validate!(config)
45
+ symbolized = symbolize(config)
46
+ missing = REQUIRED_KEYS.reject { |k| symbolized[k] && !symbolized[k].to_s.empty? }
47
+ return if missing.empty?
48
+
49
+ raise ActiveRecord::Trino::ConfigurationError,
50
+ "activerecord-trino-adapter: missing required config keys: #{missing.join(', ')}"
51
+ end
52
+
53
+ def symbolize(config)
54
+ config.to_h.transform_keys(&:to_sym)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "benchmark"
4
+
5
+ module ActiveRecord
6
+ module Trino
7
+ # Diagnostic helpers for Trino-backed models. Useful when
8
+ # investigating why a Trino-backed query is slow.
9
+ module Diagnostics
10
+ module_function
11
+
12
+ # Profile a Trino-backed AR model's first-query latency. Resets
13
+ # the column cache to force a schema re-fetch, times it, then runs
14
+ # a sample SELECT and pulls the per-query breakdown from Trino's
15
+ # own response.
16
+ #
17
+ # Returns a Hash with:
18
+ # :schema_time Float seconds spent on information_schema.columns
19
+ # :query_time Float seconds spent on the sample SELECT (Ruby-side)
20
+ # :query_id Trino query_id (cross-reference in the Trino UI)
21
+ # :info_uri URL to the query's stats page in the Trino UI
22
+ # :queued_time_ms Trino-side: time the query spent queued
23
+ # :elapsed_time_ms Trino-side: total wall clock (queued + execution)
24
+ # :cpu_time_ms Trino-side: CPU time spent on the query
25
+ # :wall_time_ms Trino-side: wall-clock execution time (planning + exec)
26
+ # :state "FINISHED" / "FAILED" / etc.
27
+ #
28
+ # Note: the Trino 351 StatementStats model does not expose
29
+ # planning_time as a discrete field — it is folded into wall_time.
30
+ # If you need the planning slice specifically, subtract wall_time
31
+ # from elapsed_time as a rough approximation.
32
+ def profile(model_class)
33
+ connection = model_class.connection
34
+ model_class.reset_column_information
35
+ schema_time = ::Benchmark.realtime { model_class.columns }
36
+
37
+ # Issue the sample SELECT through the adapter directly so it is the
38
+ # last query touching the connection — AR's higher-level paths can
39
+ # fire follow-up bookkeeping queries (SHOW TABLES, etc.) that would
40
+ # otherwise overwrite last_query_* before we read them.
41
+ table = connection.quote_table_name(model_class.table_name)
42
+ sql = "SELECT * FROM #{table} LIMIT 1"
43
+ query_time = ::Benchmark.realtime { connection.exec_query(sql) }
44
+
45
+ build_result(connection, schema_time, query_time)
46
+ end
47
+
48
+ def build_result(connection, schema_time, query_time)
49
+ stats = connection.last_query_stats || {}
50
+ {
51
+ schema_time: schema_time,
52
+ query_time: query_time,
53
+ query_id: connection.last_query_id,
54
+ info_uri: connection.last_query_info_uri,
55
+ queued_time_ms: stats[:queued_time_millis],
56
+ elapsed_time_ms: stats[:elapsed_time_millis],
57
+ cpu_time_ms: stats[:cpu_time_millis],
58
+ wall_time_ms: stats[:wall_time_millis],
59
+ state: stats[:state],
60
+ }
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Trino
5
+ class Error < StandardError; end
6
+
7
+ class ReadOnlyError < Error; end
8
+
9
+ class UnsupportedTypeError < Error; end
10
+
11
+ class ConfigurationError < Error; end
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require "json"
5
+
6
+ module ActiveRecord
7
+ module Trino
8
+ module Type
9
+ class Json < ActiveModel::Type::Value
10
+ def type
11
+ :json
12
+ end
13
+
14
+ def cast(value)
15
+ return value if value.nil? || value.is_a?(::Hash) || value.is_a?(::Array)
16
+ return value unless value.is_a?(::String)
17
+
18
+ ::JSON.parse(value)
19
+ rescue ::JSON::ParserError
20
+ value
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require "time"
5
+
6
+ module ActiveRecord
7
+ module Trino
8
+ module Type
9
+ class TimestampWithZone < ActiveModel::Type::Value
10
+ def type
11
+ :datetime
12
+ end
13
+
14
+ def cast(value)
15
+ return value if value.nil? || value.is_a?(::Time) || value.is_a?(::DateTime)
16
+ return value unless value.is_a?(::String)
17
+
18
+ ::Time.parse(value)
19
+ rescue ::ArgumentError
20
+ value
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module ActiveRecord
6
+ module Trino
7
+ module Type
8
+ class Unsupported < ActiveModel::Type::Value
9
+ def initialize(trino_type = nil)
10
+ super()
11
+ @trino_type = trino_type
12
+ end
13
+
14
+ def type
15
+ :unsupported
16
+ end
17
+
18
+ def cast(_value)
19
+ raise ActiveRecord::Trino::UnsupportedTypeError,
20
+ "activerecord-trino-adapter: cannot cast Trino type #{@trino_type.inspect}; " \
21
+ "select scalar columns explicitly or extract values via Trino SQL"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Trino
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "active_support"
5
+ require "active_support/notifications"
6
+ require "trino-client"
7
+
8
+ require_relative "trino/version"
9
+ require_relative "trino/errors"
10
+ require_relative "trino/config"
11
+ require_relative "trino/type/json"
12
+ require_relative "trino/type/timestamp_with_zone"
13
+ require_relative "trino/type/unsupported"
14
+ require_relative "trino/diagnostics"
15
+
16
+ if ActiveRecord::ConnectionAdapters.respond_to?(:register)
17
+ ActiveRecord::ConnectionAdapters.register(
18
+ "trino",
19
+ "ActiveRecord::ConnectionAdapters::TrinoAdapter",
20
+ "active_record/connection_adapters/trino_adapter"
21
+ )
22
+ else
23
+ # Rails 7.1 uses the legacy adapter_method pattern: ActiveRecord::Base needs
24
+ # a trino_connection class method that returns a new TrinoAdapter. Rails 7.2+
25
+ # replaced this with ConnectionAdapters.register above.
26
+ require "active_record/connection_adapters/trino_adapter"
27
+ ActiveRecord::Base.singleton_class.define_method(:trino_connection) do |config|
28
+ ActiveRecord::ConnectionAdapters::TrinoAdapter.new(config)
29
+ end
30
+ end
31
+
32
+ module ActiveRecord
33
+ module Trino
34
+ def self.reset_schema_cache!(model_class)
35
+ model_class.reset_column_information
36
+ return unless model_class.connection.respond_to?(:schema_cache)
37
+
38
+ model_class.connection.schema_cache.clear!
39
+ end
40
+ end
41
+ end
metadata ADDED
@@ -0,0 +1,216 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-trino-adapter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Garett Arrowood
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: activesupport
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '7.1'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '8.1'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '7.1'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '8.1'
52
+ - !ruby/object:Gem::Dependency
53
+ name: trino-client
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - '='
57
+ - !ruby/object:Gem::Version
58
+ version: 2.2.4
59
+ type: :runtime
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - '='
64
+ - !ruby/object:Gem::Version
65
+ version: 2.2.4
66
+ - !ruby/object:Gem::Dependency
67
+ name: appraisal
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - '='
71
+ - !ruby/object:Gem::Version
72
+ version: 2.5.0
73
+ type: :development
74
+ prerelease: false
75
+ version_requirements: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - '='
78
+ - !ruby/object:Gem::Version
79
+ version: 2.5.0
80
+ - !ruby/object:Gem::Dependency
81
+ name: license_finder
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - '='
85
+ - !ruby/object:Gem::Version
86
+ version: 7.2.1
87
+ type: :development
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - '='
92
+ - !ruby/object:Gem::Version
93
+ version: 7.2.1
94
+ - !ruby/object:Gem::Dependency
95
+ name: pry-byebug
96
+ requirement: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - '='
99
+ - !ruby/object:Gem::Version
100
+ version: 3.10.1
101
+ type: :development
102
+ prerelease: false
103
+ version_requirements: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - '='
106
+ - !ruby/object:Gem::Version
107
+ version: 3.10.1
108
+ - !ruby/object:Gem::Dependency
109
+ name: rspec
110
+ requirement: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - '='
113
+ - !ruby/object:Gem::Version
114
+ version: 3.13.2
115
+ type: :development
116
+ prerelease: false
117
+ version_requirements: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - '='
120
+ - !ruby/object:Gem::Version
121
+ version: 3.13.2
122
+ - !ruby/object:Gem::Dependency
123
+ name: rubocop
124
+ requirement: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - '='
127
+ - !ruby/object:Gem::Version
128
+ version: 1.82.1
129
+ type: :development
130
+ prerelease: false
131
+ version_requirements: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - '='
134
+ - !ruby/object:Gem::Version
135
+ version: 1.82.1
136
+ - !ruby/object:Gem::Dependency
137
+ name: rubocop-powerhome
138
+ requirement: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ type: :development
144
+ prerelease: false
145
+ version_requirements: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ - !ruby/object:Gem::Dependency
151
+ name: webmock
152
+ requirement: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - '='
155
+ - !ruby/object:Gem::Version
156
+ version: 3.26.2
157
+ type: :development
158
+ prerelease: false
159
+ version_requirements: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - '='
162
+ - !ruby/object:Gem::Version
163
+ version: 3.26.2
164
+ description: A read-only ActiveRecord SQL adapter for Trino, built on top of the trino-client
165
+ gem. Lets Rails applications query a Trino data warehouse using familiar ActiveRecord
166
+ scopes, where clauses, and joins, while preventing accidental writes.
167
+ email:
168
+ - garettarrowood@gmail.com
169
+ executables: []
170
+ extensions: []
171
+ extra_rdoc_files: []
172
+ files:
173
+ - LICENSE.txt
174
+ - README.md
175
+ - lib/active_record/connection_adapters/trino/column.rb
176
+ - lib/active_record/connection_adapters/trino/database_statements.rb
177
+ - lib/active_record/connection_adapters/trino/quoting.rb
178
+ - lib/active_record/connection_adapters/trino/read_only.rb
179
+ - lib/active_record/connection_adapters/trino/safety_belts.rb
180
+ - lib/active_record/connection_adapters/trino/schema_statements.rb
181
+ - lib/active_record/connection_adapters/trino/type_map.rb
182
+ - lib/active_record/connection_adapters/trino_adapter.rb
183
+ - lib/active_record/trino.rb
184
+ - lib/active_record/trino/config.rb
185
+ - lib/active_record/trino/diagnostics.rb
186
+ - lib/active_record/trino/errors.rb
187
+ - lib/active_record/trino/type/json.rb
188
+ - lib/active_record/trino/type/timestamp_with_zone.rb
189
+ - lib/active_record/trino/type/unsupported.rb
190
+ - lib/active_record/trino/version.rb
191
+ homepage: https://github.com/powerhome/activerecord-trino-adapter
192
+ licenses:
193
+ - MIT
194
+ metadata:
195
+ homepage_uri: https://github.com/powerhome/activerecord-trino-adapter
196
+ source_code_uri: https://github.com/powerhome/activerecord-trino-adapter
197
+ changelog_uri: https://github.com/powerhome/activerecord-trino-adapter/blob/main/CHANGELOG.md
198
+ rubygems_mfa_required: 'true'
199
+ rdoc_options: []
200
+ require_paths:
201
+ - lib
202
+ required_ruby_version: !ruby/object:Gem::Requirement
203
+ requirements:
204
+ - - ">="
205
+ - !ruby/object:Gem::Version
206
+ version: '3.2'
207
+ required_rubygems_version: !ruby/object:Gem::Requirement
208
+ requirements:
209
+ - - ">="
210
+ - !ruby/object:Gem::Version
211
+ version: '0'
212
+ requirements: []
213
+ rubygems_version: 4.0.3
214
+ specification_version: 4
215
+ summary: Read-only ActiveRecord adapter for Trino
216
+ test_files: []