rooq 1.0.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 +7 -0
- data/.tool-versions +1 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +33 -0
- data/CLAUDE.md +54 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +116 -0
- data/LICENSE +661 -0
- data/README.md +98 -0
- data/Rakefile +130 -0
- data/USAGE.md +850 -0
- data/exe/rooq +7 -0
- data/lib/rooq/adapters/postgresql.rb +117 -0
- data/lib/rooq/adapters.rb +3 -0
- data/lib/rooq/cli.rb +230 -0
- data/lib/rooq/condition.rb +104 -0
- data/lib/rooq/configuration.rb +56 -0
- data/lib/rooq/connection.rb +131 -0
- data/lib/rooq/context.rb +141 -0
- data/lib/rooq/dialect/base.rb +27 -0
- data/lib/rooq/dialect/postgresql.rb +531 -0
- data/lib/rooq/dialect.rb +9 -0
- data/lib/rooq/dsl/delete_query.rb +37 -0
- data/lib/rooq/dsl/insert_query.rb +43 -0
- data/lib/rooq/dsl/select_query.rb +301 -0
- data/lib/rooq/dsl/update_query.rb +44 -0
- data/lib/rooq/dsl.rb +28 -0
- data/lib/rooq/executor.rb +65 -0
- data/lib/rooq/expression.rb +494 -0
- data/lib/rooq/field.rb +71 -0
- data/lib/rooq/generator/code_generator.rb +91 -0
- data/lib/rooq/generator/introspector.rb +265 -0
- data/lib/rooq/generator.rb +9 -0
- data/lib/rooq/parameter_converter.rb +98 -0
- data/lib/rooq/query_validator.rb +176 -0
- data/lib/rooq/result.rb +248 -0
- data/lib/rooq/schema_validator.rb +56 -0
- data/lib/rooq/table.rb +69 -0
- data/lib/rooq/version.rb +5 -0
- data/lib/rooq.rb +25 -0
- data/rooq.gemspec +35 -0
- data/sorbet/config +4 -0
- metadata +115 -0
data/exe/rooq
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rooq
|
|
4
|
+
module Adapters
|
|
5
|
+
module PostgreSQL
|
|
6
|
+
# A simple connection pool for PostgreSQL connections.
|
|
7
|
+
# For production use, consider using a more robust pool like connection_pool gem.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# pool = Rooq::Adapters::PostgreSQL::ConnectionPool.new(size: 5) do
|
|
11
|
+
# PG.connect(dbname: 'myapp', host: 'localhost')
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# ctx = Rooq::Context.using_pool(pool)
|
|
15
|
+
# # ... execute queries ...
|
|
16
|
+
# pool.shutdown
|
|
17
|
+
#
|
|
18
|
+
# @example With timeout
|
|
19
|
+
# pool = Rooq::Adapters::PostgreSQL::ConnectionPool.new(size: 10, timeout: 5) do
|
|
20
|
+
# PG.connect(connection_string)
|
|
21
|
+
# end
|
|
22
|
+
class ConnectionPool < Rooq::ConnectionPool
|
|
23
|
+
class TimeoutError < Rooq::Error; end
|
|
24
|
+
|
|
25
|
+
attr_reader :size
|
|
26
|
+
|
|
27
|
+
# Create a new connection pool.
|
|
28
|
+
# @param size [Integer] the maximum number of connections
|
|
29
|
+
# @param timeout [Numeric] seconds to wait for a connection (nil = wait forever)
|
|
30
|
+
# @yield the block that creates a new connection
|
|
31
|
+
def initialize(size: 5, timeout: nil, &block)
|
|
32
|
+
super()
|
|
33
|
+
@size = size
|
|
34
|
+
@timeout = timeout
|
|
35
|
+
@create_connection = block
|
|
36
|
+
@mutex = Mutex.new
|
|
37
|
+
@condition = ConditionVariable.new
|
|
38
|
+
@available = []
|
|
39
|
+
@in_use = []
|
|
40
|
+
@shutdown = false
|
|
41
|
+
|
|
42
|
+
# Pre-create all connections
|
|
43
|
+
@size.times { @available << @create_connection.call }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Check out a connection from the pool.
|
|
47
|
+
# @return [PG::Connection] a database connection
|
|
48
|
+
# @raise [TimeoutError] if timeout expires while waiting
|
|
49
|
+
def checkout
|
|
50
|
+
@mutex.synchronize do
|
|
51
|
+
raise Error, "Pool has been shut down" if @shutdown
|
|
52
|
+
|
|
53
|
+
deadline = @timeout ? Time.now + @timeout : nil
|
|
54
|
+
|
|
55
|
+
while @available.empty?
|
|
56
|
+
if deadline
|
|
57
|
+
remaining = deadline - Time.now
|
|
58
|
+
raise TimeoutError, "Timed out waiting for connection" if remaining <= 0
|
|
59
|
+
|
|
60
|
+
@condition.wait(@mutex, remaining)
|
|
61
|
+
else
|
|
62
|
+
@condition.wait(@mutex)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
raise Error, "Pool has been shut down" if @shutdown
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
connection = @available.pop
|
|
69
|
+
@in_use << connection
|
|
70
|
+
connection
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check in a connection back to the pool.
|
|
75
|
+
# @param connection [PG::Connection] the connection to return
|
|
76
|
+
def checkin(connection)
|
|
77
|
+
@mutex.synchronize do
|
|
78
|
+
@in_use.delete(connection)
|
|
79
|
+
@available << connection unless @shutdown
|
|
80
|
+
@condition.signal
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @return [Integer] number of available connections
|
|
85
|
+
def available
|
|
86
|
+
@mutex.synchronize { @available.length }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Shutdown the pool and close all connections.
|
|
90
|
+
def shutdown
|
|
91
|
+
@mutex.synchronize do
|
|
92
|
+
@shutdown = true
|
|
93
|
+
@available.each { |conn| conn.close if conn.respond_to?(:close) }
|
|
94
|
+
@in_use.each { |conn| conn.close if conn.respond_to?(:close) }
|
|
95
|
+
@available.clear
|
|
96
|
+
@in_use.clear
|
|
97
|
+
@condition.broadcast
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Create a Context configured for PostgreSQL.
|
|
103
|
+
# @param connection [PG::Connection] a single connection
|
|
104
|
+
# @return [Rooq::Context]
|
|
105
|
+
def self.context(connection)
|
|
106
|
+
Rooq::Context.using(connection, dialect: Rooq::Dialect::PostgreSQL.new)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Create a Context with a connection pool.
|
|
110
|
+
# @param pool [ConnectionPool] a connection pool
|
|
111
|
+
# @return [Rooq::Context]
|
|
112
|
+
def self.pooled_context(pool)
|
|
113
|
+
Rooq::Context.using_pool(pool, dialect: Rooq::Dialect::PostgreSQL.new)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
data/lib/rooq/cli.rb
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "optparse"
|
|
6
|
+
require "sorbet-runtime"
|
|
7
|
+
|
|
8
|
+
module Rooq
|
|
9
|
+
class CLI
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { params(args: T::Array[String]).void }
|
|
13
|
+
def initialize(args)
|
|
14
|
+
@args = args
|
|
15
|
+
@options = T.let({
|
|
16
|
+
schema: "public",
|
|
17
|
+
output: nil,
|
|
18
|
+
stdout: false,
|
|
19
|
+
typed: true,
|
|
20
|
+
namespace: "Schema",
|
|
21
|
+
database: nil,
|
|
22
|
+
host: "localhost",
|
|
23
|
+
port: 5432,
|
|
24
|
+
username: nil,
|
|
25
|
+
password: nil
|
|
26
|
+
}, T::Hash[Symbol, T.untyped])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
sig { returns(Integer) }
|
|
30
|
+
def run
|
|
31
|
+
parse_options!
|
|
32
|
+
|
|
33
|
+
case @args.first
|
|
34
|
+
when "generate", "gen", "g"
|
|
35
|
+
generate_command
|
|
36
|
+
when "version", "-v", "--version"
|
|
37
|
+
version_command
|
|
38
|
+
when "help", "-h", "--help", nil
|
|
39
|
+
help_command
|
|
40
|
+
else
|
|
41
|
+
$stderr.puts "Unknown command: #{@args.first}"
|
|
42
|
+
$stderr.puts "Run 'rooq help' for usage information."
|
|
43
|
+
1
|
|
44
|
+
end
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
$stderr.puts "Error: #{e.message}"
|
|
47
|
+
$stderr.puts e.backtrace.first(5).join("\n") if ENV["DEBUG"]
|
|
48
|
+
1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
sig { void }
|
|
54
|
+
def parse_options!
|
|
55
|
+
parser = OptionParser.new do |opts|
|
|
56
|
+
opts.banner = "Usage: rooq <command> [options]"
|
|
57
|
+
|
|
58
|
+
opts.on("-d", "--database DATABASE", "Database name (required)") do |v|
|
|
59
|
+
@options[:database] = v
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
opts.on("-h", "--host HOST", "Database host (default: localhost)") do |v|
|
|
63
|
+
@options[:host] = v
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
opts.on("-p", "--port PORT", Integer, "Database port (default: 5432)") do |v|
|
|
67
|
+
@options[:port] = v
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
opts.on("-U", "--username USERNAME", "Database username") do |v|
|
|
71
|
+
@options[:username] = v
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
opts.on("-W", "--password PASSWORD", "Database password") do |v|
|
|
75
|
+
@options[:password] = v
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
opts.on("-s", "--schema SCHEMA", "Schema name (default: public)") do |v|
|
|
79
|
+
@options[:schema] = v
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
opts.on("-o", "--output FILE", "Output file (default: lib/schema.rb)") do |v|
|
|
83
|
+
@options[:output] = v
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
opts.on("-n", "--namespace NAMESPACE", "Module namespace (default: Schema)") do |v|
|
|
87
|
+
@options[:namespace] = v
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
opts.on("--stdout", "Print to stdout instead of file") do
|
|
91
|
+
@options[:stdout] = true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
opts.on("--[no-]typed", "Generate Sorbet types (default: true)") do |v|
|
|
95
|
+
@options[:typed] = v
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
opts.on("--help", "Show this help message") do
|
|
99
|
+
puts opts
|
|
100
|
+
exit 0
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
parser.parse!(@args)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
sig { returns(Integer) }
|
|
108
|
+
def generate_command
|
|
109
|
+
@args.shift # Remove the "generate" command
|
|
110
|
+
|
|
111
|
+
unless @options[:database]
|
|
112
|
+
$stderr.puts "Error: Database name is required (-d DATABASE)"
|
|
113
|
+
$stderr.puts "Run 'rooq help' for usage information."
|
|
114
|
+
return 1
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
require "pg"
|
|
118
|
+
|
|
119
|
+
connection = connect_to_database
|
|
120
|
+
introspector = Generator::Introspector.new(connection)
|
|
121
|
+
schema_info = introspector.introspect_schema(schema: @options[:schema])
|
|
122
|
+
|
|
123
|
+
generator = Generator::CodeGenerator.new(
|
|
124
|
+
schema_info,
|
|
125
|
+
typed: @options[:typed],
|
|
126
|
+
namespace: @options[:namespace]
|
|
127
|
+
)
|
|
128
|
+
code = generator.generate
|
|
129
|
+
|
|
130
|
+
if @options[:stdout]
|
|
131
|
+
puts code
|
|
132
|
+
else
|
|
133
|
+
output_file = @options[:output] || default_output_file
|
|
134
|
+
FileUtils.mkdir_p(File.dirname(output_file))
|
|
135
|
+
File.write(output_file, code)
|
|
136
|
+
puts "Generated #{output_file}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
connection.close
|
|
140
|
+
0
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
sig { returns(String) }
|
|
144
|
+
def default_output_file
|
|
145
|
+
# Convert namespace to filename: MyApp::Schema -> my_app/schema.rb
|
|
146
|
+
namespace = @options[:namespace].to_s
|
|
147
|
+
filename = namespace.gsub("::", "/").gsub(/([a-z])([A-Z])/, '\1_\2').downcase
|
|
148
|
+
"lib/#{filename}.rb"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
sig { returns(T.untyped) }
|
|
152
|
+
def connect_to_database
|
|
153
|
+
connection_params = {
|
|
154
|
+
dbname: @options[:database],
|
|
155
|
+
host: @options[:host],
|
|
156
|
+
port: @options[:port]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
connection_params[:user] = @options[:username] if @options[:username]
|
|
160
|
+
connection_params[:password] = @options[:password] if @options[:password]
|
|
161
|
+
|
|
162
|
+
# Also check environment variables
|
|
163
|
+
connection_params[:user] ||= ENV["PGUSER"]
|
|
164
|
+
connection_params[:password] ||= ENV["PGPASSWORD"]
|
|
165
|
+
connection_params[:host] = ENV["PGHOST"] if ENV["PGHOST"]
|
|
166
|
+
connection_params[:port] = ENV["PGPORT"].to_i if ENV["PGPORT"]
|
|
167
|
+
|
|
168
|
+
PG.connect(connection_params)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
sig { returns(Integer) }
|
|
172
|
+
def version_command
|
|
173
|
+
puts "rooq #{Rooq::VERSION}"
|
|
174
|
+
0
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
sig { returns(Integer) }
|
|
178
|
+
def help_command
|
|
179
|
+
puts <<~HELP
|
|
180
|
+
rOOQ - A jOOQ-inspired query builder for Ruby
|
|
181
|
+
|
|
182
|
+
Usage: rooq <command> [options]
|
|
183
|
+
|
|
184
|
+
Commands:
|
|
185
|
+
generate, gen, g Generate Ruby table definitions from database schema
|
|
186
|
+
version Show version
|
|
187
|
+
help Show this help message
|
|
188
|
+
|
|
189
|
+
Options for 'generate':
|
|
190
|
+
-d, --database DATABASE Database name (required)
|
|
191
|
+
-h, --host HOST Database host (default: localhost)
|
|
192
|
+
-p, --port PORT Database port (default: 5432)
|
|
193
|
+
-U, --username USERNAME Database username
|
|
194
|
+
-W, --password PASSWORD Database password
|
|
195
|
+
-s, --schema SCHEMA Database schema (default: public)
|
|
196
|
+
-o, --output FILE Output file (default: lib/schema.rb)
|
|
197
|
+
-n, --namespace NAMESPACE Module namespace (default: Schema)
|
|
198
|
+
--stdout Print to stdout instead of file
|
|
199
|
+
--[no-]typed Generate Sorbet types (default: true)
|
|
200
|
+
|
|
201
|
+
Environment Variables:
|
|
202
|
+
PGHOST Default database host
|
|
203
|
+
PGPORT Default database port
|
|
204
|
+
PGUSER Default database username
|
|
205
|
+
PGPASSWORD Default database password
|
|
206
|
+
|
|
207
|
+
Examples:
|
|
208
|
+
# Generate schema to lib/schema.rb
|
|
209
|
+
rooq generate -d myapp_development
|
|
210
|
+
|
|
211
|
+
# Generate with custom namespace (writes to lib/my_app/db.rb)
|
|
212
|
+
rooq generate -d myapp_development -n MyApp::DB
|
|
213
|
+
|
|
214
|
+
# Generate to custom file
|
|
215
|
+
rooq generate -d myapp_development -o db/schema.rb
|
|
216
|
+
|
|
217
|
+
# Generate without Sorbet types
|
|
218
|
+
rooq generate -d myapp_development --no-typed
|
|
219
|
+
|
|
220
|
+
# Print to stdout instead of file
|
|
221
|
+
rooq generate -d myapp_development --stdout
|
|
222
|
+
|
|
223
|
+
# Connect to remote database
|
|
224
|
+
rooq generate -d myapp -h db.example.com -U postgres -W secret
|
|
225
|
+
|
|
226
|
+
HELP
|
|
227
|
+
0
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
module Rooq
|
|
7
|
+
# Type alias for any condition type
|
|
8
|
+
AnyCondition = T.type_alias { T.any(Condition, CombinedCondition, ExistsCondition) }
|
|
9
|
+
|
|
10
|
+
class Condition
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { returns(Expression) }
|
|
14
|
+
attr_reader :expression
|
|
15
|
+
|
|
16
|
+
sig { returns(Symbol) }
|
|
17
|
+
attr_reader :operator
|
|
18
|
+
|
|
19
|
+
sig { returns(T.untyped) }
|
|
20
|
+
attr_reader :value
|
|
21
|
+
|
|
22
|
+
sig { params(expression: Expression, operator: Symbol, value: T.untyped).void }
|
|
23
|
+
def initialize(expression, operator, value)
|
|
24
|
+
@expression = expression
|
|
25
|
+
@operator = operator
|
|
26
|
+
@value = value
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Backwards compatibility
|
|
31
|
+
sig { returns(Expression) }
|
|
32
|
+
def field
|
|
33
|
+
@expression
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { params(other: AnyCondition).returns(CombinedCondition) }
|
|
37
|
+
def and(other)
|
|
38
|
+
CombinedCondition.new(:and, [self, other])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { params(other: AnyCondition).returns(CombinedCondition) }
|
|
42
|
+
def or(other)
|
|
43
|
+
CombinedCondition.new(:or, [self, other])
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class CombinedCondition
|
|
48
|
+
extend T::Sig
|
|
49
|
+
|
|
50
|
+
sig { returns(Symbol) }
|
|
51
|
+
attr_reader :operator
|
|
52
|
+
|
|
53
|
+
sig { returns(T::Array[AnyCondition]) }
|
|
54
|
+
attr_reader :conditions
|
|
55
|
+
|
|
56
|
+
sig { params(operator: Symbol, conditions: T::Array[AnyCondition]).void }
|
|
57
|
+
def initialize(operator, conditions)
|
|
58
|
+
@operator = operator
|
|
59
|
+
@conditions = T.let(conditions.freeze, T::Array[AnyCondition])
|
|
60
|
+
freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { params(other: AnyCondition).returns(CombinedCondition) }
|
|
64
|
+
def and(other)
|
|
65
|
+
CombinedCondition.new(:and, [*@conditions, other])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
sig { params(other: AnyCondition).returns(CombinedCondition) }
|
|
69
|
+
def or(other)
|
|
70
|
+
CombinedCondition.new(:or, [*@conditions, other])
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# EXISTS condition
|
|
75
|
+
class ExistsCondition
|
|
76
|
+
extend T::Sig
|
|
77
|
+
|
|
78
|
+
sig { returns(DSL::SelectQuery) }
|
|
79
|
+
attr_reader :subquery
|
|
80
|
+
|
|
81
|
+
sig { returns(T::Boolean) }
|
|
82
|
+
attr_reader :negated
|
|
83
|
+
|
|
84
|
+
sig { params(subquery: DSL::SelectQuery, negated: T::Boolean).void }
|
|
85
|
+
def initialize(subquery, negated: false)
|
|
86
|
+
@subquery = subquery
|
|
87
|
+
@negated = negated
|
|
88
|
+
freeze
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
extend T::Sig
|
|
93
|
+
|
|
94
|
+
# Helper methods for conditions
|
|
95
|
+
sig { params(subquery: DSL::SelectQuery).returns(ExistsCondition) }
|
|
96
|
+
def self.exists(subquery)
|
|
97
|
+
ExistsCondition.new(subquery)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
sig { params(subquery: DSL::SelectQuery).returns(ExistsCondition) }
|
|
101
|
+
def self.not_exists(subquery)
|
|
102
|
+
ExistsCondition.new(subquery, negated: true)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rooq
|
|
4
|
+
# Configuration holds all settings for a Rooq context.
|
|
5
|
+
# Configurations are immutable - use #derive to create modified copies.
|
|
6
|
+
#
|
|
7
|
+
# Inspired by jOOQ's Configuration class.
|
|
8
|
+
# @see https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/Configuration.html
|
|
9
|
+
class Configuration
|
|
10
|
+
attr_reader :connection_provider, :dialect
|
|
11
|
+
|
|
12
|
+
# Create a new configuration.
|
|
13
|
+
# @param connection_provider [ConnectionProvider] the connection provider
|
|
14
|
+
# @param dialect [Dialect::Base] the SQL dialect
|
|
15
|
+
def initialize(connection_provider: nil, dialect: nil)
|
|
16
|
+
@connection_provider = connection_provider
|
|
17
|
+
@dialect = dialect || Dialect::PostgreSQL.new
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Create a new configuration from a single connection.
|
|
22
|
+
# The connection lifecycle is managed externally.
|
|
23
|
+
# @param connection [Object] a database connection
|
|
24
|
+
# @param dialect [Dialect::Base] the SQL dialect
|
|
25
|
+
# @return [Configuration]
|
|
26
|
+
def self.from_connection(connection, dialect: nil)
|
|
27
|
+
new(
|
|
28
|
+
connection_provider: DefaultConnectionProvider.new(connection),
|
|
29
|
+
dialect: dialect
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Create a new configuration from a connection pool.
|
|
34
|
+
# Connections are acquired and released per query.
|
|
35
|
+
# @param pool [ConnectionPool] a connection pool
|
|
36
|
+
# @param dialect [Dialect::Base] the SQL dialect
|
|
37
|
+
# @return [Configuration]
|
|
38
|
+
def self.from_pool(pool, dialect: nil)
|
|
39
|
+
new(
|
|
40
|
+
connection_provider: PooledConnectionProvider.new(pool),
|
|
41
|
+
dialect: dialect
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Create a derived configuration with some settings overridden.
|
|
46
|
+
# @param connection_provider [ConnectionProvider] new connection provider (optional)
|
|
47
|
+
# @param dialect [Dialect::Base] new dialect (optional)
|
|
48
|
+
# @return [Configuration] a new configuration
|
|
49
|
+
def derive(connection_provider: nil, dialect: nil)
|
|
50
|
+
Configuration.new(
|
|
51
|
+
connection_provider: connection_provider || @connection_provider,
|
|
52
|
+
dialect: dialect || @dialect
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rooq
|
|
4
|
+
# Abstract interface for connection lifecycle management.
|
|
5
|
+
# Implementations control how connections are acquired and released.
|
|
6
|
+
#
|
|
7
|
+
# This is inspired by jOOQ's ConnectionProvider interface.
|
|
8
|
+
# @see https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/ConnectionProvider.html
|
|
9
|
+
class ConnectionProvider
|
|
10
|
+
# Acquire a connection for query execution.
|
|
11
|
+
# @return [Object] a database connection
|
|
12
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
13
|
+
def acquire
|
|
14
|
+
raise NotImplementedError, "#{self.class} must implement #acquire"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Release a previously acquired connection.
|
|
18
|
+
# @param connection [Object] the connection to release
|
|
19
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
20
|
+
def release(connection)
|
|
21
|
+
raise NotImplementedError, "#{self.class} must implement #release"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Execute a block with an acquired connection, ensuring release.
|
|
25
|
+
# @yield [connection] the acquired connection
|
|
26
|
+
# @return [Object] the result of the block
|
|
27
|
+
def with_connection
|
|
28
|
+
connection = acquire
|
|
29
|
+
begin
|
|
30
|
+
yield connection
|
|
31
|
+
ensure
|
|
32
|
+
release(connection)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# A connection provider that wraps a single connection.
|
|
38
|
+
# The connection lifecycle is managed externally by the caller.
|
|
39
|
+
# Release is a no-op - the connection stays open until closed externally.
|
|
40
|
+
#
|
|
41
|
+
# Use this when you want to control transactions and connection lifecycle yourself.
|
|
42
|
+
class DefaultConnectionProvider < ConnectionProvider
|
|
43
|
+
attr_reader :connection
|
|
44
|
+
|
|
45
|
+
# @param connection [Object] the connection to wrap
|
|
46
|
+
def initialize(connection)
|
|
47
|
+
super()
|
|
48
|
+
@connection = connection
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Always returns the same connection instance.
|
|
52
|
+
# @return [Object] the wrapped connection
|
|
53
|
+
def acquire
|
|
54
|
+
@connection
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# No-op - the connection lifecycle is managed externally.
|
|
58
|
+
# @param connection [Object] the connection (ignored)
|
|
59
|
+
def release(connection)
|
|
60
|
+
# No-op: connection lifecycle is managed externally
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# A connection provider that acquires connections from a pool.
|
|
65
|
+
# Connections are returned to the pool after each query.
|
|
66
|
+
#
|
|
67
|
+
# The pool must respond to #checkout (or #acquire) and optionally #checkin.
|
|
68
|
+
# If the pool doesn't respond to #checkin, the connection's #close method is called.
|
|
69
|
+
class PooledConnectionProvider < ConnectionProvider
|
|
70
|
+
attr_reader :pool
|
|
71
|
+
|
|
72
|
+
# @param pool [ConnectionPool] a connection pool
|
|
73
|
+
def initialize(pool)
|
|
74
|
+
super()
|
|
75
|
+
@pool = pool
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Acquire a connection from the pool.
|
|
79
|
+
# @return [Object] a database connection
|
|
80
|
+
def acquire
|
|
81
|
+
if @pool.respond_to?(:checkout)
|
|
82
|
+
@pool.checkout
|
|
83
|
+
elsif @pool.respond_to?(:acquire)
|
|
84
|
+
@pool.acquire
|
|
85
|
+
else
|
|
86
|
+
raise Error, "Pool must respond to #checkout or #acquire"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Release a connection back to the pool.
|
|
91
|
+
# @param connection [Object] the connection to release
|
|
92
|
+
def release(connection)
|
|
93
|
+
if @pool.respond_to?(:checkin)
|
|
94
|
+
@pool.checkin(connection)
|
|
95
|
+
elsif connection.respond_to?(:close)
|
|
96
|
+
connection.close
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Abstract interface for connection pools.
|
|
102
|
+
# Implementations manage a pool of reusable database connections.
|
|
103
|
+
class ConnectionPool
|
|
104
|
+
# Check out a connection from the pool.
|
|
105
|
+
# @return [Object] a database connection
|
|
106
|
+
def checkout
|
|
107
|
+
raise NotImplementedError, "#{self.class} must implement #checkout"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Check in a connection back to the pool.
|
|
111
|
+
# @param connection [Object] the connection to return
|
|
112
|
+
def checkin(connection)
|
|
113
|
+
raise NotImplementedError, "#{self.class} must implement #checkin"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @return [Integer] total pool size
|
|
117
|
+
def size
|
|
118
|
+
raise NotImplementedError, "#{self.class} must implement #size"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @return [Integer] number of available connections
|
|
122
|
+
def available
|
|
123
|
+
raise NotImplementedError, "#{self.class} must implement #available"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Shutdown the pool and close all connections.
|
|
127
|
+
def shutdown
|
|
128
|
+
raise NotImplementedError, "#{self.class} must implement #shutdown"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|