db 0.12.0 → 0.14.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: 190f7bf46c9e6f9e7af5b291eb71d07db32a6db3ac8997bc73dd50a058327744
4
- data.tar.gz: fac1ca594b8f900b4ff7ac6e68cca1930c24fbc199a5187176f8d5e30e9006e7
3
+ metadata.gz: e32753720467fe5085b599ca44812117efa453bbb6ae959e4bb8990cab6d4087
4
+ data.tar.gz: b3a6480f045f16d015c0d9b105e361b6eddbbff489a45ac3e9145a49011ea07e
5
5
  SHA512:
6
- metadata.gz: 1d0e7f56d1ea4f323e3c7d1525d3f4f2ef35cc8c9509b3d4f181ac3e992cd9c3ae954c696acc0b898ca1c196fd5638f46d0dac5ac36bdc99d9f989e081f24761
7
- data.tar.gz: a5bca6583eeff6c0b1eb4debb7b9b8ff3d3011de914394742905bfad795e9f02ed6f65d452a36a9953db13016df67fffc546430df1f8269c60763fb7bfcfe0dc
6
+ metadata.gz: 78a879265c001e502faa4c8227a0f3457501bee7b55ac15eb4f7fe7ed267cdba0c476e411bc862dfe310b44d64fadcdb02f358fb0038f22fe3a56f4cb60c8ce9
7
+ data.tar.gz: 3b8be8be3b52d3f0d1b3cddbac2149e664a29fffed62d994eb8759f11ebb264df12ed6e620f8bf00da68ee1391c5486af59be0f5e6e274716408b585d89e1c74
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
@@ -25,6 +25,10 @@ module DB
25
25
 
26
26
  # Close all open connections in the connection pool.
27
27
  def close
28
+ @pool.wait_until_free do
29
+ Console.warn(self) {"Waiting for #{@adapter} pool to drain: #{@pool}"}
30
+ end
31
+
28
32
  @pool.close
29
33
  end
30
34
 
@@ -7,6 +7,7 @@ require_relative "../query"
7
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
@@ -32,10 +33,15 @@ module DB
32
33
  end
33
34
  end
34
35
 
36
+ # Check if the session connection is closed.
37
+ # @returns [Boolean] True if the connection is closed (nil), false otherwise.
35
38
  def closed?
36
39
  @connection.nil?
37
40
  end
38
41
 
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.
39
45
  def with_connection(&block)
40
46
  if @connection
41
47
  yield @connection
@@ -64,6 +70,10 @@ module DB
64
70
  end
65
71
  end
66
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.
67
77
  def query(fragment = String.new, **parameters)
68
78
  with_connection do
69
79
  if parameters.empty?
@@ -74,6 +84,9 @@ module DB
74
84
  end
75
85
  end
76
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.
77
90
  def clause(fragment = String.new)
78
91
  with_connection do
79
92
  Query.new(self, fragment)
@@ -7,6 +7,7 @@ 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
11
12
  # Begin a transaction.
12
13
  def begin
@@ -20,6 +21,8 @@ module DB
20
21
  self.close
21
22
  end
22
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.
23
26
  def commit?
24
27
  unless self.closed?
