diff --git a/guides/example-queries/readme.md b/guides/example-queries/readme.md new file mode 100644 index 0000000..0370a47 --- /dev/null +++ b/guides/example-queries/readme.md @@ -0,0 +1,263 @@ +# Example Queries + +This guide shows how to perform basic queries with the `db` library. + +## Setup + +~~~ ruby +require 'async' +require 'db/client' +require 'db/postgres' + +client = DB::Client.new(DB::Postgres::Adapter.new( + database: 'test', + host: '172.17.0.3', + password: 'test', + username: 'postgres', +)) +~~~ + +## A simple CREATE, INSERT and SELECT, with raw SQL + +~~~ ruby +Sync do + session = client.session + + create = "CREATE TABLE IF NOT EXISTS my_table (a_timestamp TIMESTAMP NOT NULL)" + session.query(create).call + + insert = "INSERT INTO my_table VALUES (NOW()), ('2022-12-12 12:13:14')" + session.query(insert).call + + result = session.query("SELECT * FROM my_table WHERE a_timestamp > NOW()").call + + Console.info result.field_types.to_s + Console.info result.field_names.to_s + Console.info result.to_a.to_s +ensure + session&.close +end +~~~ + +### Output + +~~~ + 0.01s info: [#] + 0.01s info: ["a_timestamp"] + 0.01s info: [[2022-12-12 12:13:14 UTC]] +~~~ + +## Parameterized CREATE, INSERT and SELECT + +The same process as before, but parameterized. Always use the parameterized form when dealing with untrusted data. + +~~~ ruby +Sync do + session = client.session + + session.clause("CREATE TABLE IF NOT EXISTS") + .identifier(:my_table) + .clause("(") + .identifier(:a_timestamp).clause("TIMESTAMP NOT NULL") + .clause(")") + .call + + session.clause("INSERT INTO") + .identifier(:my_table) + .clause("VALUES (") + .literal("NOW()") + .clause("), (") + .literal("2022-12-12 12:13:14") + .clause(")") + .call + + result = session.clause("SELECT * FROM") + .identifier(:my_table) + .clause("WHERE") + .identifier(:a_timestamp).clause(">").literal("NOW()") + .call + + Console.info result.field_types.to_s + Console.info result.field_names.to_s + Console.info result.to_a.to_s +ensure + session&.close +end +~~~ + +### Output + +~~~ + 0.01s info: [#] + 0.01s info: ["a_timestamp"] + 0.01s info: [[2022-12-12 12:13:14 UTC]] +~~~ + +## A parameterized SELECT + +~~~ ruby +Sync do |task| + session = client.session + result = session + .clause("SELECT") + .identifier(:column_one) + .clause(",") + .identifier(:column_two) + .clause("FROM") + .identifier(:another_table) + .clause("WHERE") + .identifier(:id) + .clause("=") + .literal(42) + .call + + Console.info "#{result.field_names}" + Console.info "#{result.to_a}" +end +~~~ + +### Output + +~~~ + 0.01s info: ["column_one", "column_two"] + 0.01s info: [["foo", "bar"], ["baz", "qux"]] +~~~ + +## Concurrent queries + +(Simulating slow queries with `PG_SLEEP`) + +~~~ ruby +Sync do |task| + start = Time.now + tasks = 10.times.map do + task.async do + session = client.session + result = session.query("SELECT PG_SLEEP(10)").call + result.to_a + ensure + session&.close + end + end + + results = tasks.map(&:wait) + + Console.info "Elapsed time: #{Time.now - start}s" +end +~~~ + +### Output + +~~~ +10.05s info: Elapsed time: 10.049756222s +~~~ + +## Limited to 3 connections + +(Simulating slow queries with `PG_SLEEP`) + +~~~ ruby +require 'async/semaphore' + +Sync do + semaphore = Async::Semaphore.new(3) + tasks = 10.times.map do |i| + semaphore.async do + session = client.session + Console.info "Starting task #{i}" + result = session.query("SELECT PG_SLEEP(10)").call + result.to_a + ensure + session&.close + end + end + + results = tasks.map(&:wait) + Console.info "Done" +end +~~~ + +### Output + +~~~ + 0.0s info: Starting task 0 + 0.0s info: Starting task 1 + 0.0s info: Starting task 2 +10.02s info: Completed task 0 after 10.017388464s +10.02s info: Starting task 3 +10.02s info: Completed task 1 after 10.02111175s +10.02s info: Starting task 4 +10.03s info: Completed task 2 after 10.027889587s +10.03s info: Starting task 5 +20.03s info: Completed task 3 after 10.011089096s +20.03s info: Starting task 6 +20.03s info: Completed task 4 after 10.008169111s +20.03s info: Starting task 7 +20.04s info: Completed task 5 after 10.007644749s +20.04s info: Starting task 8 +30.04s info: Completed task 6 after 10.011244562s +30.04s info: Starting task 9 +30.04s info: Completed task 7 after 10.011565997s +30.04s info: Completed task 8 after 10.004611464s +40.05s info: Completed task 9 after 10.008239803s +40.05s info: Done +~~~ + +## Sequential vs Concurrent INSERTs + +~~~ ruby +DATA = 1_000_000.times.map { SecureRandom.hex } + +def setup_tables(client) + session = client.session + + create = "CREATE TABLE IF NOT EXISTS salts (salt CHAR(32))" + session.query(create).call + + truncate = "TRUNCATE TABLE salts" + session.query(truncate).call + + session.close +end + +def chunked_insert(rows, client, task=Async::Task.current) + task.async do + session = client.session + rows.each_slice(1000) do |slice| + insert = "INSERT INTO salts VALUES " + slice.map { |x| "('#{x}')" }.join(",") + session.query(insert).call + end + ensure + session&.close + end +end + +Sync do + Console.info "Setting up tables" + setup_tables(client) + Console.info "Done" + + start = Time.now + Console.info "Starting sequential insert" + chunked_insert(DATA, client).wait + Console.info "Completed sequential insert in #{Time.now - start}s" + + start = Time.now + Console.info "Starting concurrent insert" + DATA.each_slice(10_000).map do |slice| + chunked_insert(slice, client) + end.each(&:wait) + Console.info "Completed concurrent insert in #{Time.now - start}s" +end +~~~ + +### Output + +~~~ + 1.45s info: Setting up tables + 1.49s info: Done + 1.49s info: Starting sequential insert + 8.49s info: Completed sequential insert in 7.006533933s + 8.49s info: Starting concurrent insert + 9.92s info: Completed concurrent insert in 1.431470847s +~~~ diff --git a/guides/executing-queries/readme.md b/guides/executing-queries/readme.md index 0e727aa..648013f 100644 --- a/guides/executing-queries/readme.md +++ b/guides/executing-queries/readme.md @@ -1,6 +1,8 @@ # Executing Queries -This guide explains how to escape and execute queries. In order to execute a query, you need a connection. Database connections are stateful, and this state is encapsulated by a context. +This guide explains how to escape and execute queries. + +In order to execute a query, you need a connection. Database connections are stateful, and this state is encapsulated by a context. ## Contexts diff --git a/guides/getting-started/readme.md b/guides/getting-started/readme.md index 726eb35..037a00a 100644 --- a/guides/getting-started/readme.md +++ b/guides/getting-started/readme.md @@ -41,12 +41,11 @@ Sync do session = client.session # Execute the query and get a result set: - result = session.call("SHOW SERVER_VERSION") + result = session.call("SELECT VERSION()") # Convert the result set to an array and print it out: pp result.to_a - # => [["12.3"]] - + # => [["PostgreSQL 16.3 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 14.1.1 20240522, 64-bit"]] ensure # Return the connection to the client connection pool: session.close @@ -82,13 +81,8 @@ Sync do # Convert the result set to an array and print it out: pp result.to_a # => [["10.4.13-MariaDB"]] - ensure # Return the connection to the client connection pool: session.close end ~~~ - -## Streaming Results - -Some database adaptors may stream results row by row. This reduces memory usage and latency. Because of that, you may only call {ruby #each} or {ruby #to_a} on a query result once. diff --git a/guides/links.yaml b/guides/links.yaml index 75f8572..6e28ef5 100644 --- a/guides/links.yaml +++ b/guides/links.yaml @@ -2,3 +2,5 @@ getting-started: order: 1 executing-queries: order: 2 +example-queries: + order: 3 diff --git a/readme.md b/readme.md index c0c3cb7..b9c3303 100644 --- a/readme.md +++ b/readme.md @@ -11,7 +11,13 @@ Provides event-driven asynchronous drivers for various database adaptors, includ ## Usage -Please see the [project documentation](https://socketry.github.io/db). +Please see the [project documentation](https://socketry.github.io/db/) for more details. + + - [Getting Started](https://socketry.github.io/db/guides/getting-started/index) - This guide explains how to use `db` for database queries. + + - [Executing Queries](https://socketry.github.io/db/guides/executing-queries/index) - This guide explains how to escape and execute queries. + + - [Example Queries](https://socketry.github.io/db/guides/example-queries/index) - This guide shows how to perform basic queries with the `db` library. ## Contributing