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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/datatypes.md +28 -0
- data/context/example-queries.md +263 -0
- data/context/executing-queries.md +158 -0
- data/context/getting-started.md +88 -0
- data/context/index.yaml +22 -0
- data/lib/db/client.rb +4 -0
- data/lib/db/context/session.rb +13 -0
- data/lib/db/context/transaction.rb +3 -0
- data/lib/db/features.rb +89 -0
- data/lib/db/query.rb +13 -0
- data/lib/db/records.rb +10 -0
- data/lib/db/version.rb +2 -1
- data/lib/db.rb +1 -0
- data/readme.md +6 -0
- data/releases.md +3 -0
- data.tar.gz.sig +0 -0
- metadata +11 -9
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e32753720467fe5085b599ca44812117efa453bbb6ae959e4bb8990cab6d4087
|
4
|
+
data.tar.gz: b3a6480f045f16d015c0d9b105e361b6eddbbff489a45ac3e9145a49011ea07e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
+
~~~
|
data/context/index.yaml
ADDED
@@ -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
data/lib/db/context/session.rb
CHANGED
@@ -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
|
data/lib/db/features.rb
ADDED
@@ -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
data/lib/db.rb
CHANGED
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
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.
|
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:
|
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.
|
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.
|
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
|