25
28
  self.commit
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ module DB
7
+ # Standardized feature detection for database adapters.
8
+ # All features default to false, and adapters can enable specific capabilities.
9
+ class Features
10
+ def initialize(**features)
11
+ @features = features
12
+ end
13
+
14
+ # Check if a specific feature is enabled.
15
+ def enabled?(feature)
16
+ @features.fetch(feature, false)
17
+ end
18
+
19
+ # Get all enabled features.
20
+ def enabled_features
21
+ @features.select{|_, enabled| enabled}.keys
22
+ end
23
+
24
+ # Create a new Features instance with additional or modified features.
25
+ def with(**additional_features)
26
+ self.class.new(**@features, **additional_features)
27
+ end
28
+
29
+ # PostgreSQL-style column type modification: ALTER COLUMN name TYPE type USING expression.
30
+ def alter_column_type?
31
+ @features.fetch(:alter_column_type, false)
32
+ end
33
+
34
+ # MySQL-style column modification: MODIFY COLUMN name type.
35
+ def modify_column?
36
+ @features.fetch(:modify_column, false)
37
+ end
38
+
39
+ # Support for USING clause in column type changes.
40
+ def using_clause?
41
+ @features.fetch(:using_clause, false)
42
+ end
43
+
44
+ # Support for IF EXISTS/IF NOT EXISTS clauses.
45
+ def conditional_operations?
46
+ @features.fetch(:conditional_operations, false)
47
+ end
48
+
49
+ # Schema operations can be rolled back within transactions.
50
+ def transactional_schema?
51
+ @features.fetch(:transactional_schema, false)
52
+ end
53
+
54
+ # Multiple operations can be combined in a single ALTER TABLE statement.
55
+ def batch_alter_table?
56
+ @features.fetch(:batch_alter_table, false)
57
+ end
58
+
59
+ # Support for concurrent/online schema changes.
60
+ def concurrent_schema?
61
+ @features.fetch(:concurrent_schema, false)
62
+ end
63
+
64
+ # Support for adding constraints with validation deferred.
65
+ def deferred_constraints?
66
+ @features.fetch(:deferred_constraints, false)
67
+ end
68
+
69
+ # PostgreSQL-style SERIAL/BIGSERIAL auto-increment columns.
70
+ def serial_columns?
71
+ @features.fetch(:serial_columns, false)
72
+ end
73
+
74
+ # MySQL-style AUTO_INCREMENT auto-increment columns.
75
+ def auto_increment?
76
+ @features.fetch(:auto_increment, false)
77
+ end
78
+
79
+ # SQLite-style INTEGER PRIMARY KEY auto-increment.
80
+ def integer_primary_key_autoincrement?
81
+ @features.fetch(:integer_primary_key_autoincrement, false)
82
+ end
83
+
84
+ # Support for IDENTITY columns (SQL Server/newer PostgreSQL).
85
+ def identity_columns?
86
+ @features.fetch(:identity_columns, false)
87
+ end
88
+ end
89
+ end
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
@@ -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.12.0"
8
+ VERSION = "0.14.0"
8
9
  end
data/lib/db.rb CHANGED
@@ -6,3 +6,4 @@
6
6
  require_relative "db/version"
7
7
  require_relative "db/adapters"
8
8
  require_relative "db/client"
9
+ require_relative "db/features"
data/readme.md CHANGED
@@ -21,6 +21,12 @@ 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
+
24
30
  ## See Also
25
31
 
26
32
  - [db-postgres](https://github.com/socketry/db-postgres) - Postgres adapter for the DB gem.
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.12.0
4
+ version: 0.14.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-09-21 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,22 +52,27 @@ 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
69
+ - lib/db/features.rb
67
70
  - lib/db/query.rb
68
71
  - lib/db/records.rb
69
72
  - lib/db/version.rb
70
73
  - license.md
71
74
  - readme.md
75
+ - releases.md
72
76
  homepage: https://github.com/socketry/db
73
77
  licenses:
74
78
  - MIT
@@ -76,7 +80,6 @@ metadata:
76
80
  documentation_uri: https://socketry.github.io/db/
77
81
  funding_uri: https://github.com/sponsors/ioquatix
78
82
  source_code_uri: https://github.com/socketry/db.git
79
- post_install_message:
80
83
  rdoc_options: []
81
84
  require_paths:
82
85
  - lib
@@ -84,15 +87,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
84
87
  requirements:
85
88
  - - ">="
86
89
  - !ruby/object:Gem::Version
87
- version: '3.1'
90
+ version: '3.2'
88
91
  required_rubygems_version: !ruby/object:Gem::Requirement
89
92
  requirements:
90
93
  - - ">="
91
94
  - !ruby/object:Gem::Version
92
95
  version: '0'
93
96
  requirements: []
94
- rubygems_version: 3.5.11
95
- signing_key:
97
+ rubygems_version: 3.6.9
96
98
  specification_version: 4
97
99
  summary: A low level database access gem.
98
100
  test_files: []
metadata.gz.sig CHANGED
Binary file