db 0.11.0 → 0.13.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: '00818022e4f4c8f40dae41f5f7850b89021cbcb8165f2a1e4637d07520a2c748'
4
- data.tar.gz: 5d4ce254968d4f5187469537f2c1553970ba32e9125bea3a43de40ceb8d3bc40
3
+ metadata.gz: 64a6f849cc021f19902e7aa339156fd839b6ee03f8bb481c66a86524cdd447d8
4
+ data.tar.gz: c4bd22be9d7cc9b9862682f0c10856cac5723f684daf426ecf60737124faa7cc
5
5
  SHA512:
6
- metadata.gz: cb6f3c928363fbc5c9a12ebf6bc902636e93202ee3308edf28d4370f40c2c69c5b57d9c64f873e66642bba3afa025bcd1934366011a52040c9f882b931fafc40
7
- data.tar.gz: 0d0b154d9817cbc191c9a16203e5088538fe37375af00e060be6ae4c29379c0e03d61933420b15a36ab4efcbe003e1c279b38f0242b25b6afe156a3e10103980
6
+ metadata.gz: 1bfa44a0456cca0c87a6f40349a7b7da597bdf53e1169aa0490532f98a90fad3179d562a1c3d5f40750888659ec705d4007984eebf9f1aa97be242918d4c66f9
7
+ data.tar.gz: 130a8e292f7f584da81fb8700071d35c0cda6cac8392ebe45e0b53f0527c5f26ab527db4aac3d01d9b051772d605175f27e32625a5d12a431fde19729d8bb8cd
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,28 @@
1
+ # Data Types
2
+
3
+ This guide explains about SQL data types, and how they are used by the DB gem.
4
+
5
+ Structured Query Language (SQL) defines a set of data types that can be used to store data in a database. The data types are used to define a column in a table, and each column in a table must have a data type associated with it. The data type of a column typically defines the kind of data that the column can store, althought some database systems allow you to store any kind of data in any column.
6
+
7
+ When you build a program with a database, you need to be aware of the data types that are available in the database system you are using. The DB gem tries to expose standard data types, so that you can use the same data types across different database systems. There are two main operations that are affected by datatypes: appending literal values to SQL queries, and reading values from the database.
8
+
9
+ ## Appending Literal Data Types
10
+
11
+ The DB gem converts Ruby objects to SQL literals when you append them to a query. This is generally taken care of by the {ruby DB::Query#literal} and {ruby DB::Query#interpolate} methods, which are used to append literal values to a query. Generally speaking, the following native data types are supported:
12
+
13
+ - `Time`, `DateTime` and `Date` objects convert to an appropriate format for the database system you are using. Some systems don't natively support timezones, and so time zone information may be lost.
14
+ - `String` objects are escaped and quoted.
15
+ - `Numeric` (including `Integer` and `Float`) objects are appended as-is.
16
+ - `TrueClass` and `FalseClass` objects are converted to the appropriate boolean value for the database system you are using.
17
+ - `NilClass` objects are converted to `NULL`.
18
+
19
+ ## Reading Data Types
20
+
21
+ When you read data from the database, the DB gem tries to convert the data to the appropriate Ruby object. When a query yields rows of fields, and those fields have a well defined field type, known by the adapter, the adapter will cast those objects back into rich Ruby objects where possible. The following conversions are generally supported:
22
+
23
+ - `TEXT` and `VARCHAR` fields are converted to `String` objects.
24
+ - `INTEGER` and `FLOAT` fields are converted to `Integer` and `Float` objects respectively.
25
+ - `BOOLEAN` fields are converted to `TrueClass` and `FalseClass` objects.
26
+ - `TIMESTAMP` and `DATETIME` fields are converted to `Time` objects.
27
+ - `DATE` fields are converted to `Date` objects.
28
+ - `NULL` values are converted to `nil`.
@@ -0,0 +1,263 @@
1
+ # Example Queries
2
+
3
+ This guide shows a variety of example queries using the DB gem.
4
+
5
+ ## Setup
6
+
7
+ ~~~ ruby
8
+ require 'async'
9
+ require 'db/client'
10
+ require 'db/postgres'
11
+
12
+ client = DB::Client.new(DB::Postgres::Adapter.new(
13
+ database: 'test',
14
+ host: '172.17.0.3',
15
+ password: 'test',
16
+ username: 'postgres',
17
+ ))
18
+ ~~~
19
+
20
+ ## A simple CREATE, INSERT and SELECT, with raw SQL
21
+
22
+ ~~~ ruby
23
+ Sync do
24
+ session = client.session
25
+
26
+ create = "CREATE TABLE IF NOT EXISTS my_table (a_timestamp TIMESTAMP NOT NULL)"
27
+ session.query(create).call
28
+
29
+ insert = "INSERT INTO my_table VALUES (NOW()), ('2022-12-12 12:13:14')"
30
+ session.query(insert).call
31
+
32
+ result = session.query("SELECT * FROM my_table WHERE a_timestamp > NOW()").call
33
+
34
+ Console.info result.field_types.to_s
35
+ Console.info result.field_names.to_s
36
+ Console.info result.to_a.to_s
37
+ ensure
38
+ session&.close
39
+ end
40
+ ~~~
41
+
42
+ ### Output
43
+
44
+ ~~~
45
+ 0.01s info: [#<DB::Postgres::Native::Types::DateTime:0x00007eff3b13e688 @name="TIMESTAMP">]
46
+ 0.01s info: ["a_timestamp"]
47
+ 0.01s info: [[2022-12-12 12:13:14 UTC]]
48
+ ~~~
49
+
50
+ ## Parameterized CREATE, INSERT and SELECT
51
+
52
+ The same process as before, but parameterized. Always use the parameterized form when dealing with untrusted data.
53
+
54
+ ~~~ ruby
55
+ Sync do
56
+ session = client.session
57
+
58
+ session.clause("CREATE TABLE IF NOT EXISTS")
59
+ .identifier(:my_table)
60
+ .clause("(")
61
+ .identifier(:a_timestamp).clause("TIMESTAMP NOT NULL")
62
+ .clause(")")
63
+ .call
64
+
65
+ session.clause("INSERT INTO")
66
+ .identifier(:my_table)
67
+ .clause("VALUES (")
68
+ .literal("NOW()")
69
+ .clause("), (")
70
+ .literal("2022-12-12 12:13:14")
71
+ .clause(")")
72
+ .call
73
+
74
+ result = session.clause("SELECT * FROM")
75
+ .identifier(:my_table)
76
+ .clause("WHERE")
77
+ .identifier(:a_timestamp).clause(">").literal("NOW()")
78
+ .call
79
+
80
+ Console.info result.field_types.to_s
81
+ Console.info result.field_names.to_s
82
+ Console.info result.to_a.to_s
83
+ ensure
84
+ session&.close
85
+ end
86
+ ~~~
87
+
88
+ ### Output
89
+
90
+ ~~~
91
+ 0.01s info: [#<DB::Postgres::Native::Types::DateTime:0x00007eff3b13e688 @name="TIMESTAMP">]
92
+ 0.01s info: ["a_timestamp"]
93
+ 0.01s info: [[2022-12-12 12:13:14 UTC]]
94
+ ~~~
95
+
96
+ ## A parameterized SELECT
97
+
98
+ ~~~ ruby
99
+ Sync do |task|
100
+ session = client.session
101
+ result = session
102
+ .clause("SELECT")
103
+ .identifier(:column_one)
104
+ .clause(",")
105
+ .identifier(:column_two)
106
+ .clause("FROM")
107
+ .identifier(:another_table)
108
+ .clause("WHERE")
109
+ .identifier(:id)
110
+ .clause("=")
111
+ .literal(42)
112
+ .call
113
+
114
+ Console.info "#{result.field_names}"
115
+ Console.info "#{result.to_a}"
116
+ end
117
+ ~~~
118
+
119
+ ### Output
120
+
121
+ ~~~
122
+ 0.01s info: ["column_one", "column_two"]
123
+ 0.01s info: [["foo", "bar"], ["baz", "qux"]]
124
+ ~~~
125
+
126
+ ## Concurrent queries
127
+
128
+ (Simulating slow queries with `PG_SLEEP`)
129
+
130
+ ~~~ ruby
131
+ Sync do |task|
132
+ start = Time.now
133
+ tasks = 10.times.map do
134
+ task.async do
135
+ session = client.session
136
+ result = session.query("SELECT PG_SLEEP(10)").call
137
+ result.to_a
138
+ ensure
139
+ session&.close
140
+ end
141
+ end
142
+
143
+ results = tasks.map(&:wait)
144
+
145
+ Console.info "Elapsed time: #{Time.now - start}s"
146
+ end
147
+ ~~~
148
+
149
+ ### Output
150
+
151
+ ~~~
152
+ 10.05s info: Elapsed time: 10.049756222s
153
+ ~~~
154
+
155
+ ## Limited to 3 connections
156
+
157
+ (Simulating slow queries with `PG_SLEEP`)
158
+
159
+ ~~~ ruby
160
+ require 'async/semaphore'
161
+
162
+ Sync do
163
+ semaphore = Async::Semaphore.new(3)
164
+ tasks = 10.times.map do |i|
165
+ semaphore.async do
166
+ session = client.session
167
+ Console.info "Starting task #{i}"
168
+ result = session.query("SELECT PG_SLEEP(10)").call
169
+ result.to_a
170
+ ensure
171
+ session&.close
172
+ end
173
+ end
174
+
175
+ results = tasks.map(&:wait)
176
+ Console.info "Done"
177
+ end
178
+ ~~~
179
+
180
+ ### Output
181
+
182
+ ~~~
183
+ 0.0s info: Starting task 0
184
+ 0.0s info: Starting task 1
185
+ 0.0s info: Starting task 2
186
+ 10.02s info: Completed task 0 after 10.017388464s
187
+ 10.02s info: Starting task 3
188
+ 10.02s info: Completed task 1 after 10.02111175s
189
+ 10.02s info: Starting task 4
190
+ 10.03s info: Completed task 2 after 10.027889587s
191
+ 10.03s info: Starting task 5
192
+ 20.03s info: Completed task 3 after 10.011089096s
193
+ 20.03s info: Starting task 6
194
+ 20.03s info: Completed task 4 after 10.008169111s
195
+ 20.03s info: Starting task 7
196
+ 20.04s info: Completed task 5 after 10.007644749s
197
+ 20.04s info: Starting task 8
198
+ 30.04s info: Completed task 6 after 10.011244562s
199
+ 30.04s info: Starting task 9
200
+ 30.04s info: Completed task 7 after 10.011565997s
201
+ 30.04s info: Completed task 8 after 10.004611464s
202
+ 40.05s info: Completed task 9 after 10.008239803s
203
+ 40.05s info: Done
204
+ ~~~
205
+
206
+ ## Sequential vs Concurrent INSERTs
207
+
208
+ ~~~ ruby
209
+ DATA = 1_000_000.times.map { SecureRandom.hex }
210
+
211
+ def setup_tables(client)
212
+ session = client.session
213
+
214
+ create = "CREATE TABLE IF NOT EXISTS salts (salt CHAR(32))"
215
+ session.query(create).call
216
+
217
+ truncate = "TRUNCATE TABLE salts"
218
+ session.query(truncate).call
219
+
220
+ session.close
221
+ end
222
+
223
+ def chunked_insert(rows, client, task=Async::Task.current)
224
+ task.async do
225
+ session = client.session
226
+ rows.each_slice(1000) do |slice|
227
+ insert = "INSERT INTO salts VALUES " + slice.map { |x| "('#{x}')" }.join(",")
228
+ session.query(insert).call
229
+ end
230
+ ensure
231
+ session&.close
232
+ end
233
+ end
234
+
235
+ Sync do
236
+ Console.info "Setting up tables"
237
+ setup_tables(client)
238
+ Console.info "Done"
239
+
240
+ start = Time.now
241
+ Console.info "Starting sequential insert"
242
+ chunked_insert(DATA, client).wait
243
+ Console.info "Completed sequential insert in #{Time.now - start}s"
244
+
245
+ start = Time.now
246
+ Console.info "Starting concurrent insert"
247
+ DATA.each_slice(10_000).map do |slice|
248
+ chunked_insert(slice, client)
249
+ end.each(&:wait)
250
+ Console.info "Completed concurrent insert in #{Time.now - start}s"
251
+ end
252
+ ~~~
253
+
254
+ ### Output
255
+
256
+ ~~~
257
+ 1.45s info: Setting up tables
258
+ 1.49s info: Done
259
+ 1.49s info: Starting sequential insert
260
+ 8.49s info: Completed sequential insert in 7.006533933s
261
+ 8.49s info: Starting concurrent insert
262
+ 9.92s info: Completed concurrent insert in 1.431470847s
263
+ ~~~
@@ -0,0 +1,158 @@
1
+ # Executing Queries
2
+
3
+ This guide explains how to escape and execute queries.
4
+
5
+ In order to execute a query, you need a connection. Database connections are stateful, and this state is encapsulated by a context.
6
+
7
+ ## Contexts
8
+
9
+ Contexts represent a stateful connection to a remote server. The most generic kind, {ruby DB::Context::Session} provides methods for sending queries and processing results. {ruby DB::Context::Transaction} extends this implementation and adds methods for database transactions.
10
+
11
+ ### Sessions
12
+
13
+ A {ruby DB::Context::Session} represents a connection to the database server and can be used to send queries to the server and read rows of results.
14
+
15
+ ~~~ ruby
16
+ require 'async'
17
+ require 'db/client'
18
+ require 'db/postgres'
19
+
20
+ client = DB::Client.new(DB::Postgres::Adapter.new(database: 'test'))
21
+
22
+ Sync do
23
+ session = client.session
24
+
25
+ # Build a query, injecting the literal 42 and the identifier LIFE into the statement:
26
+ result = session
27
+ .clause("SELECT").literal(42)
28
+ .clause("AS").identifier(:LIFE).call
29
+
30
+ pp result.to_a
31
+ # => [[42]]
32
+ end
33
+ ~~~
34
+
35
+ ### Transactions
36
+
37
+ Transactions ensure consistency when selecting and inserting data. While the exact semantics are server specific, transactions normally ensure that all statements execute at a consistent point in time and that if any problem occurs during the transaction, the entire transaction is aborted.
38
+
39
+ ~~~ ruby
40
+ require 'async'
41
+ require 'db/client'
42
+ require 'db/postgres'
43
+
44
+ client = DB::Client.new(DB::Postgres::Adapter.new(database: 'test'))
45
+
46
+ Sync do
47
+ session = client.transaction
48
+
49
+ # Use the explicit DSL for generating queries:
50
+ session.clause("CREATE TABLE")
51
+ .identifier(:users)
52
+ .clause("(")
53
+ .identifier(:id).clause("BIGSERIAL PRIMARY KEY,")
54
+ .identifier(:name).clause("VARCHAR NOT NULL")
55
+ .clause(")").call
56
+
57
+ # Use interpolation for generating queries:
58
+ session.query(<<~SQL, table: :users, column: :name, value: "ioquatix").call
59
+ INSERT INTO %{table} (%{column}) VALUES (%{value})
60
+ SQL
61
+
62
+ result = session.clause("SELECT * FROM").identifier(:users).call
63
+
64
+ pp result.to_a
65
+
66
+ session.abort
67
+
68
+ ensure
69
+ session.close
70
+ end
71
+ ~~~
72
+
73
+ Because the session was aborted, the table and data are never committed:
74
+
75
+ ~~~ ruby
76
+ require 'async'
77
+ require 'db/client'
78
+ require 'db/postgres'
79
+
80
+ client = DB::Client.new(DB::Postgres::Adapter.new(database: 'test'))
81
+
82
+ Sync do
83
+ session = client.session
84
+
85
+ result = session.clause("SELECT * FROM").identifier(:users).call
86
+ # => DB::Postgres::Error: Could not get next result: ERROR: relation "users" does not exist
87
+
88
+ pp result.to_a
89
+
90
+ ensure
91
+ session.close
92
+ end
93
+ ~~~
94
+
95
+ ### Closing Sessions
96
+
97
+ It is important that you close a session or commit/abort a transaction (implicit close). Closing a session returns it to the connection pool. If you don't do this, you will leak connections. Both {ruby DB::Client#session} and {ruby DB::Client#transaction} can accept blocks and will implicitly close/commit/abort as appropriate.
98
+
99
+ ## Query Builder
100
+
101
+ A {ruby DB::Query} builder is provided to help construct queries and avoid SQL injection attacks. This query builder is bound to a {ruby DB::Context::Session} instance and provides convenient methods for constructing a query efficiently.
102
+
103
+ ### Low Level Methods
104
+
105
+ There are several low level methods for constructing queries.
106
+
107
+ - {ruby DB::Query#clause} appends an unescaped fragment of SQL text.
108
+ - {ruby DB::Query#literal} appends an escaped literal value (e.g. {ruby String}, {ruby Integer}, {ruby true}, {ruby nil}, etc).
109
+ - {ruby DB::Query#identifier} appends an escaped identifier ({ruby Symbol}, {ruby Array}, {ruby DB::Identifier}).
110
+
111
+ ~~~ ruby
112
+ require 'async'
113
+ require 'db/client'
114
+ require 'db/postgres'
115
+
116
+ client = DB::Client.new(DB::Postgres::Adapter.new(database: 'test'))
117
+
118
+ Sync do
119
+ session = client.session
120
+
121
+ # Build a query, injecting the literal 42 and the identifier LIFE into the statement:
122
+ result = session
123
+ .clause("SELECT").literal(42)
124
+ .clause("AS").identifier(:LIFE)
125
+ .call
126
+
127
+ pp result.to_a
128
+ # => [[42]]
129
+ end
130
+ ~~~
131
+
132
+ ### Interpolation Method
133
+
134
+ You can also use string interpolation to safely construct queries.
135
+
136
+ - {ruby DB::Query#interpolate} appends an interpolated query string with escaped parameters.
137
+
138
+ ~~~ ruby
139
+ require 'async'
140
+ require 'db/client'
141
+ require 'db/postgres'
142
+
143
+ client = DB::Client.new(DB::Postgres::Adapter.new(database: 'test'))
144
+
145
+ Sync do
146
+ session = client.session
147
+
148
+ # Build a query, injecting the literal 42 and the identifier LIFE into the statement:
149
+ result = session.query(<<~SQL, value: 42, column: :LIFE).call
150
+ SELECT %{value} AS %{column}
151
+ SQL
152
+
153
+ pp result.to_a
154
+ # => [[42]]
155
+ end
156
+ ~~~
157
+
158
+ Named parameters are escaped and substituted into the given fragment.
@@ -0,0 +1,88 @@
1
+ # Getting Started
2
+
3
+ This guide explains how to use `db` for database queries.
4
+
5
+ ## Installation
6
+
7
+ Add the gem to your project:
8
+
9
+ ~~~ bash
10
+ $ bundle add db
11
+ ~~~
12
+
13
+ ## Core Concepts
14
+
15
+ `db` has several core concepts:
16
+
17
+ - A {ruby DB::Client} instance which is configured to connect to a specific database using an adapter, and manages a connection pool.
18
+ - A {ruby DB::Context::Session} instance which is bound to a specific connection and allows you to execute queries and enumerate results.
19
+
20
+ ## Connecting to Postgres
21
+
22
+ Add the Postgres adaptor to your project:
23
+
24
+ ~~~ bash
25
+ $ bundle add db-postgres
26
+ ~~~
27
+
28
+ Set up the client with the appropriate credentials:
29
+
30
+ ~~~ ruby
31
+ require 'async'
32
+ require 'db/client'
33
+ require 'db/postgres'
34
+
35
+ # Create the client and connection pool:
36
+ client = DB::Client.new(DB::Postgres::Adapter.new(database: 'test'))
37
+
38
+ # Create an event loop:
39
+ Sync do
40
+ # Connect to the database:
41
+ session = client.session
42
+
43
+ # Execute the query and get a result set:
44
+ result = session.call("SELECT VERSION()")
45
+
46
+ # Convert the result set to an array and print it out:
47
+ pp result.to_a
48
+ # => [["PostgreSQL 16.3 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 14.1.1 20240522, 64-bit"]]
49
+ ensure
50
+ # Return the connection to the client connection pool:
51
+ session.close
52
+ end
53
+ ~~~
54
+
55
+ ## Connection to MariaDB/MySQL
56
+
57
+ Add the MariaDB adaptor to your project:
58
+
59
+ ~~~ bash
60
+ $ bundle add db-mariadb
61
+ ~~~
62
+
63
+ Set up the client with the appropriate credentials:
64
+
65
+ ~~~ ruby
66
+ require 'async'
67
+ require 'db/client'
68
+ require 'db/mariadb'
69
+
70
+ # Create the client and connection pool:
71
+ client = DB::Client.new(DB::MariaDB::Adapter.new(database: 'test'))
72
+
73
+ # Create an event loop:
74
+ Sync do
75
+ # Connect to the database:
76
+ session = client.session
77
+
78
+ # Execute the query and get a result set:
79
+ result = session.call("SELECT VERSION()")
80
+
81
+ # Convert the result set to an array and print it out:
82
+ pp result.to_a
83
+ # => [["10.4.13-MariaDB"]]
84
+ ensure
85
+ # Return the connection to the client connection pool:
86
+ session.close
87
+ end
88
+ ~~~
@@ -0,0 +1,22 @@
1
+ # Automatically generated context index for Utopia::Project guides.
2
+ # Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
3
+ ---
4
+ description: A low level database access gem.
5
+ metadata:
6
+ documentation_uri: https://socketry.github.io/db/
7
+ funding_uri: https://github.com/sponsors/ioquatix
8
+ source_code_uri: https://github.com/socketry/db.git
9
+ files:
10
+ - path: getting-started.md
11
+ title: Getting Started
12
+ description: This guide explains how to use `db` for database queries.
13
+ - path: executing-queries.md
14
+ title: Executing Queries
15
+ description: This guide explains how to escape and execute queries.
16
+ - path: example-queries.md
17
+ title: Example Queries
18
+ description: This guide shows a variety of example queries using the DB gem.
19
+ - path: datatypes.md
20
+ title: Data Types
21
+ description: This guide explains about SQL data types, and how they are used by
22
+ the DB gem.
data/lib/db/client.rb CHANGED
@@ -3,11 +3,10 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2020-2024, by Samuel Williams.
5
5
 
6
- require 'async/pool/controller'
6
+ require "async/pool/controller"
7
7
 
8
- require_relative 'context/transient'
9
- require_relative 'context/session'
10
- require_relative 'context/transaction'
8
+ require_relative "context/session"
9
+ require_relative "context/transaction"
11
10
 
12
11
  module DB
13
12
  # Binds a connection pool to the specified adapter.
@@ -29,19 +28,6 @@ module DB
29
28
  @pool.close
30
29
  end
31
30
 
32
- # Acquire a generic context which will acquire a connection on demand.
33
- def context(**options)
34
- context = Context::Transient.new(@pool, **options)
35
-
36
- return context unless block_given?
37
-
38
- begin
39
- yield context
40
- ensure
41
- context.close
42
- end
43
- end
44
-
45
31
  # Acquires a connection and sends the specified statement if given.
46
32
  # @parameters statement [String | Nil] An optional statement to send.
47
33
  # @yields {|session| ...} A connected session if a block is given. Implicitly closed.
@@ -53,12 +39,16 @@ module DB
53
39
  return session unless block_given?
54
40
 
55
41
  begin
42
+ session.connect!
43
+
56
44
  yield session
57
45
  ensure
58
46
  session.close
59
47
  end
60
48
  end
61
49
 
50
+ alias context session
51
+
62
52
  # Acquires a connection and starts a transaction.
63
53
  # @parameters statement [String | Nil] An optional statement to send. Defaults to `"BEGIN"`.
64
54
  # @yields {|session| ...} A connected session if a block is given. Implicitly commits, or aborts the connnection if an exception is raised.
@@ -67,7 +57,7 @@ module DB
67
57
  def transaction(**options)
68
58
  transaction = Context::Transaction.new(@pool, **options)
69
59
 
70
- transaction.call("BEGIN")
60
+ transaction.begin
71
61
 
72
62
  return transaction unless block_given?
73
63
 
@@ -3,10 +3,11 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2020-2024, by Samuel Williams.
5
5
 
6
- require_relative '../query'
7
- require_relative '../records'
6
+ require_relative "../query"
7
+ require_relative "../records"
8
8
 
9
9
  module DB
10
+ # Provides context for database operations including sessions and transactions.
10
11
  module Context
11
12
  # A connected context for sending queries and reading results.
12
13
  class Session
@@ -16,12 +17,11 @@ module DB
16
17
  @connection = nil
17
18
  end
18
19
 
19
- def connection?
20
- @connection != nil
21
- end
20
+ attr :pool
21
+ attr :connection
22
22
 
23
- # Lazy initialize underlying connection.
24
- def connection
23
+ # Pin a connection to the current session.
24
+ def connect!
25
25
  @connection ||= @pool.acquire
26
26
  end
27
27
 
@@ -33,33 +33,63 @@ module DB
33
33
  end
34
34
  end
35
35
 
36
+ # Check if the session connection is closed.
37
+ # @returns [Boolean] True if the connection is closed (nil), false otherwise.
36
38
  def closed?
37
39
  @connection.nil?
38
40
  end
39
41
 
40
- def query(fragment = String.new, **parameters)
41
- if parameters.empty?
42
- Query.new(self, fragment)
42
+ # Execute a block with a database connection, acquiring one if necessary.
43
+ # @yields {|connection| ...} The connection block.
44
+ # @parameter connection [Object] The database connection object.
45
+ def with_connection(&block)
46
+ if @connection
47
+ yield @connection
43
48
  else
44
- Query.new(self).interpolate(fragment, **parameters)
49
+ @pool.acquire do |connection|
50
+ @connection = connection
51
+
52
+ yield connection
53
+ ensure
54
+ @connection = nil
55
+ end
45
56
  end
46
57
  end
47
58
 
48
- def clause(fragment = String.new)
49
- Query.new(self, fragment)
50
- end
51
-
52
59
  # Send a query to the server.
53
60
  # @parameter statement [String] The SQL query to send.
54
61
  def call(statement, **options)
55
- connection = self.connection
56
-
57
- connection.send_query(statement, **options)
58
-
59
- if block_given?
60
- yield connection
61
- elsif result = connection.next_result
62
- return Records.wrap(result)
62
+ self.with_connection do |connection|
63
+ connection.send_query(statement, **options)
64
+
65
+ if block_given?
66
+ yield connection
67
+ elsif result = connection.next_result
68
+ return Records.wrap(result)
69
+ end
70
+ end
71
+ end
72
+
73
+ # Create a new query builder with optional initial fragment and parameters.
74
+ # @parameter fragment [String] Initial SQL fragment for the query.
75
+ # @parameter parameters [Hash] Parameters for interpolation into the fragment.
76
+ # @returns [Query] A new query builder instance.
77
+ def query(fragment = String.new, **parameters)
78
+ with_connection do
79
+ if parameters.empty?
80
+ Query.new(self, fragment)
81
+ else
82
+ Query.new(self).interpolate(fragment, **parameters)
83
+ end
84
+ end
85
+ end
86
+
87
+ # Create a new query builder with an initial clause fragment.
88
+ # @parameter fragment [String] Initial SQL clause fragment.
89
+ # @returns [Query] A new query builder instance.
90
+ def clause(fragment = String.new)
91
+ with_connection do
92
+ Query.new(self, fragment)
63
93
  end
64
94
  end
65
95
  end
@@ -3,17 +3,26 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2020-2024, by Samuel Williams.
5
5
 
6
- require_relative 'session'
6
+ require_relative "session"
7
7
 
8
8
  module DB
9
9
  module Context
10
+ # A database transaction context that extends Session with transaction management capabilities.
10
11
  class Transaction < Session
12
+ # Begin a transaction.
13
+ def begin
14
+ self.connect!
15
+ self.call("BEGIN")
16
+ end
17
+
11
18
  # Commit the transaction and return the connection to the connection pool.
12
19
  def commit
13
20
  self.call("COMMIT")
14
21
  self.close
15
22
  end
16
23
 
24
+ # Commit the transaction if it's still open, otherwise do nothing.
25
+ # This is a safe version of commit that checks if the transaction is still active.
17
26
  def commit?
18
27
  unless self.closed?
19
28
  self.commit
data/lib/db/query.rb CHANGED
@@ -6,6 +6,9 @@
6
6
  module DB
7
7
  # Represents one or more identifiers for databases, tables or columns.
8
8
  class Identifier < Array
9
+ # Convert various input types to an Identifier instance.
10
+ # @parameter name_or_identifier [Identifier, Array, Symbol, String] The value to convert.
11
+ # @returns [Identifier] An Identifier instance.
9
12
  def self.coerce(name_or_identifier)
10
13
  case name_or_identifier
11
14
  when Identifier
@@ -19,6 +22,8 @@ module DB
19
22
  end
20
23
  end
21
24
 
25
+ # Append this identifier to the provided query builder.
26
+ # @parameter query [Query] The query builder to append to.
22
27
  def append_to(query)
23
28
  query.identifier(self)
24
29
  end
@@ -38,7 +43,7 @@ module DB
38
43
  # @parameter value [String] A raw SQL string, e.g. `WHERE x > 10`.
39
44
  # @returns [Query] The mutable query itself.
40
45
  def clause(value)
41
- @buffer << ' ' unless @buffer.end_with?(' ') || @buffer.empty?
46
+ @buffer << " " unless @buffer.end_with?(" ") || @buffer.empty?
42
47
 
43
48
  @buffer << value
44
49
 
@@ -50,7 +55,7 @@ module DB
50
55
  # @parameter value [Object] Any kind of object, passed to the underlying database connection for conversion to a string representation.
51
56
  # @returns [Query] The mutable query itself.
52
57
  def literal(value)
53
- @buffer << ' ' unless @buffer.end_with?(' ')
58
+ @buffer << " " unless @buffer.end_with?(" ")
54
59
 
55
60
  @connection.append_literal(value, @buffer)
56
61
 
@@ -62,7 +67,7 @@ module DB
62
67
  # @parameter value [String | Symbol | DB::Identifier] Passed to the underlying database connection for conversion to a string representation.
63
68
  # @returns [Query] The mutable query itself.
64
69
  def identifier(value)
65
- @buffer << ' ' unless @buffer.end_with?(' ')
70
+ @buffer << " " unless @buffer.end_with?(" ")
66
71
 
67
72
  @connection.append_identifier(value, @buffer)
68
73
 
@@ -90,6 +95,10 @@ module DB
90
95
  return self
91
96
  end
92
97
 
98
+ # Generate a key column expression based on the connection's requirements.
99
+ # @parameter arguments [Array] Arguments passed to the connection's key_column method.
100
+ # @parameter options [Hash] Options passed to the connection's key_column method.
101
+ # @returns [Query] The mutable query itself.
93
102
  def key_column(*arguments, **options)
94
103
  @buffer << @connection.key_column(*arguments, **options)
95
104
 
@@ -103,10 +112,14 @@ module DB
103
112
  @context.call(@buffer, &block)
104
113
  end
105
114
 
115
+ # Get the string representation of the query buffer.
116
+ # @returns [String] The accumulated query string.
106
117
  def to_s
107
118
  @buffer
108
119
  end
109
120
 
121
+ # Inspect the query instance showing the class and current buffer contents.
122
+ # @returns [String] A string representation for debugging.
110
123
  def inspect
111
124
  "\#<#{self.class} #{@buffer.inspect}>"
112
125
  end
data/lib/db/records.rb CHANGED
@@ -6,6 +6,9 @@
6
6
  module DB
7
7
  # A buffer of records.
8
8
  class Records
9
+ # Wrap a database result into a Records instance.
10
+ # @parameter result [Object] The database result object with field_count, field_names, and to_a methods.
11
+ # @returns [Records, Nil] A Records instance or nil if there are no columns.
9
12
  def self.wrap(result)
10
13
  # We want to avoid extra memory allocations when there are no columns:
11
14
  if result.field_count == 0
@@ -15,11 +18,16 @@ module DB
15
18
  return self.new(result.field_names, result.to_a)
16
19
  end
17
20
 
21
+ # Initialize a new Records instance with columns and rows.
22
+ # @parameter columns [Array] Array of column names.
23
+ # @parameter rows [Array] Array of row data.
18
24
  def initialize(columns, rows)
19
25
  @columns = columns
20
26
  @rows = rows
21
27
  end
22
28
 
29
+ # Freeze the Records instance and its internal data structures.
30
+ # @returns [Records] The frozen Records instance.
23
31
  def freeze
24
32
  return self if frozen?
25
33
 
@@ -32,6 +40,8 @@ module DB
32
40
  attr :columns
33
41
  attr :rows
34
42
 
43
+ # Get the rows as an array.
44
+ # @returns [Array] The array of row data.
35
45
  def to_a
36
46
  @rows
37
47
  end
data/lib/db/version.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2020-2024, by Samuel Williams.
5
5
 
6
+ # @namespace
6
7
  module DB
7
- VERSION = "0.11.0"
8
+ VERSION = "0.13.0"
8
9
  end
data/lib/db.rb CHANGED
@@ -3,6 +3,6 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2020-2024, by Samuel Williams.
5
5
 
6
- require_relative 'db/version'
7
- require_relative 'db/adapters'
8
- require_relative 'db/client'
6
+ require_relative "db/version"
7
+ require_relative "db/adapters"
8
+ require_relative "db/client"
data/readme.md CHANGED
@@ -21,6 +21,20 @@ Please see the [project documentation](https://socketry.github.io/db/) for more
21
21
 
22
22
  - [Data Types](https://socketry.github.io/db/guides/datatypes/index) - This guide explains about SQL data types, and how they are used by the DB gem.
23
23
 
24
+ ## Releases
25
+
26
+ Please see the [project releases](https://socketry.github.io/db/releases/index) for all releases.
27
+
28
+ ### v0.13.0
29
+
30
+ ## See Also
31
+
32
+ - [db-postgres](https://github.com/socketry/db-postgres) - Postgres adapter for the DB gem.
33
+ - [db-mariadb](https://github.com/socketry/db-mariadb) - MariaDB/MySQL adapter for the DB gem.
34
+ - [db-model](https://github.com/socketry/db-model) - A simple object relational mapper (ORM) for the DB gem.
35
+ - [db-migrate](https://github.com/socketry/db-migrate) - Database migration tooling for the DB gem.
36
+ - [db-active\_record](https://github.com/socketry/db-active_record) - An ActiveRecord adapter for the DB gem.
37
+
24
38
  ## Contributing
25
39
 
26
40
  We welcome contributions to this project.
data/releases.md ADDED
@@ -0,0 +1,3 @@
1
+ # Releases
2
+
3
+ ## v0.13.0
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,11 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: db
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain:
11
10
  - |
@@ -37,7 +36,7 @@ cert_chain:
37
36
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
38
37
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
39
38
  -----END CERTIFICATE-----
40
- date: 2024-07-27 00:00:00.000000000 Z
39
+ date: 1980-01-02 00:00:00.000000000 Z
41
40
  dependencies:
42
41
  - !ruby/object:Gem::Dependency
43
42
  name: async-pool
@@ -53,23 +52,26 @@ dependencies:
53
52
  - - ">="
54
53
  - !ruby/object:Gem::Version
55
54
  version: '0'
56
- description:
57
- email:
58
55
  executables: []
59
56
  extensions: []
60
57
  extra_rdoc_files: []
61
58
  files:
59
+ - context/datatypes.md
60
+ - context/example-queries.md
61
+ - context/executing-queries.md
62
+ - context/getting-started.md
63
+ - context/index.yaml
62
64
  - lib/db.rb
63
65
  - lib/db/adapters.rb
64
66
  - lib/db/client.rb
65
67
  - lib/db/context/session.rb
66
68
  - lib/db/context/transaction.rb
67
- - lib/db/context/transient.rb
68
69
  - lib/db/query.rb
69
70
  - lib/db/records.rb
70
71
  - lib/db/version.rb
71
72
  - license.md
72
73
  - readme.md
74
+ - releases.md
73
75
  homepage: https://github.com/socketry/db
74
76
  licenses:
75
77
  - MIT
@@ -77,7 +79,6 @@ metadata:
77
79
  documentation_uri: https://socketry.github.io/db/
78
80
  funding_uri: https://github.com/sponsors/ioquatix
79
81
  source_code_uri: https://github.com/socketry/db.git
80
- post_install_message:
81
82
  rdoc_options: []
82
83
  require_paths:
83
84
  - lib
@@ -85,15 +86,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
85
86
  requirements:
86
87
  - - ">="
87
88
  - !ruby/object:Gem::Version
88
- version: '3.1'
89
+ version: '3.2'
89
90
  required_rubygems_version: !ruby/object:Gem::Requirement
90
91
  requirements:
91
92
  - - ">="
92
93
  - !ruby/object:Gem::Version
93
94
  version: '0'
94
95
  requirements: []
95
- rubygems_version: 3.5.11
96
- signing_key:
96
+ rubygems_version: 3.6.9
97
97
  specification_version: 4
98
98
  summary: A low level database access gem.
99
99
  test_files: []
metadata.gz.sig CHANGED
Binary file
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2020-2024, by Samuel Williams.
5
-
6
- require_relative 'session'
7
-
8
- module DB
9
- module Context
10
- # A connected context for sending queries and reading results.
11
- class Transient < Session
12
- def call(statement, **options, &block)
13
- super
14
- ensure
15
- self.close
16
- end
17
- end
18
- end
19
- end